From 3730f4b1f7e151bc28cde4243cef4c22a201b42c Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Tue, 10 Jan 2023 20:43:41 +0400 Subject: [PATCH] vue3: refactory and code improvement --- .../vue3/src/docs/sourceDecorator.test.ts | 41 ++-- .../vue3/src/docs/sourceDecorator.ts | 206 +++++++++--------- code/renderers/vue3/src/render.ts | 4 +- 3 files changed, 122 insertions(+), 129 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index 1d7d9d157c1..5d7f7949a63 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -7,10 +7,10 @@ expect.addSnapshotSerializer({ test: (val: unknown) => typeof val === 'string', }); -function generateForArgs(args: Args, slotProps: string[] | null = null) { +function generateForArgs(args: Args, slotProps: string[] | undefined = undefined) { return generateSource({ name: 'Component' }, args, {}, slotProps, true); } -function generateMultiComponentForArgs(args: Args, slotProps: string[] | null = null) { +function generateMultiComponentForArgs(args: Args, slotProps: string[] | undefined = undefined) { return generateSource([{ name: 'Component' }, { name: 'Component' }], args, {}, slotProps, true); } @@ -26,9 +26,7 @@ describe('generateSource Vue3', () => { ); }); test('null property', () => { - expect(generateForArgs({ nullProp: null })).toMatchInlineSnapshot( - `` - ); + expect(generateForArgs({ nullProp: null })).toMatchInlineSnapshot(``); }); test('string property', () => { expect(generateForArgs({ stringProp: 'mystr' })).toMatchInlineSnapshot( @@ -41,16 +39,14 @@ describe('generateSource Vue3', () => { ); }); test('object property', () => { - expect(generateForArgs({ objProp: { x: true } })).toMatchInlineSnapshot( - `` - ); + expect(generateForArgs({ objProp: { x: true } })).toMatchInlineSnapshot(``); }); test('multiple properties', () => { expect(generateForArgs({ a: 1, b: 2 })).toMatchInlineSnapshot(``); }); test('1 slot property', () => { expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, ['content'])).toMatchInlineSnapshot(` - + {{ content }} `); @@ -58,27 +54,34 @@ describe('generateSource Vue3', () => { test('multiple slot property with second slot value not set', () => { expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, ['content', 'footer'])) .toMatchInlineSnapshot(` - - + + `); }); test('multiple slot property with second slot value is set', () => { expect(generateForArgs({ content: 'xyz', footer: 'foo', myProp: 'abc' }, ['content', 'footer'])) .toMatchInlineSnapshot(` - - - + + + `); }); // test mutil components - test('mutil component with boolean true', () => { - expect(generateMultiComponentForArgs({ booleanProp: true })).toMatchInlineSnapshot( - `` - ); + test('multi component with boolean true', () => { + expect(generateMultiComponentForArgs({ booleanProp: true })).toMatchInlineSnapshot(` + + + `); }); test('component is not set', () => { - expect(generateSource(null, {}, {}, null)).toBeNull(); + expect(generateSource(null, {}, {})).toBeNull(); }); }); diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 3171f9955ef..f28b6ae7e77 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -5,7 +5,7 @@ import type { ArgTypes, Args, StoryContext, Renderer } from '@storybook/types'; import { SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; import { format } from 'prettier'; -import parserTypescript from 'prettier/parser-typescript.js'; +import parserTypescript from 'prettier/parser-typescript'; import parserHTML from 'prettier/parser-html.js'; // eslint-disable-next-line import/no-extraneous-dependencies import { isArray } from '@vue/shared'; @@ -33,8 +33,11 @@ const skipSourceRender = (context: StoryContext) => { * * @param component Component */ -function getComponentName(component: any): string | null { - return component?.name || component?.__name || component?.__docgenInfo?.__name || null; +function getComponentNameAndChildren(component: any): { name: string | null; children: any } { + return { + name: component?.name || component?.__name || component?.__docgenInfo?.__name || null, + children: component?.children || null, + }; } /** * Transform args to props string @@ -45,11 +48,11 @@ function getComponentName(component: any): string | null { function argsToSource( args: Args, argTypes: ArgTypes, - slotProps?: string[] | null, + slotProps?: string[], byRef?: boolean ): string { const argsKeys = Object.keys(args).filter( - (key: any) => !(slotProps && slotProps.indexOf(key) > -1) + (key: any) => !isArray(args[key]) && typeof args[key] !== 'object' && !slotProps?.includes(key) ); const source = argsKeys .map((key) => @@ -63,9 +66,9 @@ function argsToSource( function propToDynamicSource( key: string, - value: string | boolean | object, + value: any, argTypes: ArgTypes, - slotProps?: string[] | null + slotProps?: string[] ): string { // slot Args or default value // default value ? @@ -91,82 +94,51 @@ function propToDynamicSource( return `:${key}='${JSON.stringify(value)}'`; } - -function generateSetupScript(args: any, argTypes: ArgTypes): string { - const argsKeys = args ? Object.keys(args) : []; - let scriptBody = ''; - // eslint-disable-next-line no-restricted-syntax - for (const key of argsKeys) { - if (!(argTypes[key] && argTypes[key].defaultValue === args[key])) - if (typeof args[key] !== 'function') - scriptBody += `\n const ${key} = ref(${propValueToSource(args[key])})`; - else scriptBody += `\n const ${key} = ()=>{}`; - } - return ``; -} - -function propValueToSource(val: string | boolean | object | undefined) { - const type = typeof val; - switch (type) { - case 'boolean': - return val; - case 'object': - return `${JSON.stringify(val as object)}`; - case 'undefined': - return `${val}`; - default: - return `'${val}'`; - } -} - -function getTemplates(renderFunc: any): [] { - const ast = parserHTML.parsers.vue.parse( - renderFunc.toString(), - { vue: parserHTML.parsers.vue }, - { - locStart(node: any): number { - throw new Error('Function not implemented.'); - }, - locEnd(node: any): number { - throw new Error('Function not implemented.'); - }, - originalText: '', - semi: false, - singleQuote: false, - jsxSingleQuote: false, - trailingComma: 'none', - bracketSpacing: false, - bracketSameLine: false, - jsxBracketSameLine: false, - rangeStart: 0, - rangeEnd: 0, - parser: 'vue', - filepath: '', - requirePragma: false, - insertPragma: false, - proseWrap: 'always', - arrowParens: 'always', - plugins: [], - pluginSearchDirs: false, - htmlWhitespaceSensitivity: 'css', - endOfLine: 'auto', - quoteProps: 'preserve', - vueIndentScriptAndStyle: false, - embeddedLanguageFormatting: 'auto', - singleAttributePerLine: false, - printWidth: 0, - tabWidth: 0, - useTabs: false, - } +/** + * + * @param args generate script setup from args + * @param argTypes + */ +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"`); - let components = ast.children?.filter((element: any) => element.name); - components = components.map((element: any) => { - const { attrs: att = [] } = element; - const att3 = att?.filter((el: any) => el.name !== 'v-bind'); // as Array).push(props); - return { name: element.name, attrs: att3 }; - }); - return components; + return ``; +} +/** + * get component templates one or more + * @param renderFn + */ +function getTemplates(renderFn: any): [] { + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const ast = parserHTML.parsers.vue.parse(renderFn.toString()); + let components = ast.children?.filter( + ({ name: _name = '', type: _type = '' }) => + _name && !['template', 'script', 'style', 'slot'].includes(_name) && _type === 'element' + ); + if (!isArray(components)) { + return []; + } + components = components.map( + ({ attrs: attributes = [], name: Name = '', children: Children = [] }) => { + return { + name: Name, + attrs: attributes?.filter((el: any) => el.name !== 'v-bind'), + children: Children, + }; + } + ); + return components; + } catch (e) { + console.error(e); + } + return []; } /** @@ -181,61 +153,79 @@ export function generateSource( compOrComps: any, args: Args, argTypes: ArgTypes, - slotProps?: string[] | null, + slotProps?: string[], byRef?: boolean | undefined ): string | null { if (!compOrComps) return null; const generateComponentSource = (component: any): string | null => { - const name = getComponentName(component); + const { name, children } = getComponentNameAndChildren(component); if (!name) { return ''; } const props = argsToSource(args, argTypes, slotProps, byRef); - const slotValues = slotProps?.map((slotProp) => args[slotProp]); + const slotArgs = Object.fromEntries( + Object.entries(args).filter(([key, value]) => slotProps && slotProps.indexOf(key) > -1) + ); - if (slotValues) { - const namedSlotContents = createNamedSlots(slotProps, slotValues, byRef); + if (slotArgs && Object.keys(slotArgs).length > 0) { + const namedSlotContents = createNamedSlots(slotProps, slotArgs, byRef); return `<${name} ${props}>\n${namedSlotContents}\n`; } + if (children && children.length > 0) { + const childrenSource = children.map((child: any) => { + return generateSource( + typeof child.value === 'string' ? getTemplates(child.value) : child.value, + args, + argTypes, + slotProps, + byRef + ); + }); + + if (childrenSource.join('').trim() === '') return `<${name} ${props}/>`; + + const isNativeTag = + name.includes('template') || + name.match(/^[a-z]/) || + (name === 'Fragment' && !name.includes('-')); + + return `<${name} ${isNativeTag ? '' : props}>\n${childrenSource}\n`; + } + return `<${name} ${props}/>`; }; - // handle one component or multiple + // get one component or multiple const components = isArray(compOrComps) ? compOrComps : [compOrComps]; - let source = ''; - // eslint-disable-next-line no-restricted-syntax - for (const comp of components) { - source += `${generateComponentSource(comp)}`; - } + const source = Object.keys(components) + .map((key: any) => `${generateComponentSource(components[key])}`) + .join(`\n`); return source; } + /** * create Named Slots content in source * @param slotProps - * @param slotValues + * @param slotArgs */ function createNamedSlots( slotProps: string[] | null | undefined, - slotValues: { [key: string]: any }, - byReference?: boolean | undefined + slotArgs: Args, + byRef?: boolean | undefined ) { if (!slotProps) return ''; - if (slotProps.length === 1) return !byReference ? slotValues[0] : `{{ ${slotProps[0]} }}`; + if (slotProps.length === 1) return !byRef ? slotArgs[slotProps[0]] : `{{ ${slotProps[0]} }}`; - return slotProps - .filter((slotProp) => slotValues[slotProps.indexOf(slotProp)]) - .map( - (slotProp) => - ` ` - ) + return Object.entries(slotArgs) + .map(([key, value]) => { + return ` `; + }) .join('\n'); } /** @@ -295,8 +285,8 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = const storyComponent = components.length ? components : ctxtComponent; const slotProps: string[] = getComponentSlots(ctxtComponent); - const withScript = context?.parameters?.docs?.source?.withSetupScript; - const generatedScript = withScript ? generateSetupScript(args, argTypes) : ''; + const withScript = context?.parameters?.docs?.source?.withScriptSetup || false; + const generatedScript = withScript ? generateScriptSetup(args, argTypes, components) : ''; const generatedTemplate = generateSource(storyComponent, args, argTypes, slotProps, withScript); if (generatedTemplate) { diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index a4309d7f6af..8630f03d67d 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -11,11 +11,11 @@ export const render: ArgsStoryFn = (props, context) => { `Unable to render story ${id} as the component annotation is missing from the default export` ); } - console.log(' render ', context, ' props', props); + return h(Component, props); }; -let setupFunction = (app: any) => { }; +let setupFunction = (app: any) => {}; export const setup = (fn: (app: any) => void) => { setupFunction = fn; };