Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve source code generation #1270

Merged
merged 9 commits into from
Jun 12, 2024
5 changes: 5 additions & 0 deletions .changeset/long-plums-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sit-onyx/storybook-utils": minor
---

feat: improve code snippet generation
18 changes: 13 additions & 5 deletions packages/storybook-utils/src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export const sourceCodeTransformer = (
Object.entries(ALL_ICONS).forEach(([iconName, iconContent]) => {
const importName = getIconImportName(iconName);
const singleQuotedIconContent = `'${replaceAll(iconContent, '"', "\\'")}'`;
const escapedIconContent = `"${replaceAll(iconContent, '"', '\\"')}"`;

if (code.includes(iconContent)) {
code = code.replace(new RegExp(` (\\S+)=['"]${iconContent}['"]`), ` :$1="${importName}"`);
Expand All @@ -162,18 +163,25 @@ export const sourceCodeTransformer = (
// support icons inside objects
code = code.replace(singleQuotedIconContent, importName);
iconImports.push(`import ${importName} from "@sit-onyx/icons/${iconName}.svg?raw";`);
} else if (code.includes(escapedIconContent)) {
// support icons inside objects
code = code.replace(escapedIconContent, importName);
iconImports.push(`import ${importName} from "@sit-onyx/icons/${iconName}.svg?raw";`);
}
});

if (iconImports.length > 0) {
return `<script lang="ts" setup>
if (iconImports.length === 0) return code;

if (code.startsWith("<script")) {
const index = code.indexOf("\n");
return code.slice(0, index) + iconImports.join("\n") + "\n" + code.slice(index);
}

return `<script lang="ts" setup>
${iconImports.join("\n")}
</script>

${code}`;
}

return code;
};

/**
Expand Down
115 changes: 105 additions & 10 deletions packages/storybook-utils/src/source-code-generator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@
import { expect, test } from "vitest";
import { h } from "vue";
import {
extractSlotNames,
generatePropsSourceCode,
generateSlotSourceCode,
generateSourceCode,
parseDocgenInfo,
type SourceCodeGeneratorContext,
} from "./source-code-generator";

test("should generate source code for props", () => {
const slots = ["default", "testSlot"];
const ctx: SourceCodeGeneratorContext = {
scriptVariables: {},
imports: {},
};

const code = generatePropsSourceCode(
{
Expand All @@ -24,23 +29,36 @@ test("should generate source code for props", () => {
f: [1, 2, 3],
g: {
g1: "foo",
b2: 42,
g2: 42,
},
h: undefined,
i: null,
j: "",
k: BigInt(9007199254740991),
l: Symbol(),
m: Symbol("foo"),
modelValue: "test-v-model",
otherModelValue: 42,
default: "default slot",
testSlot: "test slot",
},
slots,
["default", "testSlot"],
["update:modelValue", "update:otherModelValue"],
ctx,
);

expect(code).toBe(
`a="foo" b='"I am double quoted"' :c="42" d :e="false" :f="[1,2,3]" :g="{'g1':'foo','b2':42}" :k="BigInt(9007199254740991)" :l="Symbol()" :m="Symbol('foo')"`,
`a="foo" b='"I am double quoted"' :c="42" d :e="false" :f="f" :g="g" :k="BigInt(9007199254740991)" :l="Symbol()" :m="Symbol('foo')" v-model="modelValue" v-model:otherModelValue="otherModelValue"`,
);

expect(ctx.scriptVariables).toStrictEqual({
f: `[1,2,3]`,
g: `{"g1":"foo","g2":42}`,
modelValue: 'ref("test-v-model")',
otherModelValue: "ref(42)",
});

expect(Array.from(ctx.imports.vue.values())).toStrictEqual(["ref"]);
});

test("should generate source code for slots", () => {
Expand Down Expand Up @@ -111,7 +129,10 @@ child 2</template>

<template #m>{{ BigInt(9007199254740991) }}</template>`;

let actualCode = generateSlotSourceCode(slots, Object.keys(slots));
let actualCode = generateSlotSourceCode(slots, Object.keys(slots), {
scriptVariables: {},
imports: {},
});
expect(actualCode).toBe(expectedCode);

// should generate the same code if getters/functions are used to return the slot content
Expand All @@ -122,10 +143,70 @@ child 2</template>
return obj;
}, {});

actualCode = generateSlotSourceCode(slotsWithGetters, Object.keys(slotsWithGetters));
actualCode = generateSlotSourceCode(slotsWithGetters, Object.keys(slotsWithGetters), {
scriptVariables: {},
imports: {},
});
expect(actualCode).toBe(expectedCode);
});

