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 - - - - - - - - - - - - - - - - - - - - - - - -`; - - 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 = ` - -`; - - 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 ``; - } - - // prefer self closing tag if no slot content exists - return ``; -}; - -/** - * 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. ) */ - 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 "