diff --git a/code/renderers/vue3/src/docs/source-code-generator.test.ts b/code/renderers/vue3/src/docs/source-code-generator.test.ts
deleted file mode 100644
index 63e99eeaef15..000000000000
--- a/code/renderers/vue3/src/docs/source-code-generator.test.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-import { expect, test } from 'vitest';
-import { h } from 'vue';
-import {
- extractSlotNames,
- generatePropsSourceCode,
- generateSlotSourceCode,
-} from './source-code-generator';
-
-test('should generate source code for props', () => {
- const slots = ['default', 'testSlot'];
-
- const code = generatePropsSourceCode(
- {
- a: 'foo',
- b: '"I am double quoted"',
- c: 42,
- d: true,
- e: false,
- f: [1, 2, 3],
- g: {
- g1: 'foo',
- b2: 42,
- },
- h: undefined,
- i: null,
- j: '',
- k: BigInt(9007199254740991),
- l: Symbol(),
- m: Symbol('foo'),
- default: 'default slot',
- testSlot: 'test slot',
- },
- slots
- );
-
- 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')"`
- );
-});
-
-test('should generate source code for slots', () => {
- // slot code generator should support primitive values (string, number etc.)
- // but also VNodes (e.g. created using h()) so custom Vue components can also be used
- // inside slots with proper generated code
-
- const slots = {
- default: 'default content',
- a: 'a content',
- b: 42,
- c: true,
- // single VNode without props
- d: h('div', 'd content'),
- // VNode with props and single child
- e: h('div', { style: 'color:red' }, 'e content'),
- // VNode with props and single child returned as getter
- f: h('div', { style: 'color:red' }, () => 'f content'),
- // VNode with multiple children
- g: h('div', { style: 'color:red' }, [
- 'child 1',
- h('span', { style: 'color:green' }, 'child 2'),
- ]),
- // VNode multiple children but returned as getter
- h: h('div', { style: 'color:red' }, () => [
- 'child 1',
- h('span', { style: 'color:green' }, 'child 2'),
- ]),
- // VNode with multiple and nested children
- i: h('div', { style: 'color:red' }, [
- 'child 1',
- h('span', { style: 'color:green' }, ['nested child 1', h('p', 'nested child 2')]),
- ]),
- j: ['child 1', 'child 2'],
- k: null,
- l: { foo: 'bar' },
- m: BigInt(9007199254740991),
- };
-
- const expectedCode = `default content
-
-a content
-
-42
-
-true
-
-d content
-
-e content
-
-f content
-
-child 1
-child 2
-
-child 1
-child 2
-
-child 1
-
nested child 1
-nested child 2
-
-child 1
-child 2
-
-{"foo":"bar"}
-
-{{ BigInt(9007199254740991) }}`;
-
- let actualCode = generateSlotSourceCode(slots, Object.keys(slots));
- expect(actualCode).toBe(expectedCode);
-
- // should generate the same code if getters/functions are used to return the slot content
- const slotsWithGetters = Object.entries(slots).reduce<
- Record (typeof slots)[keyof typeof slots]>
- >((obj, [slotName, value]) => {
- obj[slotName] = () => value;
- return obj;
- }, {});
-
- actualCode = generateSlotSourceCode(slotsWithGetters, Object.keys(slotsWithGetters));
- 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 = `Slot with bindings {{ foo }} and {{ bar }}
-
-Test link: {{ foo }}`;
-
- const actualCode = generateSlotSourceCode(slots, Object.keys(slots));
- expect(actualCode).toBe(expectedCode);
-});
-
-test.each([
- { __docgenInfo: 'invalid-value', slotNames: [] },
- { __docgenInfo: {}, slotNames: [] },
- { __docgenInfo: { slots: 'invalid-value' }, slotNames: [] },
- { __docgenInfo: { slots: ['invalid-value'] }, slotNames: [] },
- {
- __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);
-});
diff --git a/code/renderers/vue3/src/docs/source-code-generator.ts b/code/renderers/vue3/src/docs/source-code-generator.ts
deleted file mode 100644
index 56ce86035f46..000000000000
--- a/code/renderers/vue3/src/docs/source-code-generator.ts
+++ /dev/null
@@ -1,359 +0,0 @@
-/* eslint-disable no-underscore-dangle */
-import { SNIPPET_RENDERED, SourceType } from '@storybook/docs-tools';
-import { addons } from '@storybook/preview-api';
-import type { VNode } from 'vue';
-import { isVNode, watch } from 'vue';
-import type { Args, Decorator, StoryContext } from '../public-types';
-
-/**
- * Decorator to generate Vue source code for stories.
- */
-export const sourceCodeDecorator: Decorator = (storyFn, ctx) => {
- const story = storyFn();
- if (shouldSkipSourceCodeGeneration(ctx)) return story;
-
- const channel = addons.getChannel();
-
- watch(
- () => ctx.args,
- () => {
- const sourceCode = generateSourceCode(ctx);
-
- channel.emit(SNIPPET_RENDERED, {
- id: ctx.id,
- args: ctx.args,
- source: sourceCode,
- format: 'vue',
- });
- },
- { immediate: true, deep: true }
- );
-
- return story;
-};
-
-/**
- * Generate Vue source code for the given Story.
- * @returns Source code or empty string if source code could not be generated.
- */
-export const generateSourceCode = (
- ctx: Pick
-): string => {
- const componentName = ctx.component?.__name || ctx.title.split('/').at(-1)!;
-
- const slotNames = extractSlotNames(ctx.component);
- const slotSourceCode = generateSlotSourceCode(ctx.args, slotNames);
- const propsSourceCode = generatePropsSourceCode(ctx.args, slotNames);
-
- if (slotSourceCode) {
- return `
- <${componentName} ${propsSourceCode}> ${slotSourceCode} ${componentName}>
- `;
- }
-
- // prefer self closing tag if no slot content exists
- return `
- <${componentName} ${propsSourceCode} />
- `;
-};
-
-/**
- * Checks if the source code generation should be skipped for the given Story context.
- * Will be true if one of the following is true:
- * - view mode is not "docs"
- * - story is no arg story
- * - story has set custom source code via parameters.docs.source.code
- * - story has set source type to "code" via parameters.docs.source.type
- */
-export const shouldSkipSourceCodeGeneration = (context: StoryContext): boolean => {
- const sourceParams = context?.parameters.docs?.source;
- if (sourceParams?.type === SourceType.DYNAMIC) {
- // always render if the user forces it
- return false;
- }
-
- const isArgsStory = context?.parameters.__isArgsStory;
- const isDocsViewMode = context?.viewMode === 'docs';
-
- // never render if the user is forcing the block to render code, or
- // if the user provides code, or if it's not an args story.
- return (
- !isDocsViewMode || !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE
- );
-};
-
-/**
- * Gets all slot names from the `__docgenInfo` of the given component if available.
- * Requires Storybook docs addon to be enabled.
- * Default slot will always be sorted first, remaining slots are sorted alphabetically.
- */
-export const extractSlotNames = (
- component?: StoryContext['component'] & { __docgenInfo?: unknown }
-): string[] => {
- if (!component || !('__docgenInfo' in component)) return [];
-
- // type check __docgenInfo to prevent errors
- if (!component.__docgenInfo || typeof component.__docgenInfo !== 'object') return [];
- if (
- !('slots' in component.__docgenInfo) ||
- !component.__docgenInfo.slots ||
- !Array.isArray(component.__docgenInfo.slots)
- ) {
- return [];
- }
-
- return component.__docgenInfo.slots
- .map((slot) => slot.name)
- .filter((i): i is string => typeof i === 'string')
- .sort((a, b) => {
- if (a === 'default') return -1;
- if (b === 'default') return 1;
- return a.localeCompare(b);
- });
-};
-
-/**
- * Generates the source code for the given Vue component properties.
- *
- * @param args Story args / property values.
- * @param slotNames All slot names of the component. Needed to not generate code for args that are slots.
- * Can be extracted using `extractSlotNames()`.
- */
-export const generatePropsSourceCode = (
- args: Record,
- slotNames: string[]
-): string => {
- const props: string[] = [];
-
- Object.entries(args).forEach(([propName, value]) => {
- // ignore slots
- if (slotNames.includes(propName)) return;
-
- switch (typeof value) {
- case 'string':
- if (value === '') return; // do not render empty strings
-
- if (value.includes('"')) {
- props.push(`${propName}='${value}'`);
- } else {
- props.push(`${propName}="${value}"`);
- }
-
- break;
- case 'number':
- props.push(`:${propName}="${value}"`);
- break;
- case 'bigint':
- props.push(`:${propName}="BigInt(${value.toString()})"`);
- break;
- case 'boolean':
- props.push(value === true ? propName : `:${propName}="false"`);
- break;
- case 'object':
- if (value === null) return; // do not render null values
- props.push(`:${propName}="${JSON.stringify(value).replaceAll('"', "'")}"`);
- break;
- case 'symbol': {
- const symbol = `Symbol(${value.description ? `'${value.description}'` : ''})`;
- props.push(`:${propName}="${symbol}"`);
- break;
- }
- case 'function':
- // TODO: check if functions should be rendered in source code
- break;
- }
- });
-
- return props.join(' ');
-};
-
-/**
- * Generates the source code for the given Vue component slots.
- *
- * @param args Story args.
- * @param slotNames All slot names of the component. Needed to only generate slots and ignore props etc.
- * Can be extracted using `extractSlotNames()`.
- */
-export const generateSlotSourceCode = (args: Args, slotNames: string[]): string => {
- /** List of slot source codes (e.g. Content) */
- const slotSourceCodes: string[] = [];
-
- slotNames.forEach((slotName) => {
- const arg = args[slotName];
- if (!arg) return;
-
- const slotContent = generateSlotChildrenSourceCode([arg]);
- if (!slotContent) return; // do not generate source code for empty slots
-
- const slotBindings = typeof arg === 'function' ? getFunctionParamNames(arg) : [];
-
- if (slotName === 'default' && !slotBindings.length) {
- // do not add unnecessary "" tag since the default slot content without bindings
- // can be put directly into the slot without need of ""
- slotSourceCodes.push(slotContent);
- } else {
- slotSourceCodes.push(
- `${slotContent}`
- );
- }
- });
-
- return slotSourceCodes.join('\n\n');
-};
-
-/**
- * Generates the source code for the given slot children (the code inside ).
- */
-const generateSlotChildrenSourceCode = (children: unknown[]): string => {
- const slotChildrenSourceCodes: string[] = [];
-
- /**
- * Recursively generates the source code for a single slot child and all its children.
- * @returns Source code for child and all nested children or empty string if child is of a non-supported type.
- */
- const generateSingleChildSourceCode = (child: unknown): string => {
- if (isVNode(child)) {
- return generateVNodeSourceCode(child);
- }
-
- switch (typeof child) {
- case 'string':
- case 'number':
- case 'boolean':
- return child.toString();
-
- case 'object':
- if (child === null) return '';
- if (Array.isArray(child)) {
- // if child also has children, we generate them recursively
- return child
- .map(generateSingleChildSourceCode)
- .filter((code) => code !== '')
- .join('\n');
- }
- return JSON.stringify(child);
-
- case 'function': {
- const paramNames = getFunctionParamNames(child).filter(
- (param) => !['{', '}'].includes(param)
- );
-
- const parameters = paramNames.reduce>((obj, param) => {
- obj[param] = `{{ ${param} }}`;
- return obj;
- }, {});
-
- const returnValue = child(parameters);
- let slotSourceCode = generateSlotChildrenSourceCode([returnValue]);
-
- // if slot bindings are used for properties of other components, our {{ paramName }} is incorrect because
- // it would generate e.g. my-prop="{{ paramName }}", therefore, we replace it here to e.g. :my-prop="paramName"
- paramNames.forEach((param) => {
- slotSourceCode = slotSourceCode.replaceAll(
- new RegExp(` (\\S+)="{{ ${param} }}"`, 'g'),
- ` :$1="${param}"`
- );
- });
-
- return slotSourceCode;
- }
-
- case 'bigint':
- return `{{ BigInt(${child.toString()}) }}`;
-
- // the only missing case here is "symbol"
- // because rendering a symbol as slot / HTML does not make sense and is not supported by Vue
- default:
- return '';
- }
- };
-
- children.forEach((child) => {
- const sourceCode = generateSingleChildSourceCode(child);
- if (sourceCode !== '') slotChildrenSourceCodes.push(sourceCode);
- });
-
- return slotChildrenSourceCodes.join('\n');
-};
-
-/**
- * Generates source code for the given VNode and all its children (e.g. created using `h(MyComponent)` or `h("div")`).
- */
-const generateVNodeSourceCode = (vnode: VNode): string => {
- let componentName = 'component';
- if (typeof vnode.type === 'string') {
- // this is e.g. the case when rendering native HTML elements like, h("div")
- componentName = vnode.type;
- } else if (typeof vnode.type === 'object' && '__name' in vnode.type) {
- // this is the case when using custom Vue components like h(MyComponent)
- if ('name' in vnode.type && vnode.type.name) {
- // prefer custom component name set by the developer
- componentName = vnode.type.name;
- } else if ('__name' in vnode.type && vnode.type.__name) {
- // otherwise use name inferred by Vue from the file name
- componentName = vnode.type.__name;
- }
- }
-
- let childrenCode = '';
-
- if (typeof vnode.children === 'string') {
- childrenCode = vnode.children;
- } else if (Array.isArray(vnode.children)) {
- childrenCode = generateSlotChildrenSourceCode(vnode.children);
- } else if (vnode.children) {
- // children are an object, just like if regular Story args where used
- // so we can generate the source code with the regular "generateSlotSourceCode()".
- childrenCode = generateSlotSourceCode(
- vnode.children,
- // $stable is a default property in vnode.children so we need to filter it out
- // to not generate source code for it
- Object.keys(vnode.children).filter((i) => i !== '$stable')
- );
- }
-
- const props = vnode.props ? generatePropsSourceCode(vnode.props, []) : '';
-
- // prefer self closing tag if no children exist
- if (childrenCode)
- return `<${componentName}${props ? ` ${props}` : ''}>${childrenCode}${componentName}>`;
- return `<${componentName}${props ? ` ${props}` : ''} />`;
-};
-
-/**
- * Gets a list of parameters for the given function since func.arguments can not be used since
- * it throws a TypeError.
- *
- * If the arguments are destructured (e.g. "func({ foo, bar })"), the returned array will also
- * include "{" and "}".
- *
- * @see Based on https://stackoverflow.com/a/9924463
- */
-// eslint-disable-next-line @typescript-eslint/ban-types
-const getFunctionParamNames = (func: Function): string[] => {
- const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm;
- const ARGUMENT_NAMES = /([^\s,]+)/g;
-
- const fnStr = func.toString().replace(STRIP_COMMENTS, '');
- const result = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);
- if (result === null) return [];
- return result;
-};
-
-/**
- * Converts the given slot bindings/parameters to a string.
- *
- * @example
- * If no params: '#slotName'
- * If params: '#slotName="{ foo, bar }"'
- */
-const slotBindingsToString = (
- slotName: string,
- params: string[]
-): `#${string}` | `#${string}="${string}"` => {
- if (!params.length) return `#${slotName}`;
- if (params.length === 1) return `#${slotName}="${params[0]}"`;
-
- // parameters might be destructured so remove duplicated brackets here
- return `#${slotName}="{ ${params.filter((i) => !['{', '}'].includes(i)).join(', ')} }"`;
-};
diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts
index d695d7979c37..fd181a044a49 100644
--- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts
+++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts
@@ -1,304 +1,155 @@
-import { describe, expect, it } from 'vitest';
-
+import { expect, test } from 'vitest';
+import { h } from 'vue';
import {
- mapAttributesAndDirectives,
- generateAttributesSource,
- attributeSource,
- htmlEventAttributeToVueEventAttribute as htmlEventToVueEvent,
+ extractSlotNames,
+ generatePropsSourceCode,
+ generateSlotSourceCode,
} from './sourceDecorator';
-expect.addSnapshotSerializer({
- print: (val: any) => val,
- test: (val: unknown) => typeof val === 'string',
-});
+test('should generate source code for props', () => {
+ const slots = ['default', 'testSlot'];
+
+ const code = generatePropsSourceCode(
+ {
+ a: 'foo',
+ b: '"I am double quoted"',
+ c: 42,
+ d: true,
+ e: false,
+ f: [1, 2, 3],
+ g: {
+ g1: 'foo',
+ b2: 42,
+ },
+ h: undefined,
+ i: null,
+ j: '',
+ k: BigInt(9007199254740991),
+ l: Symbol(),
+ m: Symbol('foo'),
+ default: 'default slot',
+ testSlot: 'test slot',
+ },
+ slots
+ );
-describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => {
- it('camelCase boolean Arg', () => {
- expect(mapAttributesAndDirectives({ camelCaseBooleanArg: true })).toMatchInlineSnapshot(`
- [
- {
- arg: {
- content: camel-case-boolean-arg,
- loc: {
- source: camel-case-boolean-arg,
- },
- },
- exp: {
- isStatic: false,
- loc: {
- source: true,
- },
- },
- loc: {
- source: :camel-case-boolean-arg="true",
- },
- modifiers: [
- ,
- ],
- name: bind,
- type: 6,
- },
- ]
- `);
- });
- it('camelCase string Arg', () => {
- expect(mapAttributesAndDirectives({ camelCaseStringArg: 'foo' })).toMatchInlineSnapshot(`
- [
- {
- arg: {
- content: camel-case-string-arg,
- loc: {
- source: camel-case-string-arg,
- },
- },
- exp: {
- isStatic: false,
- loc: {
- source: foo,
- },
- },
- loc: {
- source: camel-case-string-arg="foo",
- },
- modifiers: [
- ,
- ],
- name: bind,
- type: 6,
- },
- ]
- `);
- });
- it('boolean arg', () => {
- expect(mapAttributesAndDirectives({ booleanarg: true })).toMatchInlineSnapshot(`
- [
- {
- arg: {
- content: booleanarg,
- loc: {
- source: booleanarg,
- },
- },
- exp: {
- isStatic: false,
- loc: {
- source: true,
- },
- },
- loc: {
- source: :booleanarg="true",
- },
- modifiers: [
- ,
- ],
- name: bind,
- type: 6,
- },
- ]
- `);
- });
- it('string arg', () => {
- expect(mapAttributesAndDirectives({ stringarg: 'bar' })).toMatchInlineSnapshot(`
- [
- {
- arg: {
- content: stringarg,
- loc: {
- source: stringarg,
- },
- },
- exp: {
- isStatic: false,
- loc: {
- source: bar,
- },
- },
- loc: {
- source: stringarg="bar",
- },
- modifiers: [
- ,
- ],
- name: bind,
- type: 6,
- },
- ]
- `);
- });
- it('number arg', () => {
- expect(mapAttributesAndDirectives({ numberarg: 2023 })).toMatchInlineSnapshot(`
- [
- {
- arg: {
- content: numberarg,
- loc: {
- source: numberarg,
- },
- },
- exp: {
- isStatic: false,
- loc: {
- source: 2023,
- },
- },
- loc: {
- source: :numberarg="2023",
- },
- modifiers: [
- ,
- ],
- name: bind,
- type: 6,
- },
- ]
- `);
- });
- it('camelCase boolean, string, and number Args', () => {
- expect(
- mapAttributesAndDirectives({
- camelCaseBooleanArg: true,
- camelCaseStringArg: 'foo',
- cameCaseNumberArg: 2023,
- })
- ).toMatchInlineSnapshot(`
- [
- {
- arg: {
- content: camel-case-boolean-arg,
- loc: {
- source: camel-case-boolean-arg,
- },
- },
- exp: {
- isStatic: false,
- loc: {
- source: true,
- },
- },
- loc: {
- source: :camel-case-boolean-arg="true",
- },
- modifiers: [
- ,
- ],
- name: bind,
- type: 6,
- },
- {
- arg: {
- content: camel-case-string-arg,
- loc: {
- source: camel-case-string-arg,
- },
- },
- exp: {
- isStatic: false,
- loc: {
- source: foo,
- },
- },
- loc: {
- source: camel-case-string-arg="foo",
- },
- modifiers: [
- ,
- ],
- name: bind,
- type: 6,
- },
- {
- arg: {
- content: came-case-number-arg,
- loc: {
- source: came-case-number-arg,
- },
- },
- exp: {
- isStatic: false,
- loc: {
- source: 2023,
- },
- },
- loc: {
- source: :came-case-number-arg="2023",
- },
- modifiers: [
- ,
- ],
- name: bind,
- type: 6,
- },
- ]
- `);
- });
+ 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')"`
+ );
});
-describe('Vue3: sourceDecorator->generateAttributesSource()', () => {
- it('camelCase boolean Arg', () => {
- expect(
- generateAttributesSource(
- mapAttributesAndDirectives({ camelCaseBooleanArg: true }),
- { camelCaseBooleanArg: true },
- [{ camelCaseBooleanArg: { type: 'boolean' } }] as any
- )
- ).toMatchInlineSnapshot(`:camel-case-boolean-arg="true"`);
- });
- it('camelCase string Arg', () => {
- expect(
- generateAttributesSource(
- mapAttributesAndDirectives({ camelCaseStringArg: 'foo' }),
- { camelCaseStringArg: 'foo' },
- [{ camelCaseStringArg: { type: 'string' } }] as any
- )
- ).toMatchInlineSnapshot(`camel-case-string-arg="foo"`);
- });
+test('should generate source code for slots', () => {
+ // slot code generator should support primitive values (string, number etc.)
+ // but also VNodes (e.g. created using h()) so custom Vue components can also be used
+ // inside slots with proper generated code
+
+ const slots = {
+ default: 'default content',
+ a: 'a content',
+ b: 42,
+ c: true,
+ // single VNode without props
+ d: h('div', 'd content'),
+ // VNode with props and single child
+ e: h('div', { style: 'color:red' }, 'e content'),
+ // VNode with props and single child returned as getter
+ f: h('div', { style: 'color:red' }, () => 'f content'),
+ // VNode with multiple children
+ g: h('div', { style: 'color:red' }, [
+ 'child 1',
+ h('span', { style: 'color:green' }, 'child 2'),
+ ]),
+ // VNode multiple children but returned as getter
+ h: h('div', { style: 'color:red' }, () => [
+ 'child 1',
+ h('span', { style: 'color:green' }, 'child 2'),
+ ]),
+ // VNode with multiple and nested children
+ i: h('div', { style: 'color:red' }, [
+ 'child 1',
+ h('span', { style: 'color:green' }, ['nested child 1', h('p', 'nested child 2')]),
+ ]),
+ j: ['child 1', 'child 2'],
+ k: null,
+ l: { foo: 'bar' },
+ m: BigInt(9007199254740991),
+ };
+
+ const expectedCode = `default content
+
+a content
+
+42
+
+true
+
+d content
+
+e content
+
+f content
+
+child 1
+child 2
- it('camelCase boolean, string, and number Args', () => {
- expect(
- generateAttributesSource(
- mapAttributesAndDirectives({
- camelCaseBooleanArg: true,
- camelCaseStringArg: 'foo',
- cameCaseNumberArg: 2023,
- }),
- {
- camelCaseBooleanArg: true,
- camelCaseStringArg: 'foo',
- cameCaseNumberArg: 2023,
- },
- [] as any
- )
- ).toMatchInlineSnapshot(
- `:camel-case-boolean-arg="true" camel-case-string-arg="foo" :came-case-number-arg="2023"`
- );
- });
+child 1
+child 2
+
+child 1
+
nested child 1
+nested child 2
+
+child 1
+child 2
+
+{"foo":"bar"}
+
+{{ BigInt(9007199254740991) }}`;
+
+ let actualCode = generateSlotSourceCode(slots, Object.keys(slots));
+ expect(actualCode).toBe(expectedCode);
+
+ // should generate the same code if getters/functions are used to return the slot content
+ const slotsWithGetters = Object.entries(slots).reduce<
+ Record (typeof slots)[keyof typeof slots]>
+ >((obj, [slotName, value]) => {
+ obj[slotName] = () => value;
+ return obj;
+ }, {});
+
+ actualCode = generateSlotSourceCode(slotsWithGetters, Object.keys(slotsWithGetters));
+ expect(actualCode).toBe(expectedCode);
});
-describe('Vue3: sourceDecorator->attributeSoure()', () => {
- it('camelCase boolean Arg', () => {
- expect(attributeSource('stringArg', 'foo')).toMatchInlineSnapshot(`stringArg="foo"`);
- });
+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 = `Slot with bindings {{ foo }} and {{ bar }}
+
+Test link: {{ foo }}`;
+
+ const actualCode = generateSlotSourceCode(slots, Object.keys(slots));
+ expect(actualCode).toBe(expectedCode);
+});
- it('html event attribute should convert to vue event directive', () => {
- expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`);
- expect(attributeSource('onclick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`);
- });
- it('normal html attribute should not convert to vue event directive', () => {
- expect(attributeSource('on-click', () => {})).toMatchInlineSnapshot(`on-click='()=>({})'`);
- });
- it('The value undefined or empty string must not be returned.', () => {
- expect(attributeSource('icon', undefined)).toMatchInlineSnapshot(`icon=""`);
- expect(attributeSource('icon', '')).toMatchInlineSnapshot(`icon=""`);
- });
- it('htmlEventAttributeToVueEventAttribute onEv => v-on:', () => {
- const htmlEventAttributeToVueEventAttribute = (attribute: string) => {
- return htmlEventToVueEvent(attribute);
- };
- expect(/^on[A-Za-z]/.test('onClick')).toBeTruthy();
- expect(htmlEventAttributeToVueEventAttribute('onclick')).toMatchInlineSnapshot(`v-on:click`);
- expect(htmlEventAttributeToVueEventAttribute('onClick')).toMatchInlineSnapshot(`v-on:click`);
- expect(htmlEventAttributeToVueEventAttribute('onChange')).toMatchInlineSnapshot(`v-on:change`);
- expect(htmlEventAttributeToVueEventAttribute('onFocus')).toMatchInlineSnapshot(`v-on:focus`);
- expect(htmlEventAttributeToVueEventAttribute('on-focus')).toMatchInlineSnapshot(`on-focus`);
- });
+test.each([
+ { __docgenInfo: 'invalid-value', slotNames: [] },
+ { __docgenInfo: {}, slotNames: [] },
+ { __docgenInfo: { slots: 'invalid-value' }, slotNames: [] },
+ { __docgenInfo: { slots: ['invalid-value'] }, slotNames: [] },
+ {
+ __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);
});
diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts
index 525cfbbfd281..6c32020f74e9 100644
--- a/code/renderers/vue3/src/docs/sourceDecorator.ts
+++ b/code/renderers/vue3/src/docs/sourceDecorator.ts
@@ -1,46 +1,80 @@
/* eslint-disable no-underscore-dangle */
+import { SNIPPET_RENDERED, SourceType } from '@storybook/docs-tools';
import { addons } from '@storybook/preview-api';
-import type { ArgTypes, Args, StoryContext } from '@storybook/types';
-
-import { SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools';
-
-import type {
- ElementNode,
- AttributeNode,
- DirectiveNode,
- TextNode,
- InterpolationNode,
- TemplateChildNode,
-} from '@vue/compiler-core';
-import { baseParse } from '@vue/compiler-core';
-import type { ConcreteComponent, FunctionalComponent, VNode } from 'vue';
-import { h, isVNode, watch } from 'vue';
-import kebabCase from 'lodash/kebabCase';
-import {
- attributeSource,
- htmlEventAttributeToVueEventAttribute,
- omitEvent,
- evalExp,
- replaceValueWithRef,
- generateExpression,
-} from './utils';
-import type { VueRenderer } from '../types';
+import type { VNode } from 'vue';
+import { isVNode, watch } from 'vue';
+import type { Args, Decorator, StoryContext } from '../public-types';
/**
- * Check if the sourcecode should be generated.
- *
- * @param context StoryContext
+ * Decorator to generate Vue source code for stories.
*/
-const skipSourceRender = (context: StoryContext) => {
- const sourceParams = context?.parameters.docs?.source;
- const isArgsStory = context?.parameters.__isArgsStory;
- const isDocsViewMode = context?.viewMode === 'docs';
+export const sourceDecorator: Decorator = (storyFn, ctx) => {
+ const story = storyFn();
+ if (shouldSkipSourceCodeGeneration(ctx)) return story;
+
+ const channel = addons.getChannel();
+
+ watch(
+ () => ctx.args,
+ () => {
+ const sourceCode = generateSourceCode(ctx);
+
+ channel.emit(SNIPPET_RENDERED, {
+ id: ctx.id,
+ args: ctx.args,
+ source: sourceCode,
+ format: 'vue',
+ });
+ },
+ { immediate: true, deep: true }
+ );
+
+ return story;
+};
+
+/**
+ * Generate Vue source code for the given Story.
+ * @returns Source code or empty string if source code could not be generated.
+ */
+export const generateSourceCode = (
+ ctx: Pick
+): string => {
+ const componentName = ctx.component?.__name || ctx.title.split('/').at(-1)!;
- // always render if the user forces it
+ const slotNames = extractSlotNames(ctx.component);
+ const slotSourceCode = generateSlotSourceCode(ctx.args, slotNames);
+ const propsSourceCode = generatePropsSourceCode(ctx.args, slotNames);
+
+ if (slotSourceCode) {
+ return `
+ <${componentName} ${propsSourceCode}> ${slotSourceCode} ${componentName}>
+ `;
+ }
+
+ // prefer self closing tag if no slot content exists
+ return `
+ <${componentName} ${propsSourceCode} />
+ `;
+};
+
+/**
+ * Checks if the source code generation should be skipped for the given Story context.
+ * Will be true if one of the following is true:
+ * - view mode is not "docs"
+ * - story is no arg story
+ * - story has set custom source code via parameters.docs.source.code
+ * - story has set source type to "code" via parameters.docs.source.type
+ */
+export const shouldSkipSourceCodeGeneration = (context: StoryContext): boolean => {
+ const sourceParams = context?.parameters.docs?.source;
if (sourceParams?.type === SourceType.DYNAMIC) {
+ // always render if the user forces it
return false;
}
+ const isArgsStory = context?.parameters.__isArgsStory;
+ const isDocsViewMode = context?.viewMode === 'docs';
+
// never render if the user is forcing the block to render code, or
// if the user provides code, or if it's not an args story.
return (
@@ -49,271 +83,277 @@ const skipSourceRender = (context: StoryContext) => {
};
/**
- *
- * @param _args
- * @param argTypes
- * @param byRef
+ * Gets all slot names from the `__docgenInfo` of the given component if available.
+ * Requires Storybook docs addon to be enabled.
+ * Default slot will always be sorted first, remaining slots are sorted alphabetically.
*/
-export function generateAttributesSource(
- tempArgs: (AttributeNode | DirectiveNode)[],
- args: Args,
- argTypes: ArgTypes,
- byRef?: boolean
-): string {
- return Object.keys(tempArgs)
- .map((key: any) => {
- const source = tempArgs[key].loc.source.replace(/\$props/g, 'args');
- const argKey = (tempArgs[key] as DirectiveNode).arg?.loc.source;
- return byRef && argKey
- ? replaceValueWithRef(source, args, argKey)
- : evalExp(source, omitEvent(args));
- })
- .join(' ');
-}
-/**
- * map attributes and directives
- * @param props
- */
-function mapAttributesAndDirectives(props: Args) {
- const tranformKey = (key: string) => (key.startsWith('on') ? key : kebabCase(key));
- return Object.keys(props).map(
- (key) =>
- ({
- name: 'bind',
- type: ['v-', '@', 'v-on'].includes(key) ? 7 : 6, // 6 is attribute, 7 is directive
- arg: { content: tranformKey(key), loc: { source: tranformKey(key) } }, // attribute name or directive name (v-bind, v-on, v-model)
- loc: { source: attributeSource(tranformKey(key), props[key]) }, // attribute value or directive value
- exp: { isStatic: false, loc: { source: props[key] } }, // directive expression
- modifiers: [''],
- }) as unknown as AttributeNode
- );
-}
-/**
- * map slots
- * @param slotsArgs
- */
-function mapSlots(
- slotsArgs: Args,
- generateComponentSource: any,
- slots: { name: string; scoped?: boolean; bindings?: { name: string }[] }[]
-): TextNode[] {
- return Object.keys(slotsArgs).map((key) => {
- const slot = slotsArgs[key];
- let slotContent = '';
-
- const scropedArgs = slots
- .find((s) => s.name === key && s.scoped)
- ?.bindings?.map((b) => b.name)
- .join(',');
-
- if (typeof slot === 'string') {
- slotContent = slot;
- } else if (typeof slot === 'function') {
- slotContent = generateExpression(slot);
- } else if (isVNode(slot)) {
- slotContent = generateComponentSource(slot);
- } else if (typeof slot === 'object' && !isVNode(slot)) {
- slotContent = JSON.stringify(slot);
- }
+export const extractSlotNames = (
+ component?: StoryContext['component'] & { __docgenInfo?: unknown }
+): string[] => {
+ if (!component || !('__docgenInfo' in component)) return [];
+
+ // type check __docgenInfo to prevent errors
+ if (!component.__docgenInfo || typeof component.__docgenInfo !== 'object') return [];
+ if (
+ !('slots' in component.__docgenInfo) ||
+ !component.__docgenInfo.slots ||
+ !Array.isArray(component.__docgenInfo.slots)
+ ) {
+ return [];
+ }
+
+ return component.__docgenInfo.slots
+ .map((slot) => slot.name)
+ .filter((i): i is string => typeof i === 'string')
+ .sort((a, b) => {
+ if (a === 'default') return -1;
+ if (b === 'default') return 1;
+ return a.localeCompare(b);
+ });
+};
- const bindingsString = scropedArgs ? `="{${scropedArgs}}"` : '';
- slotContent = slot ? `${slotContent}` : ``;
-
- return {
- type: 2,
- content: slotContent,
- loc: {
- source: slotContent,
- start: { offset: 0, line: 1, column: 0 },
- end: { offset: 0, line: 1, column: 0 },
- },
- };
- });
- // TODO: handle other cases (array, object, html,etc)
-}
/**
+ * Generates the source code for the given Vue component properties.
*
- * @param args generate script setup from args
- * @param argTypes
+ * @param args Story args / property values.
+ * @param slotNames All slot names of the component. Needed to not generate code for args that are slots.
+ * Can be extracted using `extractSlotNames()`.
*/
-function generateScriptSetup(args: Args, argTypes: ArgTypes, components: any[]): string {
- const scriptLines = Object.keys(args).map(
- (key: any) =>
- `const ${key} = ${
- typeof args[key] === 'function' ? `()=>{}` : `ref(${JSON.stringify(args[key])});`
- }`
- );
- scriptLines.unshift(`import { ref } from "vue";`);
+export const generatePropsSourceCode = (
+ args: Record,
+ slotNames: string[]
+): string => {
+ const props: string[] = [];
+
+ Object.entries(args).forEach(([propName, value]) => {
+ // ignore slots
+ if (slotNames.includes(propName)) return;
+
+ switch (typeof value) {
+ case 'string':
+ if (value === '') return; // do not render empty strings
+
+ if (value.includes('"')) {
+ props.push(`${propName}='${value}'`);
+ } else {
+ props.push(`${propName}="${value}"`);
+ }
+
+ break;
+ case 'number':
+ props.push(`:${propName}="${value}"`);
+ break;
+ case 'bigint':
+ props.push(`:${propName}="BigInt(${value.toString()})"`);
+ break;
+ case 'boolean':
+ props.push(value === true ? propName : `:${propName}="false"`);
+ break;
+ case 'object':
+ if (value === null) return; // do not render null values
+ props.push(`:${propName}="${JSON.stringify(value).replaceAll('"', "'")}"`);
+ break;
+ case 'symbol': {
+ const symbol = `Symbol(${value.description ? `'${value.description}'` : ''})`;
+ props.push(`:${propName}="${symbol}"`);
+ break;
+ }
+ case 'function':
+ // TODO: check if functions should be rendered in source code
+ break;
+ }
+ });
+
+ return props.join(' ');
+};
- return ``;
-}
/**
- * get template components one or more
- * @param renderFn
+ * Generates the source code for the given Vue component slots.
+ *
+ * @param args Story args.
+ * @param slotNames All slot names of the component. Needed to only generate slots and ignore props etc.
+ * Can be extracted using `extractSlotNames()`.
*/
-function getTemplateComponents(
- renderFn: any,
- context?: StoryContext
-): (TemplateChildNode | VNode)[] {
- try {
- const originalStoryFn = renderFn;
+export const generateSlotSourceCode = (args: Args, slotNames: string[]): string => {
+ /** List of slot source codes (e.g. Content) */
+ const slotSourceCodes: string[] = [];
- const storyFn = originalStoryFn ? originalStoryFn(context?.args, context) : context?.component;
- const story = typeof storyFn === 'function' ? storyFn() : storyFn;
+ slotNames.forEach((slotName) => {
+ const arg = args[slotName];
+ if (!arg) return;
- const { template } = story;
+ const slotContent = generateSlotChildrenSourceCode([arg]);
+ if (!slotContent) return; // do not generate source code for empty slots
- if (!template) return [h(story, context?.args)];
- return getComponents(template);
- } catch (e) {
- return [];
- }
-}
+ const slotBindings = typeof arg === 'function' ? getFunctionParamNames(arg) : [];
-function getComponents(template: string): (TemplateChildNode | VNode)[] {
- const ast = baseParse(template, {
- isNativeTag: () => true,
- decodeEntities: (rawtext, asAttr) => rawtext,
+ if (slotName === 'default' && !slotBindings.length) {
+ // do not add unnecessary "" tag since the default slot content without bindings
+ // can be put directly into the slot without need of ""
+ slotSourceCodes.push(slotContent);
+ } else {
+ slotSourceCodes.push(
+ `${slotContent}`
+ );
+ }
});
- const components = ast?.children;
- if (!components) return [];
- return components;
-}
+
+ return slotSourceCodes.join('\n\n');
+};
/**
- * Generate a vue3 template.
- *
- * @param component Component
- * @param args Args
- * @param argTypes ArgTypes
- * @param slotProp Prop used to simulate a slot
+ * Generates the source code for the given slot children (the code inside ).
*/
+const generateSlotChildrenSourceCode = (children: unknown[]): string => {
+ const slotChildrenSourceCodes: string[] = [];
-export function generateTemplateSource(
- componentOrNodes: (ConcreteComponent | TemplateChildNode)[] | TemplateChildNode | VNode,
- { args, argTypes }: { args: Args; argTypes: ArgTypes },
- byRef = false
-) {
- const isElementNode = (node: any) => node && node.type === 1;
- const isInterpolationNode = (node: any) => node && node.type === 5;
- const isTextNode = (node: any) => node && node.type === 2;
-
- const generateComponentSource = (
- componentOrNode: ConcreteComponent | TemplateChildNode | VNode
- ) => {
- if (isElementNode(componentOrNode)) {
- const { tag: name, props: attributes, children } = componentOrNode as ElementNode;
- const childSources: string =
- typeof children === 'string'
- ? children
- : children.map((child: TemplateChildNode) => generateComponentSource(child)).join('');
- const props = generateAttributesSource(attributes, args, argTypes, byRef);
-
- return childSources === ''
- ? `<${name} ${props} />`
- : `<${name} ${props}>${childSources}${name}>`;
+ /**
+ * Recursively generates the source code for a single slot child and all its children.
+ * @returns Source code for child and all nested children or empty string if child is of a non-supported type.
+ */
+ const generateSingleChildSourceCode = (child: unknown): string => {
+ if (isVNode(child)) {
+ return generateVNodeSourceCode(child);
}
- if (isTextNode(componentOrNode)) {
- const { content } = componentOrNode as TextNode;
- return content;
- }
- if (isInterpolationNode(componentOrNode)) {
- const { content } = componentOrNode as InterpolationNode;
- const expValue = evalExp(content.loc.source, args);
- if (expValue === content.loc.source) return `{{${expValue}}}`;
- return eval(expValue);
- }
- if (isVNode(componentOrNode)) {
- const vnode = componentOrNode as VNode;
- const { props, type, children } = vnode;
- const slotsProps = typeof children === 'string' ? undefined : (children as Args);
- const componentSlots = (type as any)?.__docgenInfo?.slots;
-
- const attrsProps = slotsProps
- ? Object.fromEntries(
- Object.entries(props ?? {})
- .filter(([key, value]) => !slotsProps[key] && !['class', 'style'].includes(key))
- .map(([key, value]) => [key, value])
- )
- : props;
- const attributes = mapAttributesAndDirectives(attrsProps ?? {});
- const slotArgs = Object.fromEntries(
- Object.entries(props ?? {}).filter(([key, value]) => slotsProps?.[key])
- );
+ switch (typeof child) {
+ case 'string':
+ case 'number':
+ case 'boolean':
+ return child.toString();
- const childSources: string = children
- ? typeof children === 'string'
- ? children
- : mapSlots(slotArgs as Args, generateComponentSource, componentSlots ?? [])
- .map((child) => child.content)
- .join('')
- : '';
- const name =
- typeof type === 'string'
- ? type
- : (type as FunctionalComponent).name ||
- (type as ConcreteComponent).__name ||
- (type as any).__docgenInfo?.displayName;
- const propsSource = generateAttributesSource(attributes, args, argTypes, byRef);
- return childSources.trim() === ''
- ? `<${name} ${propsSource}/>`
- : `<${name} ${propsSource}>${childSources}${name}>`;
- }
+ case 'object':
+ if (child === null) return '';
+ if (Array.isArray(child)) {
+ // if child also has children, we generate them recursively
+ return child
+ .map(generateSingleChildSourceCode)
+ .filter((code) => code !== '')
+ .join('\n');
+ }
+ return JSON.stringify(child);
+
+ case 'function': {
+ const paramNames = getFunctionParamNames(child).filter(
+ (param) => !['{', '}'].includes(param)
+ );
+
+ const parameters = paramNames.reduce>((obj, param) => {
+ obj[param] = `{{ ${param} }}`;
+ return obj;
+ }, {});
- return null;
+ const returnValue = child(parameters);
+ let slotSourceCode = generateSlotChildrenSourceCode([returnValue]);
+
+ // if slot bindings are used for properties of other components, our {{ paramName }} is incorrect because
+ // it would generate e.g. my-prop="{{ paramName }}", therefore, we replace it here to e.g. :my-prop="paramName"
+ paramNames.forEach((param) => {
+ slotSourceCode = slotSourceCode.replaceAll(
+ new RegExp(` (\\S+)="{{ ${param} }}"`, 'g'),
+ ` :$1="${param}"`
+ );
+ });
+
+ return slotSourceCode;
+ }
+
+ case 'bigint':
+ return `{{ BigInt(${child.toString()}) }}`;
+
+ // the only missing case here is "symbol"
+ // because rendering a symbol as slot / HTML does not make sense and is not supported by Vue
+ default:
+ return '';
+ }
};
- const componentsOrNodes = Array.isArray(componentOrNodes) ? componentOrNodes : [componentOrNodes];
- const source = componentsOrNodes
- .map((componentOrNode) => generateComponentSource(componentOrNode))
- .join(' ');
- return source || null;
-}
+ children.forEach((child) => {
+ const sourceCode = generateSingleChildSourceCode(child);
+ if (sourceCode !== '') slotChildrenSourceCodes.push(sourceCode);
+ });
+
+ return slotChildrenSourceCodes.join('\n');
+};
/**
- * source decorator.
- * @param storyFn Fn
- * @param context StoryContext
+ * Generates source code for the given VNode and all its children (e.g. created using `h(MyComponent)` or `h("div")`).
*/
-export const sourceDecorator = (storyFn: any, context: StoryContext) => {
- const skip = skipSourceRender(context);
- const story = storyFn();
+const generateVNodeSourceCode = (vnode: VNode): string => {
+ let componentName = 'component';
+ if (typeof vnode.type === 'string') {
+ // this is e.g. the case when rendering native HTML elements like, h("div")
+ componentName = vnode.type;
+ } else if (typeof vnode.type === 'object' && '__name' in vnode.type) {
+ // this is the case when using custom Vue components like h(MyComponent)
+ if ('name' in vnode.type && vnode.type.name) {
+ // prefer custom component name set by the developer
+ componentName = vnode.type.name;
+ } else if ('__name' in vnode.type && vnode.type.__name) {
+ // otherwise use name inferred by Vue from the file name
+ componentName = vnode.type.__name;
+ }
+ }
- watch(
- () => context.args,
- () => {
- if (!skip) {
- generateSource(context);
- }
- },
- { immediate: true, deep: true }
- );
- return story;
+ let childrenCode = '';
+
+ if (typeof vnode.children === 'string') {
+ childrenCode = vnode.children;
+ } else if (Array.isArray(vnode.children)) {
+ childrenCode = generateSlotChildrenSourceCode(vnode.children);
+ } else if (vnode.children) {
+ // children are an object, just like if regular Story args where used
+ // so we can generate the source code with the regular "generateSlotSourceCode()".
+ childrenCode = generateSlotSourceCode(
+ vnode.children,
+ // $stable is a default property in vnode.children so we need to filter it out
+ // to not generate source code for it
+ Object.keys(vnode.children).filter((i) => i !== '$stable')
+ );
+ }
+
+ const props = vnode.props ? generatePropsSourceCode(vnode.props, []) : '';
+
+ // prefer self closing tag if no children exist
+ if (childrenCode)
+ return `<${componentName}${props ? ` ${props}` : ''}>${childrenCode}${componentName}>`;
+ return `<${componentName}${props ? ` ${props}` : ''} />`;
};
-export function generateSource(context: StoryContext) {
- const channel = addons.getChannel();
- const { args = {}, argTypes = {}, id } = context || {};
- const storyComponents = getTemplateComponents(context?.originalStoryFn, context);
+/**
+ * Gets a list of parameters for the given function since func.arguments can not be used since
+ * it throws a TypeError.
+ *
+ * If the arguments are destructured (e.g. "func({ foo, bar })"), the returned array will also
+ * include "{" and "}".
+ *
+ * @see Based on https://stackoverflow.com/a/9924463
+ */
+// eslint-disable-next-line @typescript-eslint/ban-types
+const getFunctionParamNames = (func: Function): string[] => {
+ const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm;
+ const ARGUMENT_NAMES = /([^\s,]+)/g;
- const withScript = context?.parameters?.docs?.source?.withScriptSetup || false;
- const generatedScript = withScript ? generateScriptSetup(args, argTypes, storyComponents) : '';
- const generatedTemplate = generateTemplateSource(storyComponents, context, withScript);
+ const fnStr = func.toString().replace(STRIP_COMMENTS, '');
+ const result = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);
+ if (result === null) return [];
+ return result;
+};
- if (generatedTemplate) {
- const source = `${generatedScript}\n \n ${generatedTemplate} \n`;
- channel.emit(SNIPPET_RENDERED, { id, args, source, format: 'vue' });
- return source;
- }
- return null;
-}
-// export local function for testing purpose
-export {
- generateScriptSetup,
- getTemplateComponents as getComponentsFromRenderFn,
- getComponents as getComponentsFromTemplate,
- mapAttributesAndDirectives,
- attributeSource,
- htmlEventAttributeToVueEventAttribute,
+/**
+ * Converts the given slot bindings/parameters to a string.
+ *
+ * @example
+ * If no params: '#slotName'
+ * If params: '#slotName="{ foo, bar }"'
+ */
+const slotBindingsToString = (
+ slotName: string,
+ params: string[]
+): `#${string}` | `#${string}="${string}"` => {
+ if (!params.length) return `#${slotName}`;
+ if (params.length === 1) return `#${slotName}="${params[0]}"`;
+
+ // parameters might be destructured so remove duplicated brackets here
+ return `#${slotName}="{ ${params.filter((i) => !['{', '}'].includes(i)).join(', ')} }"`;
};
diff --git a/code/renderers/vue3/src/entry-preview-docs.ts b/code/renderers/vue3/src/entry-preview-docs.ts
index a7651d44f8c9..420642148291 100644
--- a/code/renderers/vue3/src/entry-preview-docs.ts
+++ b/code/renderers/vue3/src/entry-preview-docs.ts
@@ -1,7 +1,6 @@
import { enhanceArgTypes, extractComponentDescription } from '@storybook/docs-tools';
import type { ArgTypesEnhancer } from '@storybook/types';
import { extractArgTypes } from './docs/extractArgTypes';
-import { sourceCodeDecorator } from './docs/source-code-generator';
import { sourceDecorator } from './docs/sourceDecorator';
import type { VueRenderer } from './types';
@@ -13,13 +12,6 @@ export const parameters = {
},
};
-// TODO: check with Storybook maintainers how to release the new decorator.
-// Maybe as opt-in parameter for now which might become the default in future Storybook
-// versions once its well tested by projects.
-// Or add another type to the "SourceType" enum for this
-const codeSnippetType = 'new' as 'legacy' | 'new';
-const codeDecoratorToUse = codeSnippetType === 'legacy' ? sourceDecorator : sourceCodeDecorator;
-
-export const decorators = [codeDecoratorToUse];
+export const decorators = [sourceDecorator];
export const argTypesEnhancers: ArgTypesEnhancer[] = [enhanceArgTypes];