test("should generate source code for slots with bindings", () => {
type TestBindings = {
foo: string;
bar?: number;
};

const slots = {
a: ({ foo, bar }: TestBindings) => `Slot with bindings ${foo} and ${bar}`,
b: ({ foo }: TestBindings) => h("a", { href: foo, target: foo }, `Test link: ${foo}`),
};

const expectedCode = `<template #a="{ foo, bar }">Slot with bindings {{ foo }} and {{ bar }}</template>

<template #b="{ foo }"><a :href="foo" :target="foo">Test link: {{ foo }}</a></template>`;

const actualCode = generateSlotSourceCode(slots, Object.keys(slots), {
imports: {},
scriptVariables: {},
});
expect(actualCode).toBe(expectedCode);
});

test("should generate source code with <script setup> block", () => {
const actualCode = generateSourceCode({
title: "MyComponent",
component: {
__docgenInfo: {
slots: [{ name: "mySlot" }],
events: [{ name: "update:c" }],
},
},
args: {
a: 42,
b: "foo",
c: [1, 2, 3],
d: { bar: "baz" },
mySlot: () => h("div", { test: [1, 2], d: { nestedProp: "foo" } }),
},
});

expect(actualCode).toBe(`<script lang="ts" setup>
import { ref } from "vue";

const c = ref([1,2,3]);

const d = {"bar":"baz"};

const d1 = {"nestedProp":"foo"};

const test = [1,2];
</script>

<template>
<MyComponent :a="42" b="foo" v-model:c="c" :d="d"> <template #mySlot><div :d="d1" :test="test" /></template> </MyComponent>
</template>`);
});

test.each([
{ __docgenInfo: "invalid-value", slotNames: [] },
{ __docgenInfo: {}, slotNames: [] },
Expand All @@ -135,7 +216,21 @@ test.each([
__docgenInfo: { slots: [{ name: "slot-1" }, { name: "slot-2" }, { notName: "slot-3" }] },
slotNames: ["slot-1", "slot-2"],
},
])("should extract slots names from __docgenInfo", ({ __docgenInfo, slotNames }) => {
const actualNames = extractSlotNames({ __docgenInfo });
expect(actualNames).toStrictEqual(slotNames);
])("should parse slots names from __docgenInfo", ({ __docgenInfo, slotNames }) => {
const docgenInfo = parseDocgenInfo({ __docgenInfo });
expect(docgenInfo.slotNames).toStrictEqual(slotNames);
});

test.each([
{ __docgenInfo: "invalid-value", eventNames: [] },
{ __docgenInfo: {}, eventNames: [] },
{ __docgenInfo: { events: "invalid-value" }, eventNames: [] },
{ __docgenInfo: { events: ["invalid-value"] }, eventNames: [] },
{
__docgenInfo: { events: [{ name: "event-1" }, { name: "event-2" }, { notName: "event-3" }] },
eventNames: ["event-1", "event-2"],
},
])("should parse event names from __docgenInfo", ({ __docgenInfo, eventNames }) => {
const docgenInfo = parseDocgenInfo({ __docgenInfo });
expect(docgenInfo.eventNames).toStrictEqual(eventNames);
});
Loading