From 9f1f4ef6e665420c3509ccc99338e992c9e9dfe9 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 23 Feb 2023 19:57:37 +0400 Subject: [PATCH 001/112] rewrite source decorator with optimisation --- .../vue3/src/docs/sourceDecorator.ts | 204 +++++++++--------- code/renderers/vue3/src/docs/tests/Button.vue | 50 +++++ .../vue3/src/docs/tests/source.test.ts | 26 +++ code/renderers/vue3/src/render.ts | 30 ++- 4 files changed, 191 insertions(+), 119 deletions(-) create mode 100644 code/renderers/vue3/src/docs/tests/Button.vue create mode 100644 code/renderers/vue3/src/docs/tests/source.test.ts diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 93a2fbd18c38..b8e8f9cb4be1 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -1,14 +1,24 @@ +/* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable no-underscore-dangle */ import { addons, useEffect } from '@storybook/preview-api'; import type { ArgTypes, Args, StoryContext, Renderer } from '@storybook/types'; import { SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; -// eslint-disable-next-line import/no-extraneous-dependencies -import parserHTML from 'prettier/parser-html'; - -// eslint-disable-next-line import/no-extraneous-dependencies -import { isArray } from '@vue/shared'; +import type { + TemplateChildNode, + ElementNode, + AttributeNode, + DirectiveNode, + TextNode, + InterpolationNode, +} from '@vue/compiler-core'; +import { + baseParse, + // ExpressionNode, + NodeTypes, +} from '@vue/compiler-core'; +import { h } from 'vue'; type ArgEntries = [string, any][]; type Attribute = { @@ -58,41 +68,14 @@ function getComponentNameAndChildren(component: any): { * @param argTypes * @param byRef */ -function generateAttributesSource(_args: Args, argTypes: ArgTypes, byRef?: boolean): string { - // create a copy of the args object to avoid modifying the original - const args = { ..._args }; - // filter out keys that are children or slots, and convert event keys to the proper format - const argsKeys = Object.keys(args) - .filter( - (key: any) => - ['children', 'slots'].indexOf(argTypes[key]?.table?.category) === -1 || !argTypes[key] // remove slots and children - ) - .map((key) => { - const akey = - argTypes[key]?.table?.category !== 'events' // is event - ? key - .replace(/([A-Z])/g, '-$1') - .replace(/^on-/, 'v-on:') - .replace(/^:/, '') - .toLowerCase() - : `v-on:${key}`; - args[akey] = args[key]; - return akey; - }) - .filter((key, index, self) => self.indexOf(key) === index); // remove duplicated keys - - const camelCase = (str: string) => str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); - const source = argsKeys - .map((key) => - generateAttributeSource( - byRef && !key.includes(':') ? `:${key}` : key, - byRef && !key.includes(':') ? camelCase(key) : args[key], - argTypes[key] - ) - ) +function generateAttributesSource( + args: (AttributeNode | DirectiveNode)[], + argTypes: ArgTypes, + byRef?: boolean +): string { + return Object.keys(args) + .map((key: any) => args[key].loc.source) .join(' '); - - return source; } function generateAttributeSource( @@ -139,32 +122,18 @@ function generateScriptSetup(args: Args, argTypes: ArgTypes, components: any[]): * get component templates one or more * @param renderFn */ -function getTemplates(renderFn: any): [] { +function getTemplates(renderFn: any): TemplateChildNode[] { 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, - children: Children, - }; - } - ); + const { template } = renderFn(); + const ast = baseParse(template); + const components = ast?.children; + if (!components) return []; return components; } catch (e) { - console.error(e); + // console.error(e); + console.log(' no template '); + return []; } - return []; } /** @@ -176,63 +145,92 @@ function getTemplates(renderFn: any): [] { * @param slotProp Prop used to simulate a slot */ export function generateSource( - compOrComps: any, + componentOrNode: TemplateChildNode[], args: Args, argTypes: ArgTypes, byRef?: boolean | undefined ): string | null { - if (!compOrComps) return null; - const generateComponentSource = (component: any): string | null => { - const { name, children, attributes } = getComponentNameAndChildren(component); - - if (!name) { - return ''; + const generateComponentSource = (component: TemplateChildNode) => { + // const { name, children, attributes } = getComponentNameAndChildren(component); + + let attributes; + let name; + let children; + let content; + if (component.type === 1) { + const child = component as ElementNode; + attributes = child.props; + name = child.tag; + children = child.children; + } + if (component.type === 5) { + const child = component as InterpolationNode; + content = child.content; } - const argsIn = attributes ? getArgsInAttrs(args, attributes) : args; // keep only args that are in attributes - const props = generateAttributesSource(argsIn, argTypes, byRef); - const slotArgs = Object.entries(argsIn).filter( - ([arg]) => argTypes[arg]?.table?.category === 'slots' - ); - const slotProps = Object.entries(argTypes).filter( - ([arg]) => argTypes[arg]?.table?.category === 'slots' - ); - if (slotArgs && slotArgs.length > 0) { - const namedSlotContents = createNamedSlots(slotArgs, slotProps, byRef); - return `<${name} ${props}>\n${namedSlotContents}\n`; + if (component.type === 2) { + const child = component as TextNode; + content = child.content; } - if (children && children.length > 0) { - const childrenSource = children.map((child: any) => { - return generateSource( - typeof child.value === 'string' ? getTemplates(child.value) : child.value, - args, - argTypes, - byRef - ); - }); + if (typeof (component as any).render === 'function') { + // children = child.children; + const vnode = h(component, args); + if (vnode.props) { + const { props } = vnode; + const attributesNode = mapAttributesAndDirectives(props); - if (childrenSource.join('').trim() === '') return `<${name} ${props}/>`; + attributes = attributesNode; + } + name = vnode.type.__docgenInfo.displayName; + } - const isNativeTag = - name.includes('template') || - name.match(/^[a-z]/) || - (name === 'Fragment' && !name.includes('-')); + let source = ''; + const argsIn = attributes ?? []; // keep only args that are in attributes + const props = generateAttributesSource(argsIn, argTypes, byRef); + if (name) source += `<${name} ${props} >`; - return `<${name} ${isNativeTag ? '' : props}>\n${childrenSource}\n`; + if (children) { + source += children.map((node: TemplateChildNode) => generateComponentSource(node)).concat(''); } - - return `<${name} ${props}/>`; + if (content) { + if (typeof content !== 'string') content = args[content.content.toString().split('.')[1]]; + source += content; + } + if (name) source += ``; + return source; }; - // get one component or multiple - const components = isArray(compOrComps) ? compOrComps : [compOrComps]; - const source = Object.keys(components) - .map((key: any) => `${generateComponentSource(components[key])}`) - .join(`\n`); + const source = Object.keys(componentOrNode) + .map((key: any) => generateComponentSource(componentOrNode[key])) + .join(' '); + return source; } +function mapAttributesAndDirectives(props: Args) { + const eventDirective = (key: string, value: unknown) => + typeof value === 'function' + ? `${key.replace(/on([A-Z][a-z]+)/g, '@$1').toLowerCase()}='()=>{}'` + : `${key}='${value}'`; + + const source = (key: string, value: unknown) => + ['boolean', 'number', 'object'].includes(typeof value) + ? `:${key}='${value}'` + : eventDirective(key, value); + + return Object.keys(props) + .map((key) => ({ + name: key, + type: ['v-', '@', 'v-on'].includes(key) ? 7 : 6, + arg: { content: key, loc: { source: key } }, + loc: { source: source(key, props[key]) }, + exp: { isStatic: false, loc: { source: props[key] } }, + modifiers: [''], + })) + .concat(); +} + /** * create Named Slots content in source * @param slotProps @@ -299,13 +297,15 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = } const { args = {}, component: ctxtComponent, argTypes = {} } = context || {}; + const components = getTemplates(context?.originalStoryFn); - const storyComponent = components.length ? components : ctxtComponent; + const storyComponent = components.length ? components : [ctxtComponent as TemplateChildNode]; const withScript = context?.parameters?.docs?.source?.withScriptSetup || false; const generatedScript = withScript ? generateScriptSetup(args, argTypes, components) : ''; const generatedTemplate = generateSource(storyComponent, args, argTypes, withScript); + console.log(' generatedTemplate ', generatedTemplate); if (generatedTemplate) { source = `${generatedScript}\n `; diff --git a/code/renderers/vue3/src/docs/tests/Button.vue b/code/renderers/vue3/src/docs/tests/Button.vue new file mode 100644 index 000000000000..eb99ffbc5af2 --- /dev/null +++ b/code/renderers/vue3/src/docs/tests/Button.vue @@ -0,0 +1,50 @@ + + + \ No newline at end of file diff --git a/code/renderers/vue3/src/docs/tests/source.test.ts b/code/renderers/vue3/src/docs/tests/source.test.ts new file mode 100644 index 000000000000..4b11b4e6589c --- /dev/null +++ b/code/renderers/vue3/src/docs/tests/source.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from '@jest/globals'; +import { baseParse } from '@vue/compiler-core'; +import Button from './Button.vue'; + +describe('generateSource Vue3', () => { + test('get template from storyFn :', () => { + const storyFn = (args: any) => ({ + components: { Button }, + setup() { + return { + args, + }; + }, + template: `
+ + {{args.label}}`, + }); + const storyFnString = storyFn.toString(); + console.log(' storyFn ', storyFnString); + const { template } = storyFn(); + const ast = baseParse(template); + const { children, type } = ast; + console.log({ children, type }); + expect({ booleanProp: true }).toEqual({ booleanProp: true }); + }); +}); diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 6c2c68559812..19e095bc5367 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -2,7 +2,7 @@ import { createApp, h, reactive } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { Args, StoryContext } from '@storybook/csf'; -import type { StoryFnVueReturnType, VueRenderer } from './types'; +import type { VueRenderer } from './types'; export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; @@ -22,24 +22,23 @@ export const setup = (fn: (app: any) => void) => { const map = new Map< VueRenderer['canvasElement'], - { vueApp: ReturnType; reactiveArgs: any } + { vueApp: ReturnType; reactiveArgs: any; rootComponent: any } >(); -const elementMap = new Map(); - export function renderToCanvas( { storyFn, forceRemount, showMain, showException, storyContext }: RenderContext, canvasElement: VueRenderer['canvasElement'] ) { // fetch the story with the updated context (with reactive args) + const existingApp = map.get(canvasElement); + storyContext.args = reactive(storyContext.args); - const element: StoryFnVueReturnType = storyFn(); - elementMap.set(canvasElement, element); + const rootComponent: any = storyFn(); // !existingApp ? storyFn() : existingApp.rootComponent(); - const props = (element as any).render?.().props; - const reactiveArgs = props ? reactive(props) : storyContext.args; + const appProps = + rootComponent.props ?? (typeof rootComponent === 'function' ? rootComponent().props : {}); + const reactiveArgs = Object.keys(appProps).length > 0 ? reactive(appProps) : storyContext.args; - const existingApp = map.get(canvasElement); if (existingApp && !forceRemount) { updateArgs(existingApp.reactiveArgs, reactiveArgs); return () => { @@ -51,10 +50,8 @@ export function renderToCanvas( const storybookApp = createApp({ render() { - const renderedElement: any = elementMap.get(canvasElement); - const current = renderedElement && renderedElement.template ? renderedElement : element; - map.set(canvasElement, { vueApp: storybookApp, reactiveArgs }); - return h(current, reactiveArgs); + map.set(canvasElement, { vueApp: storybookApp, reactiveArgs, rootComponent }); + return h(rootComponent, reactiveArgs); }, }); @@ -89,11 +86,10 @@ function getSlots(props: Args, context: StoryContext) { * @param nextArgs * @returns */ -function updateArgs(reactiveArgs: Args, nextArgs: Args) { +export function updateArgs(reactiveArgs: Args, nextArgs: Args) { if (!nextArgs) return; - Object.keys(reactiveArgs).forEach((key) => { - delete reactiveArgs[key]; - }); + + Object.keys(reactiveArgs).forEach((key) => delete reactiveArgs[key]); Object.assign(reactiveArgs, nextArgs); } From c416f708809781534b67a29494ebee6b2faaf2f8 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 23 Feb 2023 21:37:00 +0400 Subject: [PATCH 002/112] some experimental --- .../vue3/src/docs/sourceDecorator.ts | 95 ++++++------------- 1 file changed, 28 insertions(+), 67 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index b8e8f9cb4be1..1a59b1e4c783 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -13,11 +13,7 @@ import type { TextNode, InterpolationNode, } from '@vue/compiler-core'; -import { - baseParse, - // ExpressionNode, - NodeTypes, -} from '@vue/compiler-core'; +import { baseParse } from '@vue/compiler-core'; import { h } from 'vue'; type ArgEntries = [string, any][]; @@ -46,22 +42,6 @@ const skipSourceRender = (context: StoryContext) => { return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE; }; -/** - * Extract a component name. - * - * @param component Component - */ -function getComponentNameAndChildren(component: any): { - name: string | null; - children: any; - attributes: any; -} { - return { - name: component?.name || component?.__name || component?.__docgenInfo?.__name || null, - children: component?.children || null, - attributes: component?.attributes || component?.attrs || null, - }; -} /** * * @param _args @@ -69,39 +49,23 @@ function getComponentNameAndChildren(component: any): { * @param byRef */ function generateAttributesSource( - args: (AttributeNode | DirectiveNode)[], + tempArgs: (AttributeNode | DirectiveNode)[], + args: Args, argTypes: ArgTypes, byRef?: boolean ): string { - return Object.keys(args) - .map((key: any) => args[key].loc.source) + return Object.keys(tempArgs) + .map((key: any) => { + const arg = tempArgs[key]; + console.log(`---- arg ${key} `, arg); + const storyArg = args[arg.arg.content]; + console.log(`---- arg.name ${arg.arg.content} `, args); + console.log(`---- storyArg ${arg.arg} `, storyArg); + return tempArgs[key].loc.source; + }) .join(' '); } -function generateAttributeSource( - key: string, - value: Args[keyof Args], - argType: ArgTypes[keyof ArgTypes] -): string { - if (!value) { - return ''; - } - - if (value === true) { - return key; - } - - if (key.startsWith('v-on:')) { - return `${key}='() => {}'`; - } - - if (typeof value === 'string') { - return `${key}='${value}'`; - } - - return `:${key}='${JSON.stringify(value)}'`; -} - /** * * @param args generate script setup from args @@ -131,7 +95,6 @@ function getTemplates(renderFn: any): TemplateChildNode[] { return components; } catch (e) { // console.error(e); - console.log(' no template '); return []; } } @@ -151,8 +114,6 @@ export function generateSource( byRef?: boolean | undefined ): string | null { const generateComponentSource = (component: TemplateChildNode) => { - // const { name, children, attributes } = getComponentNameAndChildren(component); - let attributes; let name; let children; @@ -172,9 +133,8 @@ export function generateSource( const child = component as TextNode; content = child.content; } - - if (typeof (component as any).render === 'function') { - // children = child.children; + const concreteComponent = component as any; + if (typeof concreteComponent.render === 'function') { const vnode = h(component, args); if (vnode.props) { const { props } = vnode; @@ -182,12 +142,12 @@ export function generateSource( attributes = attributesNode; } - name = vnode.type.__docgenInfo.displayName; + name = concreteComponent.tag || concreteComponent.name || concreteComponent.__name; } let source = ''; - const argsIn = attributes ?? []; // keep only args that are in attributes - const props = generateAttributesSource(argsIn, argTypes, byRef); + const templateAttrs = attributes ?? []; // keep only args that are in attributes + const props = generateAttributesSource(templateAttrs, args, argTypes, byRef); if (name) source += `<${name} ${props} >`; if (children) { @@ -219,16 +179,17 @@ function mapAttributesAndDirectives(props: Args) { ? `:${key}='${value}'` : eventDirective(key, value); - return Object.keys(props) - .map((key) => ({ - name: key, - type: ['v-', '@', 'v-on'].includes(key) ? 7 : 6, - arg: { content: key, loc: { source: key } }, - loc: { source: source(key, props[key]) }, - exp: { isStatic: false, loc: { source: props[key] } }, - modifiers: [''], - })) - .concat(); + return Object.keys(props).map( + (key) => + ({ + name: 'bind', + type: ['v-', '@', 'v-on'].includes(key) ? 7 : 6, + arg: { content: key, loc: { source: key } }, + loc: { source: source(key, props[key]) }, + exp: { isStatic: false, loc: { source: props[key] } }, + modifiers: [''], + } as unknown as AttributeNode) + ); } /** From 3dc96b1aca1b2ad495893bca960b1cd44b43d97f Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Fri, 24 Feb 2023 13:43:09 +0400 Subject: [PATCH 003/112] start writing propre tests for source decorator --- .../vue3/src/docs/sourceDecorator.test.ts | 149 +++++++++--------- 1 file changed, 74 insertions(+), 75 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index d3189e9eb767..dd04efa27886 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from '@jest/globals'; import type { Args } from '@storybook/types'; -import { generateSource } from './sourceDecorator'; + +import { generateSource, getComponentsFromTemplate } from './sourceDecorator'; expect.addSnapshotSerializer({ print: (val: any) => val, @@ -12,84 +13,82 @@ function generateArgTypes(args: Args, slotProps: string[] | undefined) { return acc; }, {} as Record); } -function generateForArgs(args: Args, slotProps: string[] | undefined = undefined) { - return generateSource({ name: 'Component' }, args, generateArgTypes(args, slotProps), true); -} -function generateMultiComponentForArgs(args: Args, slotProps: string[] | undefined = undefined) { - return generateSource( - [{ name: 'Component' }, { name: 'Component' }], - args, - generateArgTypes(args, slotProps), - true - ); + +function generateForArgs( + args: Args, + slotProps: string[] | undefined = undefined, + template = '' +) { + const components = getComponentsFromTemplate(template); + return generateSource(components, args, generateArgTypes(args, slotProps), true); } -describe('generateSource Vue3', () => { +describe('generateSource snippet Vue3', () => { test('boolean true', () => { - expect(generateForArgs({ booleanProp: true })).toMatchInlineSnapshot( - `` - ); + expect( + generateForArgs({ booleanProp: true }, [], ``) + ).toMatchInlineSnapshot(``); }); test('boolean false', () => { - expect(generateForArgs({ booleanProp: false })).toMatchInlineSnapshot( - `` - ); - }); - test('null property', () => { - expect(generateForArgs({ nullProp: null })).toMatchInlineSnapshot( - `` - ); - }); - test('string property', () => { - expect(generateForArgs({ stringProp: 'mystr' })).toMatchInlineSnapshot( - `` - ); - }); - test('number property', () => { - expect(generateForArgs({ numberProp: 42 })).toMatchInlineSnapshot( - `` - ); - }); - test('object property', () => { - 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 }} - - `); - }); - test('multiple slot property with second slot value not set', () => { - expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, ['content', 'footer'])) - .toMatchInlineSnapshot(` - - {{ content }} - - `); - }); - 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('multi component with boolean true', () => { - expect(generateMultiComponentForArgs({ booleanProp: true })).toMatchInlineSnapshot(` - - - `); - }); - test('component is not set', () => { - expect(generateSource(null, {}, {})).toBeNull(); + expect( + generateForArgs({ booleanProp: false }, [], ``) + ).toMatchInlineSnapshot(``); }); + // test('null property', () => { + // expect(generateForArgs({ nullProp: null })).toMatchInlineSnapshot( + // `` + // ); + // }); + // test('string property', () => { + // expect(generateForArgs({ stringProp: 'mystr' })).toMatchInlineSnapshot( + // `` + // ); + // }); + // test('number property', () => { + // expect(generateForArgs({ numberProp: 42 })).toMatchInlineSnapshot( + // `` + // ); + // }); + // test('object property', () => { + // 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 }} + // + // `); + // }); + // test('multiple slot property with second slot value not set', () => { + // expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, ['content', 'footer'])) + // .toMatchInlineSnapshot(` + // + // {{ content }} + // + // `); + // }); + // 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('multi component with boolean true', () => { + // expect(generateMultiComponentForArgs({ booleanProp: true })).toMatchInlineSnapshot(` + // + // + // `); + // }); + // test('component is not set', () => { + // expect(generateSource(null, {}, {})).toBeNull(); + // }); }); From 207ab63e8d9cf88f6161b633d8455936abd1eb1d Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Fri, 24 Feb 2023 16:00:32 +0400 Subject: [PATCH 004/112] write tests for 2 main src decorator functions --- .../vue3/src/docs/sourceDecorator.test.ts | 330 ++++++++++++++---- 1 file changed, 265 insertions(+), 65 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index dd04efa27886..18c1296f4d19 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -1,7 +1,13 @@ import { describe, expect, test } from '@jest/globals'; import type { Args } from '@storybook/types'; -import { generateSource, getComponentsFromTemplate } from './sourceDecorator'; +import type { ArgsType } from 'jest-mock'; +import { + generateSource, + getComponentsFromTemplate, + mapAttributesAndDirectives, + generateAttributesSource, +} from './sourceDecorator'; expect.addSnapshotSerializer({ print: (val: any) => val, @@ -23,72 +29,266 @@ function generateForArgs( return generateSource(components, args, generateArgTypes(args, slotProps), true); } -describe('generateSource snippet Vue3', () => { - test('boolean true', () => { +describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { + test('camelCase boolean Arg', () => { + expect(mapAttributesAndDirectives({ camelCaseBooleanArg: true })).toMatchInlineSnapshot(` + Array [ + Object { + arg: Object { + content: camel-case-boolean-arg, + loc: Object { + source: camel-case-boolean-arg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: true, + }, + }, + loc: Object { + source: :camel-case-boolean-arg='true', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + ] + `); + }); + test('camelCase string Arg', () => { + expect(mapAttributesAndDirectives({ camelCaseStringArg: 'foo' })).toMatchInlineSnapshot(` + Array [ + Object { + arg: Object { + content: camel-case-string-arg, + loc: Object { + source: camel-case-string-arg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: foo, + }, + }, + loc: Object { + source: camel-case-string-arg='foo', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + ] + `); + }); + test('boolean arg', () => { + expect(mapAttributesAndDirectives({ booleanarg: true })).toMatchInlineSnapshot(` + Array [ + Object { + arg: Object { + content: booleanarg, + loc: Object { + source: booleanarg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: true, + }, + }, + loc: Object { + source: :booleanarg='true', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + ] + `); + }); + test('string arg', () => { + expect(mapAttributesAndDirectives({ stringarg: 'bar' })).toMatchInlineSnapshot(` + Array [ + Object { + arg: Object { + content: stringarg, + loc: Object { + source: stringarg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: bar, + }, + }, + loc: Object { + source: stringarg='bar', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + ] + `); + }); + test('number arg', () => { + expect(mapAttributesAndDirectives({ numberarg: 2023 })).toMatchInlineSnapshot(` + Array [ + Object { + arg: Object { + content: numberarg, + loc: Object { + source: numberarg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: 2023, + }, + }, + loc: Object { + source: :numberarg='2023', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + ] + `); + }); + test('camelCase boolean, string, and number Args', () => { expect( - generateForArgs({ booleanProp: true }, [], ``) - ).toMatchInlineSnapshot(``); + mapAttributesAndDirectives({ + camelCaseBooleanArg: true, + camelCaseStringArg: 'foo', + cameCaseNumberArg: 2023, + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + arg: Object { + content: camel-case-boolean-arg, + loc: Object { + source: camel-case-boolean-arg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: true, + }, + }, + loc: Object { + source: :camel-case-boolean-arg='true', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + Object { + arg: Object { + content: camel-case-string-arg, + loc: Object { + source: camel-case-string-arg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: foo, + }, + }, + loc: Object { + source: camel-case-string-arg='foo', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + Object { + arg: Object { + content: came-case-number-arg, + loc: Object { + source: came-case-number-arg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: 2023, + }, + }, + loc: Object { + source: :came-case-number-arg='2023', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + ] + `); }); - test('boolean false', () => { +}); + +describe('Vue3: sourceDecorator->generateAttributesSource()', () => { + test('camelCase boolean Arg', () => { expect( - generateForArgs({ booleanProp: false }, [], ``) - ).toMatchInlineSnapshot(``); + generateAttributesSource( + mapAttributesAndDirectives({ camelCaseBooleanArg: true }), + { camelCaseBooleanArg: true }, + [{ camelCaseBooleanArg: { type: 'boolean' } }] as ArgsType + ) + ).toMatchInlineSnapshot(`:camel-case-boolean-arg='true'`); + }); + test('camelCase string Arg', () => { + expect( + generateAttributesSource( + mapAttributesAndDirectives({ camelCaseStringArg: 'foo' }), + { camelCaseStringArg: 'foo' }, + [{ camelCaseStringArg: { type: 'string' } }] as ArgsType + ) + ).toMatchInlineSnapshot(`camel-case-string-arg='foo'`); + }); + + test('camelCase boolean, string, and number Args', () => { + expect( + generateAttributesSource( + mapAttributesAndDirectives({ + camelCaseBooleanArg: true, + camelCaseStringArg: 'foo', + cameCaseNumberArg: 2023, + }), + { + camelCaseBooleanArg: true, + camelCaseStringArg: 'foo', + cameCaseNumberArg: 2023, + }, + [] as ArgsType + ) + ).toMatchInlineSnapshot( + `:camel-case-boolean-arg='true' camel-case-string-arg='foo' :came-case-number-arg='2023'` + ); }); - // test('null property', () => { - // expect(generateForArgs({ nullProp: null })).toMatchInlineSnapshot( - // `` - // ); - // }); - // test('string property', () => { - // expect(generateForArgs({ stringProp: 'mystr' })).toMatchInlineSnapshot( - // `` - // ); - // }); - // test('number property', () => { - // expect(generateForArgs({ numberProp: 42 })).toMatchInlineSnapshot( - // `` - // ); - // }); - // test('object property', () => { - // 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 }} - // - // `); - // }); - // test('multiple slot property with second slot value not set', () => { - // expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, ['content', 'footer'])) - // .toMatchInlineSnapshot(` - // - // {{ content }} - // - // `); - // }); - // 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('multi component with boolean true', () => { - // expect(generateMultiComponentForArgs({ booleanProp: true })).toMatchInlineSnapshot(` - // - // - // `); - // }); - // test('component is not set', () => { - // expect(generateSource(null, {}, {})).toBeNull(); - // }); }); + +describe('generateSource snippet Vue3', () => {}); From 6ab9fc1c364d33c0e2b35c81ee492503dfb31dda Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Fri, 24 Feb 2023 16:01:40 +0400 Subject: [PATCH 005/112] add new tests --- .../vue3/src/docs/sourceDecorator.ts | 166 +++++++----------- code/renderers/vue3/src/docs/tests/Button.vue | 50 ------ .../vue3/src/docs/tests/source.test.ts | 26 --- 3 files changed, 65 insertions(+), 177 deletions(-) delete mode 100644 code/renderers/vue3/src/docs/tests/Button.vue delete mode 100644 code/renderers/vue3/src/docs/tests/source.test.ts diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 1a59b1e4c783..e1569d49222e 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -6,23 +6,18 @@ import type { ArgTypes, Args, StoryContext, Renderer } from '@storybook/types'; import { SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; import type { - TemplateChildNode, ElementNode, AttributeNode, DirectiveNode, TextNode, InterpolationNode, + TemplateChildNode, } from '@vue/compiler-core'; import { baseParse } from '@vue/compiler-core'; +import type { Component } from 'vue'; import { h } from 'vue'; +import { kebabCase } from 'lodash'; -type ArgEntries = [string, any][]; -type Attribute = { - name: string; - value: string; - sourceSpan?: any; - valueSpan?: any; -} & Record; /** * Check if the sourcecode should be generated. * @@ -42,13 +37,22 @@ const skipSourceRender = (context: StoryContext) => { return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE; }; +const directiveSource = (key: string, value: unknown) => + typeof value === 'function' + ? `${key.replace(/on([A-Z][a-z]+)/g, '@$1').toLowerCase()}='()=>{}'` + : `${key}='${value}'`; + +const attributeSource = (key: string, value: unknown) => + ['boolean', 'number', 'object'].includes(typeof value) + ? `:${key}='${JSON.stringify(value)}'` + : directiveSource(key, value); /** * * @param _args * @param argTypes * @param byRef */ -function generateAttributesSource( +export function generateAttributesSource( tempArgs: (AttributeNode | DirectiveNode)[], args: Args, argTypes: ArgTypes, @@ -57,10 +61,15 @@ function generateAttributesSource( return Object.keys(tempArgs) .map((key: any) => { const arg = tempArgs[key]; - console.log(`---- arg ${key} `, arg); - const storyArg = args[arg.arg.content]; - console.log(`---- arg.name ${arg.arg.content} `, args); - console.log(`---- storyArg ${arg.arg} `, storyArg); + if (arg.type === 7) { + const { exp, arg: argName } = arg; + const argKey = argName?.content; + const argExpValue = exp?.content; + const argValue = argKey ? args[argKey] : JSON.stringify(args); + return argKey + ? attributeSource(argKey, argValue) + : tempArgs[key].loc.source.replace(`"${argExpValue}"`, `'${argValue}'`); + } return tempArgs[key].loc.source; }) .join(' '); @@ -86,15 +95,19 @@ function generateScriptSetup(args: Args, argTypes: ArgTypes, components: any[]): * get component templates one or more * @param renderFn */ -function getTemplates(renderFn: any): TemplateChildNode[] { +function getComponentsFromRenderFn(renderFn: any): TemplateChildNode[] { + const { template } = renderFn(); + if (!template) return []; + return getComponentsFromTemplate(template); +} + +export function getComponentsFromTemplate(template: string): TemplateChildNode[] { try { - const { template } = renderFn(); const ast = baseParse(template); const components = ast?.children; if (!components) return []; return components; } catch (e) { - // console.error(e); return []; } } @@ -108,41 +121,43 @@ function getTemplates(renderFn: any): TemplateChildNode[] { * @param slotProp Prop used to simulate a slot */ export function generateSource( - componentOrNode: TemplateChildNode[], + componentOrNode: (TemplateChildNode | (Component & { type?: number }))[], args: Args, argTypes: ArgTypes, byRef?: boolean | undefined ): string | null { - const generateComponentSource = (component: TemplateChildNode) => { + const generateComponentSource = ( + component: TemplateChildNode | (Component & { type?: number }) + ) => { let attributes; let name; let children; let content; - if (component.type === 1) { - const child = component as ElementNode; - attributes = child.props; - name = child.tag; - children = child.children; - } - if (component.type === 5) { - const child = component as InterpolationNode; - content = child.content; - } - - if (component.type === 2) { - const child = component as TextNode; - content = child.content; + if (component) { + if (component.type === 1) { + const child = component as ElementNode; + attributes = child.props; + name = child.tag; + children = child.children; + } + if (component.type === 5) { + const child = component as InterpolationNode; + content = child.content; + } + if (component.type === 2) { + const child = component as TextNode; + content = child.content; + } } - const concreteComponent = component as any; + const concreteComponent = component as Component & { render: any }; if (typeof concreteComponent.render === 'function') { const vnode = h(component, args); if (vnode.props) { const { props } = vnode; - const attributesNode = mapAttributesAndDirectives(props); - - attributes = attributesNode; + attributes = mapAttributesAndDirectives(props); } - name = concreteComponent.tag || concreteComponent.name || concreteComponent.__name; + name = + vnode.type || concreteComponent.tag || concreteComponent.name || concreteComponent.__name; } let source = ''; @@ -151,7 +166,7 @@ export function generateSource( if (name) source += `<${name} ${props} >`; if (children) { - source += children.map((node: TemplateChildNode) => generateComponentSource(node)).concat(''); + source += children.map((node: TemplateChildNode) => generateComponentSource(node)).join(' '); } if (content) { if (typeof content !== 'string') content = args[content.content.toString().split('.')[1]]; @@ -161,79 +176,26 @@ export function generateSource( return source; }; - const source = Object.keys(componentOrNode) - .map((key: any) => generateComponentSource(componentOrNode[key])) - .join(' '); + if (Array.isArray(componentOrNode)) { + return componentOrNode.map((node) => generateComponentSource(node)).join(' '); + } - return source; + return null; } -function mapAttributesAndDirectives(props: Args) { - const eventDirective = (key: string, value: unknown) => - typeof value === 'function' - ? `${key.replace(/on([A-Z][a-z]+)/g, '@$1').toLowerCase()}='()=>{}'` - : `${key}='${value}'`; - - const source = (key: string, value: unknown) => - ['boolean', 'number', 'object'].includes(typeof value) - ? `:${key}='${value}'` - : eventDirective(key, value); - +export function mapAttributesAndDirectives(props: Args) { return Object.keys(props).map( (key) => ({ name: 'bind', - type: ['v-', '@', 'v-on'].includes(key) ? 7 : 6, - arg: { content: key, loc: { source: key } }, - loc: { source: source(key, props[key]) }, - exp: { isStatic: false, loc: { source: props[key] } }, + type: ['v-', '@', 'v-on'].includes(key) ? 7 : 6, // 6 is attribute, 7 is directive + arg: { content: kebabCase(key), loc: { source: kebabCase(key) } }, // attribute name or directive name (v-bind, v-on, v-model) + loc: { source: attributeSource(kebabCase(key), props[key]) }, // attribute value or directive value + exp: { isStatic: false, loc: { source: props[key] } }, // directive expression modifiers: [''], } as unknown as AttributeNode) ); } - -/** - * create Named Slots content in source - * @param slotProps - * @param slotArgs - */ - -function createNamedSlots(slotArgs: ArgEntries, slotProps: ArgEntries, byRef?: boolean) { - if (!slotArgs) return ''; - const many = slotProps.length > 1; - return slotArgs - .map(([key, value]) => { - const content = !byRef ? JSON.stringify(value) : `{{ ${key} }}`; - return many ? ` ` : ` ${content}`; - }) - .join('\n'); -} - -function getArgsInAttrs(args: Args, attributes: Attribute[]) { - return Object.keys(args).reduce((acc, prop) => { - if (attributes?.find((attr: any) => attr.name === 'v-bind')) { - acc[prop] = args[prop]; - } - const attribute = attributes?.find( - (attr: any) => attr.name === prop || attr.name === `:${prop}` - ); - if (attribute) { - acc[prop] = attribute.name === `:${prop}` ? args[prop] : attribute.value; - } - if (Object.keys(acc).length === 0) { - attributes?.forEach((attr: any) => { - acc[attr.name] = JSON.parse(JSON.stringify(attr.value)); - }); - } - return acc; - }, {} as Record); -} - -/** - * format prettier for vue - * @param source - */ - /** * source decorator. * @param storyFn Fn @@ -259,7 +221,9 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = const { args = {}, component: ctxtComponent, argTypes = {} } = context || {}; - const components = getTemplates(context?.originalStoryFn); + const components = getComponentsFromRenderFn(context?.originalStoryFn); + + console.log(' components ', components); const storyComponent = components.length ? components : [ctxtComponent as TemplateChildNode]; diff --git a/code/renderers/vue3/src/docs/tests/Button.vue b/code/renderers/vue3/src/docs/tests/Button.vue deleted file mode 100644 index eb99ffbc5af2..000000000000 --- a/code/renderers/vue3/src/docs/tests/Button.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - \ No newline at end of file diff --git a/code/renderers/vue3/src/docs/tests/source.test.ts b/code/renderers/vue3/src/docs/tests/source.test.ts deleted file mode 100644 index 4b11b4e6589c..000000000000 --- a/code/renderers/vue3/src/docs/tests/source.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expect, test } from '@jest/globals'; -import { baseParse } from '@vue/compiler-core'; -import Button from './Button.vue'; - -describe('generateSource Vue3', () => { - test('get template from storyFn :', () => { - const storyFn = (args: any) => ({ - components: { Button }, - setup() { - return { - args, - }; - }, - template: `
- - {{args.label}}`, - }); - const storyFnString = storyFn.toString(); - console.log(' storyFn ', storyFnString); - const { template } = storyFn(); - const ast = baseParse(template); - const { children, type } = ast; - console.log({ children, type }); - expect({ booleanProp: true }).toEqual({ booleanProp: true }); - }); -}); From 46307ba9905199b4f69173da80eb685c19026552 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sun, 26 Feb 2023 01:13:08 +0400 Subject: [PATCH 006/112] adjusting the render with tests --- code/renderers/vue3/src/decorateStory.ts | 6 +- .../vue3/src/docs/sourceDecorator.ts | 116 ++++++++++++++---- code/renderers/vue3/src/render.ts | 4 +- 3 files changed, 98 insertions(+), 28 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index e2a56ec7204b..575842f16b9c 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -33,11 +33,7 @@ function prepare( }; } - return { - render() { - return h(story); - }, - }; + return () => h(story); } export function decorateStory( diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index e1569d49222e..ca24568bd6b8 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -3,7 +3,7 @@ import { addons, useEffect } from '@storybook/preview-api'; import type { ArgTypes, Args, StoryContext, Renderer } from '@storybook/types'; -import { SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; +import { DocgenInfo, getDocgenSection, SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; import type { ElementNode, @@ -15,8 +15,8 @@ import type { } from '@vue/compiler-core'; import { baseParse } from '@vue/compiler-core'; import type { Component } from 'vue'; -import { h } from 'vue'; -import { kebabCase } from 'lodash'; +import { toDisplayString, h } from 'vue'; +import { camelCase, kebabCase } from 'lodash'; /** * Check if the sourcecode should be generated. @@ -37,14 +37,25 @@ const skipSourceRender = (context: StoryContext) => { return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE; }; +const displayObject = (obj: Args) => { + const a = Object.keys(obj).map((key) => `${key}:"${obj[key]}"`); + return `{${a.join(',')}}`; +}; +const htmlEventAttributeToVueEventAttribute = (key: string) => { + return /^on[A-Za-z]/.test(key) ? key.replace(/^on/, 'v-on:').toLowerCase() : key; +}; +// html event attribute to vue event attribute +// is html event attribute + const directiveSource = (key: string, value: unknown) => - typeof value === 'function' - ? `${key.replace(/on([A-Z][a-z]+)/g, '@$1').toLowerCase()}='()=>{}'` + key.includes('on') + ? `${htmlEventAttributeToVueEventAttribute(key)}='()=>({})'` : `${key}='${value}'`; const attributeSource = (key: string, value: unknown) => + // convert html event key to vue event key ['boolean', 'number', 'object'].includes(typeof value) - ? `:${key}='${JSON.stringify(value)}'` + ? `:${key}='${value && typeof value === 'object' ? displayObject(value) : value}'` : directiveSource(key, value); /** * @@ -61,14 +72,17 @@ export function generateAttributesSource( return Object.keys(tempArgs) .map((key: any) => { const arg = tempArgs[key]; + console.log('------> arg:', arg); if (arg.type === 7) { const { exp, arg: argName } = arg; const argKey = argName?.content; const argExpValue = exp?.content; - const argValue = argKey ? args[argKey] : JSON.stringify(args); + const propValue = args[camelCase(argKey)]; + console.log('-->argKey', argKey, 'argExpValue :', argExpValue, 'propValue :', propValue); + const argValue = argKey ? propValue : toDisplayString(args); return argKey ? attributeSource(argKey, argValue) - : tempArgs[key].loc.source.replace(`"${argExpValue}"`, `'${argValue}'`); + : toDisplayString(tempArgs[key].loc.source); // tempArgs[key].loc.source.replace(`"${argExpValue}"`, `'${argValue}'`); } return tempArgs[key].loc.source; }) @@ -96,12 +110,16 @@ function generateScriptSetup(args: Args, argTypes: ArgTypes, components: any[]): * @param renderFn */ function getComponentsFromRenderFn(renderFn: any): TemplateChildNode[] { - const { template } = renderFn(); - if (!template) return []; - return getComponentsFromTemplate(template); + try { + const { template } = renderFn(); + if (!template) return []; + return getComponentsFromTemplate(template); + } catch (e) { + return []; + } } -export function getComponentsFromTemplate(template: string): TemplateChildNode[] { +function getComponentsFromTemplate(template: string): TemplateChildNode[] { try { const ast = baseParse(template); const components = ast?.children; @@ -149,15 +167,35 @@ export function generateSource( content = child.content; } } - const concreteComponent = component as Component & { render: any }; + const concreteComponent = component as Component & { + render: any; + props: any; + slots: any; + tag?: string; + name?: string; + __name?: string; + }; if (typeof concreteComponent.render === 'function') { const vnode = h(component, args); if (vnode.props) { const { props } = vnode; - attributes = mapAttributesAndDirectives(props); + concreteComponent.slots = getDocgenSection(concreteComponent, 'slots'); + const slotsProps = {} as Args; + const attrsProps = { ...props } as Args; + Object.keys(props).forEach((prop: any) => { + const isSlot = concreteComponent.slots.find( + ({ name: slotName }: { name: string }) => slotName === prop + ); + if (isSlot?.name) { + slotsProps[prop] = props[prop]; + delete attrsProps[prop]; + } + }); + + attributes = mapAttributesAndDirectives(attrsProps); + children = mapSlots(slotsProps); } - name = - vnode.type || concreteComponent.tag || concreteComponent.name || concreteComponent.__name; + name = concreteComponent.tag || concreteComponent.name || concreteComponent.__name; } let source = ''; @@ -166,10 +204,11 @@ export function generateSource( if (name) source += `<${name} ${props} >`; if (children) { - source += children.map((node: TemplateChildNode) => generateComponentSource(node)).join(' '); + source += children.map((node: TemplateChildNode) => generateComponentSource(node)).join(''); } if (content) { - if (typeof content !== 'string') content = args[content.content.toString().split('.')[1]]; + // eslint-disable-next-line no-eval + if (typeof content !== 'string') content = eval(content.loc.source); // it's a binding safe to eval source += content; } if (name) source += ``; @@ -183,14 +222,15 @@ export function generateSource( return null; } -export function mapAttributesAndDirectives(props: Args) { +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: kebabCase(key), loc: { source: kebabCase(key) } }, // attribute name or directive name (v-bind, v-on, v-model) - loc: { source: attributeSource(kebabCase(key), props[key]) }, // attribute value or directive value + 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) @@ -238,3 +278,37 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = return story; }; +// export local function for testing purpose +export { + generateScriptSetup, + getComponentsFromRenderFn, + getComponentsFromTemplate, + mapAttributesAndDirectives, + attributeSource, + htmlEventAttributeToVueEventAttribute, +}; + +function mapSlots(slotsProps: Args): TextNode[] { + return Object.keys(slotsProps).map((key) => { + const slot = slotsProps[key]; + let slotContent = ''; + if (typeof slot === 'function') slotContent = ``; + if (key === 'default') { + slotContent = JSON.stringify(slot); + } + slotContent = ``; + + const txt: TextNode = { + type: 2, + content: slotContent, + loc: { + source: slotContent, + start: { offset: 0, line: 1, column: 0 }, + end: { offset: 0, line: 1, column: 0 }, + }, + }; + return txt; + }); + + // TODO: handle other cases (array, object, html,etc) +} diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 19e095bc5367..968976eda4b9 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -11,7 +11,7 @@ export const render: ArgsStoryFn = (props, context) => { `Unable to render story ${id} as the component annotation is missing from the default export` ); } - + console.log(' props ', props); return h(Component, props, getSlots(props, context)); }; @@ -33,7 +33,7 @@ export function renderToCanvas( const existingApp = map.get(canvasElement); storyContext.args = reactive(storyContext.args); - const rootComponent: any = storyFn(); // !existingApp ? storyFn() : existingApp.rootComponent(); + const rootComponent: any = !existingApp ? storyFn() : existingApp.rootComponent; const appProps = rootComponent.props ?? (typeof rootComponent === 'function' ? rootComponent().props : {}); From 4c14b109bd24e9d439dc102106fc19ee4c188fb5 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sun, 26 Feb 2023 01:14:21 +0400 Subject: [PATCH 007/112] add more tests for sources decorator --- .../vue3/src/docs/sourceDecorator.test.ts | 107 +++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index 18c1296f4d19..3ca9aed946a1 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -7,6 +7,8 @@ import { getComponentsFromTemplate, mapAttributesAndDirectives, generateAttributesSource, + attributeSource, + htmlEventAttributeToVueEventAttribute as htmlEventToVueEvent, } from './sourceDecorator'; expect.addSnapshotSerializer({ @@ -291,4 +293,107 @@ describe('Vue3: sourceDecorator->generateAttributesSource()', () => { }); }); -describe('generateSource snippet Vue3', () => {}); +describe('Vue3: generateSource() snippet', () => { + test('template component camelCase string Arg', () => { + expect( + generateForArgs( + { + camelCaseStringArg: 'foo', + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot(``); + }); + + test('template component camelCase bool Arg', () => { + expect( + generateForArgs( + { + camelCaseBooleanArg: true, + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot(``); + }); + + test('template component camelCase bool, string Arg', () => { + expect( + generateForArgs( + { + camelCaseBooleanArg: true, + camelCaseStringArg: 'foo', + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot( + `` + ); + }); + + test('template component camelCase object Arg', () => { + expect( + generateForArgs( + { + camelCaseObjectArg: { foo: 'bar' }, + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot(``); + }); + + test('template component camelCase object Arg and Slot', () => { + expect( + generateForArgs( + { + camelCaseObjectArg: { foo: 'bar' }, + }, + [] as ArgsType, + ` SLOT ` + ) + ).toMatchInlineSnapshot(` SLOT `); + }); + + test('template component camelCase object Arg and dynamic Slot content', () => { + expect( + generateForArgs( + { + camelCaseObjectArg: { foo: 'bar' }, + camelCaseStringSlotArg: 'foo', + }, + [] as ArgsType, + ` SLOT {{args.camelCaseStringSlotArg}}` + ) + ).toMatchInlineSnapshot( + ` SLOT foo` + ); + }); +}); + +describe('Vue3: sourceDecorator->attributeSoure()', () => { + test('camelCase boolean Arg', () => { + expect(attributeSource('stringArg', 'foo')).toMatchInlineSnapshot(`stringArg='foo'`); + }); + + test('html event attribute should convert to vue event directive', () => { + expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); + expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); + }); + test('normal html attribute should not convert to vue event directive', () => { + expect(attributeSource('on-click', () => {})).toMatchInlineSnapshot(`on-click='()=>({})'`); + }); + test('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`); + }); +}); From c8b7e68c9f6461aaea4914f160c01fd38c019755 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sun, 26 Feb 2023 01:15:17 +0400 Subject: [PATCH 008/112] testing the render --- code/renderers/vue3/src/docs/sourceDecorator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index ca24568bd6b8..53b45b140d3e 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -3,7 +3,7 @@ import { addons, useEffect } from '@storybook/preview-api'; import type { ArgTypes, Args, StoryContext, Renderer } from '@storybook/types'; -import { DocgenInfo, getDocgenSection, SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; +import { getDocgenSection, SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; import type { ElementNode, From c8e157eca62da102991141ef0d037d882eaa6e9a Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 27 Feb 2023 15:35:40 +0400 Subject: [PATCH 009/112] vue3 fixing prepareStory for decotators --- code/renderers/vue3/src/decorateStory.ts | 20 +++++- .../vue3/src/docs/sourceDecorator.ts | 8 +-- code/renderers/vue3/src/render.ts | 65 +++++++++++-------- 3 files changed, 59 insertions(+), 34 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 575842f16b9c..37192c41c8d4 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -30,10 +30,26 @@ function prepare( // Normalize so we can always spread an object ...normalizeFunctionalComponent(story), components: { ...(story.components || {}), story: innerStory }, + renderTracked(event) { + console.log('innerStory renderTracked', event); + }, + renderTriggered(event) { + console.log('innerStory renderTriggered', event); + }, }; } - return () => h(story); + return { + render() { + return h(story, this.$props); + }, + renderTracked(event) { + console.log('story renderTracked', event); + }, + renderTriggered(event) { + console.log('story renderTriggered', event); + }, + }; } export function decorateStory( @@ -60,7 +76,7 @@ export function decorateStory( return story; } - return prepare(decoratedStory, h(story, context.args)) as VueRenderer['storyResult']; + return prepare(decoratedStory, story) as VueRenderer['storyResult']; }, (context) => prepare(storyFn(context)) as LegacyStoryFn ); diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 53b45b140d3e..b35afeb0271b 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -72,13 +72,13 @@ export function generateAttributesSource( return Object.keys(tempArgs) .map((key: any) => { const arg = tempArgs[key]; - console.log('------> arg:', arg); + if (arg.type === 7) { - const { exp, arg: argName } = arg; + const { arg: argName } = arg; const argKey = argName?.content; - const argExpValue = exp?.content; + // const argExpValue = exp?.content; const propValue = args[camelCase(argKey)]; - console.log('-->argKey', argKey, 'argExpValue :', argExpValue, 'propValue :', propValue); + const argValue = argKey ? propValue : toDisplayString(args); return argKey ? attributeSource(argKey, argValue) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 968976eda4b9..ff326fee847f 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,5 +1,4 @@ -/* eslint-disable no-param-reassign */ -import { createApp, h, reactive } from 'vue'; +import { createApp, h, isReactive, reactive } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { Args, StoryContext } from '@storybook/csf'; import type { VueRenderer } from './types'; @@ -11,7 +10,7 @@ export const render: ArgsStoryFn = (props, context) => { `Unable to render story ${id} as the component annotation is missing from the default export` ); } - console.log(' props ', props); + return h(Component, props, getSlots(props, context)); }; @@ -22,46 +21,56 @@ export const setup = (fn: (app: any) => void) => { const map = new Map< VueRenderer['canvasElement'], - { vueApp: ReturnType; reactiveArgs: any; rootComponent: any } + { vueApp: ReturnType; reactiveArgs: any } >(); export function renderToCanvas( { storyFn, forceRemount, showMain, showException, storyContext }: RenderContext, canvasElement: VueRenderer['canvasElement'] ) { - // fetch the story with the updated context (with reactive args) const existingApp = map.get(canvasElement); - - storyContext.args = reactive(storyContext.args); - const rootComponent: any = !existingApp ? storyFn() : existingApp.rootComponent; - - const appProps = - rootComponent.props ?? (typeof rootComponent === 'function' ? rootComponent().props : {}); - const reactiveArgs = Object.keys(appProps).length > 0 ? reactive(appProps) : storyContext.args; - + const reactiveArgs: Args = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; + // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { - updateArgs(existingApp.reactiveArgs, reactiveArgs); + updateArgs(existingApp.reactiveArgs, storyContext.args); return () => { teardown(existingApp.vueApp, canvasElement); }; } - if (existingApp && forceRemount) teardown(existingApp.vueApp, canvasElement); - const storybookApp = createApp({ - render() { - map.set(canvasElement, { vueApp: storybookApp, reactiveArgs, rootComponent }); - return h(rootComponent, reactiveArgs); + // create vue app for the story + const vueStoryApp = createApp({ + setup() { + let { args } = storyContext; + args = reactive(reactiveArgs); + const rootComponent = storyFn(args); + map.set(canvasElement, { + vueApp: vueStoryApp, + reactiveArgs, + }); + return () => h(rootComponent, args); + }, + onMounted() { + map.set(canvasElement, { + vueApp: vueStoryApp, + reactiveArgs, + }); + }, + renderTracked(event) { + console.log('--renderTracked ', event); + }, + renderTriggered(event) { + console.log('--renderTriggered ', event); }, }); - - storybookApp.config.errorHandler = (e: unknown) => showException(e as Error); - setupFunction(storybookApp); - storybookApp.mount(canvasElement); + vueStoryApp.config.errorHandler = (e: unknown) => showException(e as Error); + setupFunction(vueStoryApp); + vueStoryApp.mount(canvasElement); showMain(); return () => { - teardown(storybookApp, canvasElement); + teardown(vueStoryApp, canvasElement); }; } @@ -87,10 +96,10 @@ function getSlots(props: Args, context: StoryContext) { * @returns */ export function updateArgs(reactiveArgs: Args, nextArgs: Args) { - if (!nextArgs) return; - - Object.keys(reactiveArgs).forEach((key) => delete reactiveArgs[key]); - Object.assign(reactiveArgs, nextArgs); + const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs); + Object.entries(nextArgs).forEach(([key, value]) => { + currentArgs[key] = value; + }); } function teardown( From 2a81e98b981014bd1e16240ff58ffe646fd5d80b Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 27 Feb 2023 17:24:01 +0400 Subject: [PATCH 010/112] some refactoring and cleanup --- code/renderers/vue3/src/docs/sourceDecorator.ts | 2 +- code/renderers/vue3/src/render.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index b35afeb0271b..c2fccc56882f 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -75,7 +75,7 @@ export function generateAttributesSource( if (arg.type === 7) { const { arg: argName } = arg; - const argKey = argName?.content; + const argKey = argName?.loc.source; // const argExpValue = exp?.content; const propValue = args[camelCase(argKey)]; diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index ff326fee847f..dbdf220cb527 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-param-reassign */ import { createApp, h, isReactive, reactive } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { Args, StoryContext } from '@storybook/csf'; @@ -42,14 +43,13 @@ export function renderToCanvas( // create vue app for the story const vueStoryApp = createApp({ setup() { - let { args } = storyContext; - args = reactive(reactiveArgs); - const rootComponent = storyFn(args); + storyContext.args = reactive(reactiveArgs); + const rootComponent = storyFn(); map.set(canvasElement, { vueApp: vueStoryApp, reactiveArgs, }); - return () => h(rootComponent, args); + return () => h(rootComponent, reactiveArgs); }, onMounted() { map.set(canvasElement, { @@ -58,10 +58,10 @@ export function renderToCanvas( }); }, renderTracked(event) { - console.log('--renderTracked ', event); + console.log('vueApp--renderTracked ', event); }, renderTriggered(event) { - console.log('--renderTriggered ', event); + console.log('vueApp--renderTriggered ', event); }, }); vueStoryApp.config.errorHandler = (e: unknown) => showException(e as Error); From 233361a61365cd2782d9e0758b2647cb9cef0fba Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 27 Feb 2023 22:36:15 +0400 Subject: [PATCH 011/112] fix null slots --- code/renderers/vue3/src/render.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index dbdf220cb527..ebcfd3442527 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -30,6 +30,7 @@ export function renderToCanvas( canvasElement: VueRenderer['canvasElement'] ) { const existingApp = map.get(canvasElement); + const reactiveArgs: Args = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { @@ -41,19 +42,19 @@ export function renderToCanvas( if (existingApp && forceRemount) teardown(existingApp.vueApp, canvasElement); // create vue app for the story - const vueStoryApp = createApp({ + const vueApp = createApp({ setup() { storyContext.args = reactive(reactiveArgs); - const rootComponent = storyFn(); + const rootElement = storyFn(); map.set(canvasElement, { - vueApp: vueStoryApp, + vueApp, reactiveArgs, }); - return () => h(rootComponent, reactiveArgs); + return () => h(rootElement, reactiveArgs); }, onMounted() { map.set(canvasElement, { - vueApp: vueStoryApp, + vueApp, reactiveArgs, }); }, @@ -64,13 +65,13 @@ export function renderToCanvas( console.log('vueApp--renderTriggered ', event); }, }); - vueStoryApp.config.errorHandler = (e: unknown) => showException(e as Error); - setupFunction(vueStoryApp); - vueStoryApp.mount(canvasElement); + vueApp.config.errorHandler = (e: unknown) => showException(e as Error); + setupFunction(vueApp); + vueApp.mount(canvasElement); showMain(); return () => { - teardown(vueStoryApp, canvasElement); + teardown(vueApp, canvasElement); }; } From 2b205f8a5ed2e7beeb3baf07049fdad34c457d01 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 27 Feb 2023 22:37:24 +0400 Subject: [PATCH 012/112] handle no slots , null issue --- .../vue3/src/docs/sourceDecorator.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index c2fccc56882f..cb2ff6131304 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -75,7 +75,7 @@ export function generateAttributesSource( if (arg.type === 7) { const { arg: argName } = arg; - const argKey = argName?.loc.source; + const argKey = argName?.loc.source ?? (argName as any).content; // const argExpValue = exp?.content; const propValue = args[camelCase(argKey)]; @@ -180,17 +180,17 @@ export function generateSource( if (vnode.props) { const { props } = vnode; concreteComponent.slots = getDocgenSection(concreteComponent, 'slots'); + const { slots } = concreteComponent; const slotsProps = {} as Args; const attrsProps = { ...props } as Args; - Object.keys(props).forEach((prop: any) => { - const isSlot = concreteComponent.slots.find( - ({ name: slotName }: { name: string }) => slotName === prop - ); - if (isSlot?.name) { - slotsProps[prop] = props[prop]; - delete attrsProps[prop]; - } - }); + if (slots) + Object.keys(props).forEach((prop: any) => { + const isSlot = slots.find(({ name: slotName }: { name: string }) => slotName === prop); + if (isSlot?.name) { + slotsProps[prop] = props[prop]; + delete attrsProps[prop]; + } + }); attributes = mapAttributesAndDirectives(attrsProps); children = mapSlots(slotsProps); From b874cbe0fc1b53da540a5d55bbfe411a4b3000a7 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Wed, 1 Mar 2023 16:29:36 +0400 Subject: [PATCH 013/112] fix args Inheritance and reactivity --- .../store/template/stories/args.stories.ts | 1 + code/renderers/vue3/src/decorateStory.ts | 31 ++++++++++++++++--- .../vue3/src/docs/sourceDecorator.ts | 26 +++++++--------- code/renderers/vue3/src/render.ts | 27 +++++++++------- 4 files changed, 55 insertions(+), 30 deletions(-) diff --git a/code/lib/store/template/stories/args.stories.ts b/code/lib/store/template/stories/args.stories.ts index e1e6eb7104f4..e655f9e9d50f 100644 --- a/code/lib/store/template/stories/args.stories.ts +++ b/code/lib/store/template/stories/args.stories.ts @@ -73,6 +73,7 @@ export const Events = { await within(canvasElement).findByText(/initial/); await channel.emit(UPDATE_STORY_ARGS, { storyId: id, updatedArgs: { test: 'updated' } }); + await new Promise((resolve) => channel.once(STORY_ARGS_UPDATED, resolve)); await within(canvasElement).findByText(/updated/); await channel.emit(RESET_STORY_ARGS, { storyId: id }); diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 37192c41c8d4..ab905becc1de 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -31,10 +31,10 @@ function prepare( ...normalizeFunctionalComponent(story), components: { ...(story.components || {}), story: innerStory }, renderTracked(event) { - console.log('innerStory renderTracked', event); + // console.log('innerStory renderTracked', event); }, renderTriggered(event) { - console.log('innerStory renderTriggered', event); + // console.log('innerStory renderTriggered', event); }, }; } @@ -44,10 +44,10 @@ function prepare( return h(story, this.$props); }, renderTracked(event) { - console.log('story renderTracked', event); + // console.log('story renderTracked', event); }, renderTriggered(event) { - console.log('story renderTriggered', event); + // console.log('story renderTriggered', event); }, }; } @@ -61,6 +61,9 @@ export function decorateStory( let story: VueRenderer['storyResult'] | undefined; const decoratedStory: VueRenderer['storyResult'] = decorator((update) => { + // we should update the context with the update object from the decorator in reactive way + // so that the story will be re-rendered with the new context + updateReactiveContext(context, update); story = decorated({ ...context, ...sanitizeStoryContextUpdate(update), @@ -81,3 +84,23 @@ export function decorateStory( (context) => prepare(storyFn(context)) as LegacyStoryFn ); } + +function updateReactiveContext( + context: StoryContext, + update: + | import('@storybook/csf').StoryContextUpdate> + | undefined +) { + if (update) { + const { args, argTypes } = update; + if (args && !argTypes) { + const deepCopy = JSON.parse(JSON.stringify(args)); + Object.keys(context.args).forEach((key) => { + delete context.args[key]; + }); + Object.keys(args).forEach((key) => { + context.args[key] = deepCopy[key]; + }); + } + } +} diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index cb2ff6131304..8bd2866ff0a1 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -263,14 +263,11 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = const components = getComponentsFromRenderFn(context?.originalStoryFn); - console.log(' components ', components); - const storyComponent = components.length ? components : [ctxtComponent as TemplateChildNode]; const withScript = context?.parameters?.docs?.source?.withScriptSetup || false; const generatedScript = withScript ? generateScriptSetup(args, argTypes, components) : ''; const generatedTemplate = generateSource(storyComponent, args, argTypes, withScript); - console.log(' generatedTemplate ', generatedTemplate); if (generatedTemplate) { source = `${generatedScript}\n `; @@ -278,15 +275,6 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = return story; }; -// export local function for testing purpose -export { - generateScriptSetup, - getComponentsFromRenderFn, - getComponentsFromTemplate, - mapAttributesAndDirectives, - attributeSource, - htmlEventAttributeToVueEventAttribute, -}; function mapSlots(slotsProps: Args): TextNode[] { return Object.keys(slotsProps).map((key) => { @@ -298,7 +286,7 @@ function mapSlots(slotsProps: Args): TextNode[] { } slotContent = ``; - const txt: TextNode = { + return { type: 2, content: slotContent, loc: { @@ -307,8 +295,16 @@ function mapSlots(slotsProps: Args): TextNode[] { end: { offset: 0, line: 1, column: 0 }, }, }; - return txt; }); - // TODO: handle other cases (array, object, html,etc) } + +// export local function for testing purpose +export { + generateScriptSetup, + getComponentsFromRenderFn, + getComponentsFromTemplate, + mapAttributesAndDirectives, + attributeSource, + htmlEventAttributeToVueEventAttribute, +}; diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index ebcfd3442527..68b42e3245a6 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -31,7 +31,8 @@ export function renderToCanvas( ) { const existingApp = map.get(canvasElement); - const reactiveArgs: Args = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; + const reactiveArgs = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; + // updateArgs(reactiveArgs, storyContext.initialArgs); // update the reactiveArgs with the latest args // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { updateArgs(existingApp.reactiveArgs, storyContext.args); @@ -44,25 +45,21 @@ export function renderToCanvas( // create vue app for the story const vueApp = createApp({ setup() { - storyContext.args = reactive(reactiveArgs); + storyContext.args = reactiveArgs; const rootElement = storyFn(); - map.set(canvasElement, { - vueApp, - reactiveArgs, - }); return () => h(rootElement, reactiveArgs); }, - onMounted() { + mounted() { map.set(canvasElement, { vueApp, reactiveArgs, }); }, renderTracked(event) { - console.log('vueApp--renderTracked ', event); + // console.log('vueApp--renderTracked ', event); }, renderTriggered(event) { - console.log('vueApp--renderTriggered ', event); + // console.log('vueApp--renderTriggered ', event); }, }); vueApp.config.errorHandler = (e: unknown) => showException(e as Error); @@ -98,8 +95,16 @@ function getSlots(props: Args, context: StoryContext) { */ export function updateArgs(reactiveArgs: Args, nextArgs: Args) { const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs); - Object.entries(nextArgs).forEach(([key, value]) => { - currentArgs[key] = value; + + Object.keys(currentArgs).forEach((key) => { + const componentArg = currentArgs[key]; + if (typeof componentArg === 'object') { + Object.keys(componentArg).forEach((key2) => { + componentArg[key2] = nextArgs[key2]; + }); + } else { + currentArgs[key] = nextArgs[key]; + } }); } From 3d0b620e82638eadd1d9b4d9ea6237b9c2cac9d2 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Wed, 1 Mar 2023 20:29:54 +0400 Subject: [PATCH 014/112] fix null value exception for mdx stories --- .../vue3/src/docs/sourceDecorator.ts | 45 ++++++++++--------- code/renderers/vue3/src/render.ts | 2 +- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 8bd2866ff0a1..dd550176a839 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -75,7 +75,7 @@ export function generateAttributesSource( if (arg.type === 7) { const { arg: argName } = arg; - const argKey = argName?.loc.source ?? (argName as any).content; + const argKey = argName?.loc.source ?? (argName as any)?.content; // const argExpValue = exp?.content; const propValue = args[camelCase(argKey)]; @@ -139,7 +139,10 @@ function getComponentsFromTemplate(template: string): TemplateChildNode[] { * @param slotProp Prop used to simulate a slot */ export function generateSource( - componentOrNode: (TemplateChildNode | (Component & { type?: number }))[], + componentOrNode: + | (TemplateChildNode | (Component & { type?: number }))[] + | TemplateChildNode + | (Component & { type?: number }), args: Args, argTypes: ArgTypes, byRef?: boolean | undefined @@ -151,22 +154,23 @@ export function generateSource( let name; let children; let content; - if (component) { - if (component.type === 1) { - const child = component as ElementNode; - attributes = child.props; - name = child.tag; - children = child.children; - } - if (component.type === 5) { - const child = component as InterpolationNode; - content = child.content; - } - if (component.type === 2) { - const child = component as TextNode; - content = child.content; - } + if (!component) return null; + + if (component.type === 1) { + const child = component as ElementNode; + attributes = child.props; + name = child.tag; + children = child.children; } + if (component.type === 5) { + const child = component as InterpolationNode; + content = child.content; + } + if (component.type === 2) { + const child = component as TextNode; + content = child.content; + } + const concreteComponent = component as Component & { render: any; props: any; @@ -214,8 +218,9 @@ export function generateSource( if (name) source += ``; return source; }; - - if (Array.isArray(componentOrNode)) { + if (componentOrNode && !Array.isArray(componentOrNode)) + return generateComponentSource(componentOrNode); + if (componentOrNode && componentOrNode.length) { return componentOrNode.map((node) => generateComponentSource(node)).join(' '); } @@ -263,7 +268,7 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = const components = getComponentsFromRenderFn(context?.originalStoryFn); - const storyComponent = components.length ? components : [ctxtComponent as TemplateChildNode]; + const storyComponent = components.length ? components : (ctxtComponent as TemplateChildNode); const withScript = context?.parameters?.docs?.source?.withScriptSetup || false; const generatedScript = withScript ? generateScriptSetup(args, argTypes, components) : ''; diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 68b42e3245a6..25b4edbb1293 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -32,7 +32,7 @@ export function renderToCanvas( const existingApp = map.get(canvasElement); const reactiveArgs = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; - // updateArgs(reactiveArgs, storyContext.initialArgs); // update the reactiveArgs with the latest args + // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { updateArgs(existingApp.reactiveArgs, storyContext.args); From da8b3b4051f8503acf4b370a34b206a38995ba2a Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Wed, 1 Mar 2023 20:38:50 +0400 Subject: [PATCH 015/112] fix slot double quote display --- code/renderers/vue3/src/render.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 25b4edbb1293..207a43ef37bc 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -82,7 +82,10 @@ function getSlots(props: Args, context: StoryContext) { const { argTypes } = context; const slots = Object.entries(props) .filter(([key, value]) => argTypes[key]?.table?.category === 'slots') - .map(([key, value]) => [key, () => h('span', JSON.stringify(value))]); + .map(([key, value]) => [ + key, + () => h('template', typeof value === 'object' ? JSON.stringify(value) : value), + ]); return Object.fromEntries(slots); } From dc32a8377aa41e8aba24973be20cb5830ec9af50 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Wed, 1 Mar 2023 22:23:47 +0400 Subject: [PATCH 016/112] fix source for mdx using originFn and context --- code/renderers/vue3/src/docs/sourceDecorator.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index dd550176a839..4e476bec49cd 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -109,9 +109,12 @@ function generateScriptSetup(args: Args, argTypes: ArgTypes, components: any[]): * get component templates one or more * @param renderFn */ -function getComponentsFromRenderFn(renderFn: any): TemplateChildNode[] { +function getComponentsFromRenderFn( + renderFn: any, + context?: StoryContext +): TemplateChildNode[] { try { - const { template } = renderFn(); + const { template } = context ? renderFn(context.args, context) : renderFn(); if (!template) return []; return getComponentsFromTemplate(template); } catch (e) { @@ -266,7 +269,7 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = const { args = {}, component: ctxtComponent, argTypes = {} } = context || {}; - const components = getComponentsFromRenderFn(context?.originalStoryFn); + const components = getComponentsFromRenderFn(context?.originalStoryFn, context); const storyComponent = components.length ? components : (ctxtComponent as TemplateChildNode); From 14eca7cf7bf52b56829a4eddfb4465e78f8b3ef9 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 2 Mar 2023 13:29:23 +0400 Subject: [PATCH 017/112] refactory generateSource to be elegante --- .../vue3/src/docs/sourceDecorator.ts | 148 ++++++++---------- code/renderers/vue3/src/render.ts | 1 - 2 files changed, 67 insertions(+), 82 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 4e476bec49cd..a7a21d5afbb6 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -14,10 +14,22 @@ import type { TemplateChildNode, } from '@vue/compiler-core'; import { baseParse } from '@vue/compiler-core'; -import type { Component } from 'vue'; +import type { Component, VNodeProps } from 'vue'; import { toDisplayString, h } from 'vue'; import { camelCase, kebabCase } from 'lodash'; +type StoryVueComponent = Component & { + render: any; + props: VNodeProps; + slots: any; + tag?: string; + name?: string; + __name?: string; + __file?: string; + __docs?: any; + __docsGen?: any; + __docsExtracted?: any; +}; /** * Check if the sourcecode should be generated. * @@ -75,7 +87,7 @@ export function generateAttributesSource( if (arg.type === 7) { const { arg: argName } = arg; - const argKey = argName?.loc.source ?? (argName as any)?.content; + const argKey = argName ? argName?.loc.source : undefined; // (argName as any)?.content; // const argExpValue = exp?.content; const propValue = args[camelCase(argKey)]; @@ -141,93 +153,67 @@ function getComponentsFromTemplate(template: string): TemplateChildNode[] { * @param argTypes ArgTypes * @param slotProp Prop used to simulate a slot */ -export function generateSource( - componentOrNode: - | (TemplateChildNode | (Component & { type?: number }))[] - | TemplateChildNode - | (Component & { type?: number }), + +function generateSource( + componentOrNodes: (StoryVueComponent | TemplateChildNode)[] | TemplateChildNode, args: Args, argTypes: ArgTypes, - byRef?: boolean | undefined -): string | null { - const generateComponentSource = ( - component: TemplateChildNode | (Component & { type?: number }) - ) => { - let attributes; - let name; - let children; - let content; - if (!component) return null; - - if (component.type === 1) { - const child = component as ElementNode; - attributes = child.props; - name = child.tag; - children = child.children; - } - if (component.type === 5) { - const child = component as InterpolationNode; - content = child.content; - } - if (component.type === 2) { - const child = component as TextNode; - content = child.content; + byRef = false +) { + const isComponent = (component: any) => component && typeof component.render === 'function'; + 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: StoryVueComponent | TemplateChildNode) => { + if (isElementNode(componentOrNode)) { + const { tag: name, props: attributes, children } = componentOrNode as ElementNode; + const childSources: string = children + .map((child: TemplateChildNode) => generateComponentSource(child)) + .join(''); + const props = generateAttributesSource(attributes, args, argTypes, byRef); + return `<${name} ${props}>${childSources}`; } - const concreteComponent = component as Component & { - render: any; - props: any; - slots: any; - tag?: string; - name?: string; - __name?: string; - }; - if (typeof concreteComponent.render === 'function') { - const vnode = h(component, args); - if (vnode.props) { - const { props } = vnode; - concreteComponent.slots = getDocgenSection(concreteComponent, 'slots'); - const { slots } = concreteComponent; - const slotsProps = {} as Args; - const attrsProps = { ...props } as Args; - if (slots) - Object.keys(props).forEach((prop: any) => { - const isSlot = slots.find(({ name: slotName }: { name: string }) => slotName === prop); - if (isSlot?.name) { - slotsProps[prop] = props[prop]; - delete attrsProps[prop]; - } - }); - - attributes = mapAttributesAndDirectives(attrsProps); - children = mapSlots(slotsProps); - } - name = concreteComponent.tag || concreteComponent.name || concreteComponent.__name; + if (isInterpolationNode(componentOrNode) || isTextNode(componentOrNode)) { + const { content } = componentOrNode as InterpolationNode | TextNode; + // eslint-disable-next-line no-eval + if (typeof content !== 'string') return eval(content.loc.source); // it's a binding safe to eval + return content; } - let source = ''; - const templateAttrs = attributes ?? []; // keep only args that are in attributes - const props = generateAttributesSource(templateAttrs, args, argTypes, byRef); - if (name) source += `<${name} ${props} >`; - - if (children) { - source += children.map((node: TemplateChildNode) => generateComponentSource(node)).join(''); + if (isComponent(componentOrNode)) { + const concreteComponent = componentOrNode as StoryVueComponent; + const vnode = h(componentOrNode, args); + const { props } = vnode; + const { slots } = getDocgenSection(concreteComponent, 'slots') || {}; + const slotsProps = {} as Args; + const attrsProps = { ...props }; + if (slots && props) + Object.keys(props).forEach((prop: any) => { + const isSlot = slots.find(({ name: slotName }: { name: string }) => slotName === prop); + if (isSlot?.name) { + slotsProps[prop] = props[prop]; + delete attrsProps[prop]; + } + }); + const attributes = mapAttributesAndDirectives(attrsProps); + const childSources: string = mapSlots(slotsProps) + .map((child) => generateComponentSource(child)) + .join(''); + const name = concreteComponent.tag || concreteComponent.name || concreteComponent.__name; + const propsSource = generateAttributesSource(attributes, args, argTypes, byRef); + return `<${name} ${propsSource}>${childSources}`; } - if (content) { - // eslint-disable-next-line no-eval - if (typeof content !== 'string') content = eval(content.loc.source); // it's a binding safe to eval - source += content; - } - if (name) source += ``; - return source; + + return null; }; - if (componentOrNode && !Array.isArray(componentOrNode)) - return generateComponentSource(componentOrNode); - if (componentOrNode && componentOrNode.length) { - return componentOrNode.map((node) => generateComponentSource(node)).join(' '); - } - return null; + const componentsOrNodes = Array.isArray(componentOrNodes) ? componentOrNodes : [componentOrNodes]; + const source = componentsOrNodes + .map((componentOrNode) => generateComponentSource(componentOrNode)) + .join(' '); + return source || null; } function mapAttributesAndDirectives(props: Args) { @@ -289,10 +275,10 @@ function mapSlots(slotsProps: Args): TextNode[] { const slot = slotsProps[key]; let slotContent = ''; if (typeof slot === 'function') slotContent = ``; + slotContent = ``; if (key === 'default') { slotContent = JSON.stringify(slot); } - slotContent = ``; return { type: 2, diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 207a43ef37bc..d56362c0e6c4 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -32,7 +32,6 @@ export function renderToCanvas( const existingApp = map.get(canvasElement); const reactiveArgs = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; - // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { updateArgs(existingApp.reactiveArgs, storyContext.args); From b7584963d5d346b82da6b526950976d8a216b005 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 2 Mar 2023 21:12:20 +0400 Subject: [PATCH 018/112] add some tests for source decorator and render --- .../vue3/src/docs/sourceDecorator.test.ts | 18 +++---- .../vue3/src/docs/sourceDecorator.ts | 2 +- code/renderers/vue3/src/render.test.ts | 51 +++++++++++++++++++ 3 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 code/renderers/vue3/src/render.test.ts diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index 3ca9aed946a1..78616e4d06a2 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -303,7 +303,7 @@ describe('Vue3: generateSource() snippet', () => { [] as ArgsType, `` ) - ).toMatchInlineSnapshot(``); + ).toMatchInlineSnapshot(``); }); test('template component camelCase bool Arg', () => { @@ -315,7 +315,7 @@ describe('Vue3: generateSource() snippet', () => { [] as ArgsType, `` ) - ).toMatchInlineSnapshot(``); + ).toMatchInlineSnapshot(``); }); test('template component camelCase bool, string Arg', () => { @@ -329,7 +329,7 @@ describe('Vue3: generateSource() snippet', () => { `` ) ).toMatchInlineSnapshot( - `` + `` ); }); @@ -342,7 +342,7 @@ describe('Vue3: generateSource() snippet', () => { [] as ArgsType, `` ) - ).toMatchInlineSnapshot(``); + ).toMatchInlineSnapshot(``); }); test('template component camelCase object Arg and Slot', () => { @@ -352,9 +352,9 @@ describe('Vue3: generateSource() snippet', () => { camelCaseObjectArg: { foo: 'bar' }, }, [] as ArgsType, - ` SLOT ` + ` SLOT ` ) - ).toMatchInlineSnapshot(` SLOT `); + ).toMatchInlineSnapshot(` SLOT `); }); test('template component camelCase object Arg and dynamic Slot content', () => { @@ -365,10 +365,10 @@ describe('Vue3: generateSource() snippet', () => { camelCaseStringSlotArg: 'foo', }, [] as ArgsType, - ` SLOT {{args.camelCaseStringSlotArg}}` + ` SLOT {{args.camelCaseStringSlotArg}}` ) ).toMatchInlineSnapshot( - ` SLOT foo` + ` SLOT foo` ); }); }); @@ -380,7 +380,7 @@ describe('Vue3: sourceDecorator->attributeSoure()', () => { test('html event attribute should convert to vue event directive', () => { expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); - expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); + expect(attributeSource('onclick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); }); test('normal html attribute should not convert to vue event directive', () => { expect(attributeSource('on-click', () => {})).toMatchInlineSnapshot(`on-click='()=>({})'`); diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index a7a21d5afbb6..fe91412f8818 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -154,7 +154,7 @@ function getComponentsFromTemplate(template: string): TemplateChildNode[] { * @param slotProp Prop used to simulate a slot */ -function generateSource( +export function generateSource( componentOrNodes: (StoryVueComponent | TemplateChildNode)[] | TemplateChildNode, args: Args, argTypes: ArgTypes, diff --git a/code/renderers/vue3/src/render.test.ts b/code/renderers/vue3/src/render.test.ts new file mode 100644 index 000000000000..c705fb16cdc1 --- /dev/null +++ b/code/renderers/vue3/src/render.test.ts @@ -0,0 +1,51 @@ +import { expectTypeOf } from 'expect-type'; + +import { reactive } from 'vue'; +import { updateArgs } from './render'; + +describe('Render Story', () => { + test('update reactive Args updateArgs()', () => { + const reactiveArgs = reactive({ argFoo: 'foo', argBar: 'bar' }); // get reference to reactiveArgs or create a new one; + expectTypeOf(reactiveArgs).toMatchTypeOf>(); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ argFoo: string; argBar: string }>(); + + const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; + updateArgs(reactiveArgs, newArgs); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ argFoo: string; argBar: string }>(); + expect(reactiveArgs).toEqual(newArgs); + }); + + test('update reactive Args component new arg updateArgs()', () => { + const reactiveArgs = reactive({ objectArg: { argFoo: 'foo', argBar: 'bar' } }); // get reference to reactiveArgs or create a new one; + expectTypeOf(reactiveArgs).toMatchTypeOf>(); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string; argBar: string } }>(); + + const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; + updateArgs(reactiveArgs, newArgs); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string; argBar: string } }>(); + expect(reactiveArgs).toEqual({ objectArg: newArgs }); + }); + + test('update reactive Args component 2 args updateArgs()', () => { + const reactiveArgs = reactive({ + objectArg: { argFoo: 'foo' }, + objectArg2: { argBar: 'bar' }, + }); // get reference to reactiveArgs or create a new one; + expectTypeOf(reactiveArgs).toMatchTypeOf>(); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ + objectArg: { argFoo: string }; + objectArg2: { argBar: string }; + }>(); + + const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; + updateArgs(reactiveArgs, newArgs); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ + objectArg: { argFoo: string }; + objectArg2: { argBar: string }; + }>(); + expect(reactiveArgs).toEqual({ + objectArg: { argFoo: newArgs.argFoo }, + objectArg2: { argBar: newArgs.argBar }, + }); + }); +}); From ada469dd401268bf53509b9573d93b97364f37c5 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sat, 4 Mar 2023 13:52:19 +0400 Subject: [PATCH 019/112] add some tests and fix CSF2 with decorator --- code/renderers/vue3/src/decorateStory.ts | 29 ++++++++------ code/renderers/vue3/src/render.test.ts | 25 ++++++++---- code/renderers/vue3/src/render.ts | 51 +++++++++++++++++++----- 3 files changed, 76 insertions(+), 29 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index ab905becc1de..21cb19537ef0 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -1,5 +1,5 @@ import type { ConcreteComponent, Component, ComponentOptions } from 'vue'; -import { h } from 'vue'; +import { reactive, h } from 'vue'; import type { DecoratorFunction, StoryContext, LegacyStoryFn } from '@storybook/types'; import { sanitizeStoryContextUpdate } from '@storybook/preview-api'; @@ -19,21 +19,22 @@ function prepare( rawStory: VueRenderer['storyResult'], innerStory?: ConcreteComponent ): Component | null { - const story = rawStory as ComponentOptions; + const story = normalizeFunctionalComponent(rawStory as ComponentOptions); if (story == null) { return null; } if (innerStory) { + // console.log('innerStory', innerStory); return { // Normalize so we can always spread an object ...normalizeFunctionalComponent(story), components: { ...(story.components || {}), story: innerStory }, - renderTracked(event) { + renderTracked() { // console.log('innerStory renderTracked', event); }, - renderTriggered(event) { + renderTriggered() { // console.log('innerStory renderTriggered', event); }, }; @@ -41,13 +42,13 @@ function prepare( return { render() { - return h(story, this.$props); + return h(story); }, renderTracked(event) { - // console.log('story renderTracked', event); + console.log('story renderTracked', event); }, renderTriggered(event) { - // console.log('story renderTriggered', event); + console.log('story renderTriggered', event); }, }; } @@ -63,11 +64,11 @@ export function decorateStory( const decoratedStory: VueRenderer['storyResult'] = decorator((update) => { // we should update the context with the update object from the decorator in reactive way // so that the story will be re-rendered with the new context - updateReactiveContext(context, update); story = decorated({ ...context, ...sanitizeStoryContextUpdate(update), }); + if (update) updateReactiveContext(context, update); return story; }, context); @@ -79,7 +80,8 @@ export function decorateStory( return story; } - return prepare(decoratedStory, story) as VueRenderer['storyResult']; + const storyFuntion = () => h(story ?? 'story', context.args); + return prepare(decoratedStory, storyFuntion) as VueRenderer['storyResult']; }, (context) => prepare(storyFn(context)) as LegacyStoryFn ); @@ -91,16 +93,19 @@ function updateReactiveContext( | import('@storybook/csf').StoryContextUpdate> | undefined ) { + context.args = reactive(context.args); if (update) { const { args, argTypes } = update; if (args && !argTypes) { const deepCopy = JSON.parse(JSON.stringify(args)); - Object.keys(context.args).forEach((key) => { - delete context.args[key]; - }); + console.log(' updated Args ', deepCopy); + // Object.keys(context.args).forEach((key) => { + // delete context.args[key]; + // }); Object.keys(args).forEach((key) => { context.args[key] = deepCopy[key]; }); + console.log(' updated context.args ', context.args); } } } diff --git a/code/renderers/vue3/src/render.test.ts b/code/renderers/vue3/src/render.test.ts index c705fb16cdc1..d2977f8d6248 100644 --- a/code/renderers/vue3/src/render.test.ts +++ b/code/renderers/vue3/src/render.test.ts @@ -15,7 +15,7 @@ describe('Render Story', () => { expect(reactiveArgs).toEqual(newArgs); }); - test('update reactive Args component new arg updateArgs()', () => { + test('update reactive Args component inherit objectArg updateArgs()', () => { const reactiveArgs = reactive({ objectArg: { argFoo: 'foo', argBar: 'bar' } }); // get reference to reactiveArgs or create a new one; expectTypeOf(reactiveArgs).toMatchTypeOf>(); expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string; argBar: string } }>(); @@ -23,7 +23,17 @@ describe('Render Story', () => { const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; updateArgs(reactiveArgs, newArgs); expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string; argBar: string } }>(); - expect(reactiveArgs).toEqual({ objectArg: newArgs }); + expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo2', argBar: 'bar2' } }); + }); + + test('update reactive Args component inherit objectArg only argName argName()', () => { + const reactiveArgs = reactive({ objectArg: { argFoo: 'foo' } }); // get reference to reactiveArgs or create a new one; + expectTypeOf(reactiveArgs).toMatchTypeOf>(); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string } }>(); + + const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; + updateArgs(reactiveArgs, newArgs, ['argFoo']); + expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo2' }, argBar: 'bar2' }); }); test('update reactive Args component 2 args updateArgs()', () => { @@ -39,13 +49,12 @@ describe('Render Story', () => { const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; updateArgs(reactiveArgs, newArgs); - expectTypeOf(reactiveArgs).toEqualTypeOf<{ - objectArg: { argFoo: string }; - objectArg2: { argBar: string }; - }>(); + expect(reactiveArgs).toEqual({ - objectArg: { argFoo: newArgs.argFoo }, - objectArg2: { argBar: newArgs.argBar }, + objectArg: { argFoo: 'foo2' }, + objectArg2: { argBar: 'bar2' }, + argFoo: 'foo2', + argBar: 'bar2', }); }); }); diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index f64d668da84c..c2b0fb7455cc 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,8 +1,8 @@ /* eslint-disable no-param-reassign */ -import { createApp, h, isReactive, reactive } from 'vue'; +import { createApp, h, isReactive, reactive, shallowReactive, watch } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; -import type { Args, StoryContext } from '@storybook/csf'; -import type { VueRenderer } from './types'; +import type { Globals, Args, StoryContext } from '@storybook/csf'; +import type { StoryFnVueReturnType, VueRenderer } from './types'; export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; @@ -22,9 +22,12 @@ export const setup = (fn: (app: any) => void) => { const map = new Map< VueRenderer['canvasElement'], - { vueApp: ReturnType; reactiveArgs: any } + { + vueApp: ReturnType; + reactiveArgs: Args; + } >(); - +let reactiveState: { globals: Globals }; export function renderToCanvas( { storyFn, forceRemount, showMain, showException, storyContext }: RenderContext, canvasElement: VueRenderer['canvasElement'] @@ -32,8 +35,12 @@ export function renderToCanvas( const existingApp = map.get(canvasElement); const reactiveArgs = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; + if (!reactiveState) reactiveState = shallowReactive({ globals: storyContext.globals }); + + // updateArgs(reactiveState.globals, storyContext.globals); // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { + reactiveState.globals = storyContext.globals; updateArgs(existingApp.reactiveArgs, storyContext.args); return () => { teardown(existingApp.vueApp, canvasElement); @@ -45,8 +52,24 @@ export function renderToCanvas( const vueApp = createApp({ setup() { storyContext.args = reactiveArgs; - const rootElement = storyFn(); - return () => h(rootElement, reactiveArgs); + + let rootElement: StoryFnVueReturnType; + watch( + reactiveState, + (newVal) => { + console.log('watching reactiveState ', reactiveState.globals); + rootElement = storyFn(storyContext); + // storyContext.globals = reactiveState.globals; + console.log('reactiveState newVaue ', newVal); + }, + { immediate: true } + ); + + return () => { + console.log('rerendering reactiveState', reactiveState); + console.log('rerendering storyContext.globals', storyContext.globals); + return h(rootElement, reactiveArgs); + }; }, mounted() { map.set(canvasElement, { @@ -92,15 +115,25 @@ function getSlots(props: Args, context: StoryContext) { * @param nextArgs * @returns */ -export function updateArgs(reactiveArgs: Args, nextArgs: Args) { +export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string[]) { const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs); Object.keys(currentArgs).forEach((key) => { const componentArg = currentArgs[key]; + if (typeof componentArg === 'object') { Object.keys(componentArg).forEach((key2) => { - componentArg[key2] = nextArgs[key2]; + if (nextArgs[key2] && (argNames?.includes(key2) || !argNames)) { + console.log(`-----${key2}:${currentArgs[key][key2]} => ${nextArgs[key2]}`); + currentArgs[key][key2] = nextArgs[key2]; + } + }); + Object.keys(nextArgs).forEach((key2) => { + if (currentArgs[key][key2] === undefined) { + currentArgs[key2] = nextArgs[key2]; + } }); + // console.log('updateArgs', key, currentArgs[key]); } else { currentArgs[key] = nextArgs[key]; } From 284313a393974d8c947238a5bf13d965b34ec521 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sat, 4 Mar 2023 18:30:15 +0400 Subject: [PATCH 020/112] fixing react decorator breaks the reactivity --- code/renderers/vue3/src/render.ts | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index c2b0fb7455cc..18182109bcce 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -27,7 +27,10 @@ const map = new Map< reactiveArgs: Args; } >(); -let reactiveState: { globals: Globals }; +let reactiveState: { + [x: string]: StoryFnVueReturnType; + globals: Globals; +}; export function renderToCanvas( { storyFn, forceRemount, showMain, showException, storyContext }: RenderContext, canvasElement: VueRenderer['canvasElement'] @@ -35,12 +38,11 @@ export function renderToCanvas( const existingApp = map.get(canvasElement); const reactiveArgs = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; - if (!reactiveState) reactiveState = shallowReactive({ globals: storyContext.globals }); - // updateArgs(reactiveState.globals, storyContext.globals); // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { - reactiveState.globals = storyContext.globals; + if (reactiveState) reactiveState.globals = storyContext.globals; + updateArgs(existingApp.reactiveArgs, storyContext.args); return () => { teardown(existingApp.vueApp, canvasElement); @@ -52,23 +54,18 @@ export function renderToCanvas( const vueApp = createApp({ setup() { storyContext.args = reactiveArgs; - - let rootElement: StoryFnVueReturnType; + const rootElement: StoryFnVueReturnType = storyFn(); + reactiveState = reactive({ globals: storyContext.globals, rootElement }); watch( - reactiveState, + () => reactiveState.globals, (newVal) => { - console.log('watching reactiveState ', reactiveState.globals); - rootElement = storyFn(storyContext); - // storyContext.globals = reactiveState.globals; - console.log('reactiveState newVaue ', newVal); - }, - { immediate: true } + reactiveState.rootElement = storyFn(); + // run decorator functions + } ); return () => { - console.log('rerendering reactiveState', reactiveState); - console.log('rerendering storyContext.globals', storyContext.globals); - return h(rootElement, reactiveArgs); + return h(reactiveState.rootElement, reactiveArgs); }; }, mounted() { From 4a7d98b2e34cc7b26de6c1456986003c1e84b589 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sat, 4 Mar 2023 19:40:23 +0400 Subject: [PATCH 021/112] use remount as easier way --- code/renderers/vue3/src/decorateStory.ts | 5 ++--- code/renderers/vue3/src/render.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 21cb19537ef0..cd8da1a91c6e 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -3,6 +3,7 @@ import { reactive, h } from 'vue'; import type { DecoratorFunction, StoryContext, LegacyStoryFn } from '@storybook/types'; import { sanitizeStoryContextUpdate } from '@storybook/preview-api'; +import type { Args, StoryContextUpdate } from '@storybook/csf'; import type { VueRenderer } from './types'; /* @@ -89,9 +90,7 @@ export function decorateStory( function updateReactiveContext( context: StoryContext, - update: - | import('@storybook/csf').StoryContextUpdate> - | undefined + update: StoryContextUpdate> | undefined ) { context.args = reactive(context.args); if (update) { diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 18182109bcce..6f4decaa8407 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,9 +1,12 @@ /* eslint-disable no-param-reassign */ -import { createApp, h, isReactive, reactive, shallowReactive, watch } from 'vue'; +import { createApp, h, isReactive, reactive, watch } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { Globals, Args, StoryContext } from '@storybook/csf'; +import { global as globalThis } from '@storybook/global'; import type { StoryFnVueReturnType, VueRenderer } from './types'; +const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; + export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; if (!Component) { @@ -32,7 +35,7 @@ let reactiveState: { globals: Globals; }; export function renderToCanvas( - { storyFn, forceRemount, showMain, showException, storyContext }: RenderContext, + { storyFn, forceRemount, showMain, showException, storyContext, id }: RenderContext, canvasElement: VueRenderer['canvasElement'] ) { const existingApp = map.get(canvasElement); @@ -59,8 +62,9 @@ export function renderToCanvas( watch( () => reactiveState.globals, (newVal) => { - reactiveState.rootElement = storyFn(); + // reactiveState.rootElement = storyFn(); // run decorator functions + channel.emit('forceRemount', { storyId: id }); } ); From f681d835212db582513c3b8841096bcc1ecfaaee Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sat, 4 Mar 2023 19:49:22 +0400 Subject: [PATCH 022/112] emit using channel forceRemount --- code/renderers/vue3/src/render.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 6f4decaa8407..869ab5465f8d 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -5,8 +5,6 @@ import type { Globals, Args, StoryContext } from '@storybook/csf'; import { global as globalThis } from '@storybook/global'; import type { StoryFnVueReturnType, VueRenderer } from './types'; -const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; - export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; if (!Component) { @@ -64,6 +62,7 @@ export function renderToCanvas( (newVal) => { // reactiveState.rootElement = storyFn(); // run decorator functions + const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; channel.emit('forceRemount', { storyId: id }); } ); From f670ef54d8a83ed1f81763e097bc435a77cb750b Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sat, 4 Mar 2023 21:43:59 +0400 Subject: [PATCH 023/112] cleanup code --- code/renderers/vue3/src/render.ts | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 869ab5465f8d..24b5fe9db033 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -29,7 +29,6 @@ const map = new Map< } >(); let reactiveState: { - [x: string]: StoryFnVueReturnType; globals: Globals; }; export function renderToCanvas( @@ -55,20 +54,19 @@ export function renderToCanvas( const vueApp = createApp({ setup() { storyContext.args = reactiveArgs; + reactiveState = reactive({ globals: storyContext.globals }); const rootElement: StoryFnVueReturnType = storyFn(); - reactiveState = reactive({ globals: storyContext.globals, rootElement }); + watch( () => reactiveState.globals, (newVal) => { - // reactiveState.rootElement = storyFn(); - // run decorator functions const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; channel.emit('forceRemount', { storyId: id }); } ); return () => { - return h(reactiveState.rootElement, reactiveArgs); + return h(rootElement, reactiveArgs); }; }, mounted() { @@ -77,12 +75,6 @@ export function renderToCanvas( reactiveArgs, }); }, - renderTracked(event) { - // console.log('vueApp--renderTracked ', event); - }, - renderTriggered(event) { - // console.log('vueApp--renderTriggered ', event); - }, }); vueApp.config.errorHandler = (e: unknown) => showException(e as Error); setupFunction(vueApp); @@ -120,11 +112,10 @@ export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string Object.keys(currentArgs).forEach((key) => { const componentArg = currentArgs[key]; - + // if the arg is an object, we need to update the object if (typeof componentArg === 'object') { Object.keys(componentArg).forEach((key2) => { if (nextArgs[key2] && (argNames?.includes(key2) || !argNames)) { - console.log(`-----${key2}:${currentArgs[key][key2]} => ${nextArgs[key2]}`); currentArgs[key][key2] = nextArgs[key2]; } }); @@ -133,12 +124,18 @@ export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string currentArgs[key2] = nextArgs[key2]; } }); - // console.log('updateArgs', key, currentArgs[key]); } else { currentArgs[key] = nextArgs[key]; } }); } +/** + * unmount the vue app + * @param storybookApp + * @param canvasElement + * @returns void + * @private + * */ function teardown( storybookApp: ReturnType, From a5284cd0ccda06a1be560875d8d786eba0880144 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sat, 4 Mar 2023 22:42:23 +0400 Subject: [PATCH 024/112] fix args after tests + cleanup --- code/renderers/vue3/src/decorateStory.ts | 8 +++----- code/renderers/vue3/src/render.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index cd8da1a91c6e..6892fb1337bf 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -97,14 +97,12 @@ function updateReactiveContext( const { args, argTypes } = update; if (args && !argTypes) { const deepCopy = JSON.parse(JSON.stringify(args)); - console.log(' updated Args ', deepCopy); - // Object.keys(context.args).forEach((key) => { - // delete context.args[key]; - // }); + Object.keys(context.args).forEach((key) => { + delete context.args[key]; + }); Object.keys(args).forEach((key) => { context.args[key] = deepCopy[key]; }); - console.log(' updated context.args ', context.args); } } } diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 24b5fe9db033..55a9636e5dab 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -60,7 +60,7 @@ export function renderToCanvas( watch( () => reactiveState.globals, (newVal) => { - const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; + const channel = (globalThis as any).__STORYBOOK_ADDONS_CHANNEL__; channel.emit('forceRemount', { storyId: id }); } ); @@ -119,14 +119,14 @@ export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string currentArgs[key][key2] = nextArgs[key2]; } }); - Object.keys(nextArgs).forEach((key2) => { - if (currentArgs[key][key2] === undefined) { - currentArgs[key2] = nextArgs[key2]; - } - }); } else { currentArgs[key] = nextArgs[key]; } + Object.keys(nextArgs).forEach((key2) => { + if (currentArgs[key][key2] === undefined) { + currentArgs[key2] = nextArgs[key2]; + } + }); }); } /** From fb1c507dcd9f65b240c526ab0807cb169b16e292 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sun, 5 Mar 2023 21:54:03 +0400 Subject: [PATCH 025/112] attribute style dynamic source --- code/renderers/vue3/src/decorateStory.ts | 21 ++- .../vue3/src/docs/sourceDecorator.ts | 167 ++++++++++-------- code/renderers/vue3/src/render.test.ts | 34 +++- code/renderers/vue3/src/render.ts | 22 +-- code/renderers/vue3/src/types.ts | 11 +- 5 files changed, 158 insertions(+), 97 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 6892fb1337bf..17c3326abb90 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -27,16 +27,15 @@ function prepare( } if (innerStory) { - // console.log('innerStory', innerStory); return { // Normalize so we can always spread an object ...normalizeFunctionalComponent(story), components: { ...(story.components || {}), story: innerStory }, - renderTracked() { - // console.log('innerStory renderTracked', event); + renderTracked(event) { + console.log('innerStory renderTracked', event); // this works only in dev mode }, - renderTriggered() { - // console.log('innerStory renderTriggered', event); + renderTriggered(event) { + console.log('innerStory renderTriggered', event); }, }; } @@ -46,7 +45,7 @@ function prepare( return h(story); }, renderTracked(event) { - console.log('story renderTracked', event); + console.log('story renderTracked', event); // this works only in dev mode }, renderTriggered(event) { console.log('story renderTriggered', event); @@ -87,16 +86,20 @@ export function decorateStory( (context) => prepare(storyFn(context)) as LegacyStoryFn ); } - +/** + * update the context with the update object from the decorator in reactive way + * @param context + * @param update + */ function updateReactiveContext( context: StoryContext, update: StoryContextUpdate> | undefined ) { - context.args = reactive(context.args); + context.args = reactive(context.args); // get reference to reactiveArgs or create a new one; in case was destructured by decorator if (update) { const { args, argTypes } = update; if (args && !argTypes) { - const deepCopy = JSON.parse(JSON.stringify(args)); + const deepCopy = JSON.parse(JSON.stringify(args)); // avoid reference to args Object.keys(context.args).forEach((key) => { delete context.args[key]; }); diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index fe91412f8818..97c7a71f975a 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -14,22 +14,11 @@ import type { TemplateChildNode, } from '@vue/compiler-core'; import { baseParse } from '@vue/compiler-core'; -import type { Component, VNodeProps } from 'vue'; -import { toDisplayString, h } from 'vue'; +import { h, toDisplayString } from 'vue'; import { camelCase, kebabCase } from 'lodash'; -type StoryVueComponent = Component & { - render: any; - props: VNodeProps; - slots: any; - tag?: string; - name?: string; - __name?: string; - __file?: string; - __docs?: any; - __docsGen?: any; - __docsExtracted?: any; -}; +import type { VueStoryComponent } from '../types'; + /** * Check if the sourcecode should be generated. * @@ -49,15 +38,16 @@ const skipSourceRender = (context: StoryContext) => { return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE; }; +const omitEvent = (args: Args): Args => + Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); + const displayObject = (obj: Args) => { - const a = Object.keys(obj).map((key) => `${key}:"${obj[key]}"`); + const a = Object.keys(obj).map((key) => `${key}:'${obj[key]}'`); return `{${a.join(',')}}`; }; const htmlEventAttributeToVueEventAttribute = (key: string) => { return /^on[A-Za-z]/.test(key) ? key.replace(/^on/, 'v-on:').toLowerCase() : key; }; -// html event attribute to vue event attribute -// is html event attribute const directiveSource = (key: string, value: unknown) => key.includes('on') @@ -69,6 +59,7 @@ const attributeSource = (key: string, value: unknown) => ['boolean', 'number', 'object'].includes(typeof value) ? `:${key}='${value && typeof value === 'object' ? displayObject(value) : value}'` : directiveSource(key, value); + /** * * @param _args @@ -86,21 +77,76 @@ export function generateAttributesSource( const arg = tempArgs[key]; if (arg.type === 7) { - const { arg: argName } = arg; - const argKey = argName ? argName?.loc.source : undefined; // (argName as any)?.content; - // const argExpValue = exp?.content; + // AttributeNode binding type + const { exp, arg: argName } = arg; + const argKey = argName ? argName?.loc.source : undefined; + const argExpValue = exp?.loc.source ?? (exp as any).content; const propValue = args[camelCase(argKey)]; + const argValue = argKey ? propValue ?? argExpValue : displayObject(omitEvent(args)); + + if (argKey === 'style') { + let style = argValue; + Object.keys(args).forEach((akey) => { + const regex = new RegExp(`(\\w+)\\.${akey}`, 'g'); + style = style.replace(regex, args[akey]); + }); + return `:style="${style}"`; + } - const argValue = argKey ? propValue : toDisplayString(args); return argKey ? attributeSource(argKey, argValue) - : toDisplayString(tempArgs[key].loc.source); // tempArgs[key].loc.source.replace(`"${argExpValue}"`, `'${argValue}'`); + : tempArgs[key].loc.source.replace(`"${argExpValue}"`, `"${argValue}"`) ?? + toDisplayString(argExpValue); } + return tempArgs[key].loc.source; }) .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 slotsProps + */ +function mapSlots(slotsProps: Args): TextNode[] { + return Object.keys(slotsProps).map((key) => { + const slot = slotsProps[key]; + let slotContent = ''; + if (typeof slot === 'function') slotContent = ``; + slotContent = ``; + if (key === 'default') { + slotContent = JSON.stringify(slot); + } + 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) +} /** * * @param args generate script setup from args @@ -135,14 +181,10 @@ function getComponentsFromRenderFn( } function getComponentsFromTemplate(template: string): TemplateChildNode[] { - try { - const ast = baseParse(template); - const components = ast?.children; - if (!components) return []; - return components; - } catch (e) { - return []; - } + const ast = baseParse(template); + const components = ast?.children; + if (!components) return []; + return components; } /** @@ -155,7 +197,7 @@ function getComponentsFromTemplate(template: string): TemplateChildNode[] { */ export function generateSource( - componentOrNodes: (StoryVueComponent | TemplateChildNode)[] | TemplateChildNode, + componentOrNodes: (VueStoryComponent | TemplateChildNode)[] | TemplateChildNode, args: Args, argTypes: ArgTypes, byRef = false @@ -165,14 +207,17 @@ export function generateSource( const isInterpolationNode = (node: any) => node && node.type === 5; const isTextNode = (node: any) => node && node.type === 2; - const generateComponentSource = (componentOrNode: StoryVueComponent | TemplateChildNode) => { + const generateComponentSource = (componentOrNode: VueStoryComponent | TemplateChildNode) => { if (isElementNode(componentOrNode)) { const { tag: name, props: attributes, children } = componentOrNode as ElementNode; const childSources: string = children .map((child: TemplateChildNode) => generateComponentSource(child)) .join(''); const props = generateAttributesSource(attributes, args, argTypes, byRef); - return `<${name} ${props}>${childSources}`; + + return childSources === '' + ? `<${name} ${props} />` + : `<${name} ${props}>${childSources}`; } if (isInterpolationNode(componentOrNode) || isTextNode(componentOrNode)) { @@ -183,7 +228,7 @@ export function generateSource( } if (isComponent(componentOrNode)) { - const concreteComponent = componentOrNode as StoryVueComponent; + const concreteComponent = componentOrNode as VueStoryComponent; const vnode = h(componentOrNode, args); const { props } = vnode; const { slots } = getDocgenSection(concreteComponent, 'slots') || {}; @@ -203,7 +248,9 @@ export function generateSource( .join(''); const name = concreteComponent.tag || concreteComponent.name || concreteComponent.__name; const propsSource = generateAttributesSource(attributes, args, argTypes, byRef); - return `<${name} ${propsSource}>${childSources}`; + return childSources.trim() === '' + ? `<${name} ${propsSource}/>` + : `<${name} ${propsSource}>${childSources}`; } return null; @@ -216,20 +263,6 @@ export function generateSource( return source || null; } -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) - ); -} /** * source decorator. * @param storyFn Fn @@ -239,9 +272,7 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = const channel = addons.getChannel(); const skip = skipSourceRender(context); const story = storyFn(); - let source: string; - useEffect(() => { if (!skip && source) { const { id, args } = context; @@ -270,29 +301,19 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = return story; }; -function mapSlots(slotsProps: Args): TextNode[] { - return Object.keys(slotsProps).map((key) => { - const slot = slotsProps[key]; - let slotContent = ''; - if (typeof slot === 'function') slotContent = ``; - slotContent = ``; - if (key === 'default') { - slotContent = JSON.stringify(slot); - } - - 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) +export function getTemplateSource(context: StoryContext) { + const channel = addons.getChannel(); + const components = getComponentsFromRenderFn(context?.originalStoryFn, context); + const storyComponent = components.length ? components : (context.component as TemplateChildNode); + const generatedTemplate = generateSource(storyComponent, context.args, context.argTypes); + if (generatedTemplate) { + const source = ``; + const { id, args } = context; + channel.emit(SNIPPET_RENDERED, { id, args, source, format: 'vue' }); + return source; + } + return null; } - // export local function for testing purpose export { generateScriptSetup, diff --git a/code/renderers/vue3/src/render.test.ts b/code/renderers/vue3/src/render.test.ts index d2977f8d6248..5c39ef32a6b2 100644 --- a/code/renderers/vue3/src/render.test.ts +++ b/code/renderers/vue3/src/render.test.ts @@ -12,7 +12,7 @@ describe('Render Story', () => { const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; updateArgs(reactiveArgs, newArgs); expectTypeOf(reactiveArgs).toEqualTypeOf<{ argFoo: string; argBar: string }>(); - expect(reactiveArgs).toEqual(newArgs); + expect(reactiveArgs).toEqual({ argFoo: 'foo2', argBar: 'bar2' }); }); test('update reactive Args component inherit objectArg updateArgs()', () => { @@ -36,7 +36,7 @@ describe('Render Story', () => { expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo2' }, argBar: 'bar2' }); }); - test('update reactive Args component 2 args updateArgs()', () => { + test('update reactive Args component 2 object args -> updateArgs()', () => { const reactiveArgs = reactive({ objectArg: { argFoo: 'foo' }, objectArg2: { argBar: 'bar' }, @@ -53,8 +53,34 @@ describe('Render Story', () => { expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo2' }, objectArg2: { argBar: 'bar2' }, - argFoo: 'foo2', - argBar: 'bar2', }); }); + + test('update reactive Args component object with object -> updateArgs()', () => { + const reactiveArgs = reactive({ + objectArg: { argFoo: 'foo' }, + }); // get reference to reactiveArgs or create a new one; + expectTypeOf(reactiveArgs).toMatchTypeOf>(); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ + objectArg: { argFoo: string }; + }>(); + + const newArgs = { objectArg: { argFoo: 'bar' } }; + updateArgs(reactiveArgs, newArgs); + + expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'bar' } }); + }); + + test('update reactive Args component no arg with all args -> updateArgs()', () => { + const reactiveArgs = reactive({ objectArg: { argFoo: 'foo' } }); // get reference to reactiveArgs or create a new one; + expectTypeOf(reactiveArgs).toMatchTypeOf>(); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ + objectArg: { argFoo: string }; + }>(); + + const newArgs = { objectArg: { argFoo: 'bar' } }; + updateArgs(reactiveArgs, newArgs); + + expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'bar' } }); + }); }); diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 55a9636e5dab..5877135a98bb 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,9 +1,10 @@ /* eslint-disable no-param-reassign */ import { createApp, h, isReactive, reactive, watch } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; -import type { Globals, Args, StoryContext } from '@storybook/csf'; +import type { Globals, Args, StoryContext, Renderer } from '@storybook/csf'; import { global as globalThis } from '@storybook/global'; import type { StoryFnVueReturnType, VueRenderer } from './types'; +import { getTemplateSource as generateTemplateSource } from './docs/sourceDecorator'; export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; @@ -44,6 +45,7 @@ export function renderToCanvas( if (reactiveState) reactiveState.globals = storyContext.globals; updateArgs(existingApp.reactiveArgs, storyContext.args); + generateTemplateSource(storyContext as StoryContext); return () => { teardown(existingApp.vueApp, canvasElement); }; @@ -109,26 +111,26 @@ function getSlots(props: Args, context: StoryContext) { */ export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string[]) { const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs); - + const notMappedArgs = { ...nextArgs }; Object.keys(currentArgs).forEach((key) => { const componentArg = currentArgs[key]; // if the arg is an object, we need to update the object if (typeof componentArg === 'object') { - Object.keys(componentArg).forEach((key2) => { - if (nextArgs[key2] && (argNames?.includes(key2) || !argNames)) { - currentArgs[key][key2] = nextArgs[key2]; + Object.keys(componentArg).forEach((aKey) => { + if (nextArgs[aKey] && (argNames?.includes(aKey) || !argNames)) { + currentArgs[key][aKey] = nextArgs[aKey]; + delete notMappedArgs[aKey]; } }); } else { currentArgs[key] = nextArgs[key]; } - Object.keys(nextArgs).forEach((key2) => { - if (currentArgs[key][key2] === undefined) { - currentArgs[key2] = nextArgs[key2]; - } - }); + }); + Object.keys(notMappedArgs).forEach((key) => { + currentArgs[key] = notMappedArgs[key]; }); } + /** * unmount the vue app * @param storybookApp diff --git a/code/renderers/vue3/src/types.ts b/code/renderers/vue3/src/types.ts index 15809f9094e2..f5b4576cedf7 100644 --- a/code/renderers/vue3/src/types.ts +++ b/code/renderers/vue3/src/types.ts @@ -1,5 +1,5 @@ import type { StoryContext as StoryContextBase, WebRenderer } from '@storybook/types'; -import type { ConcreteComponent } from 'vue'; +import type { ConcreteComponent, Slots, VNodeProps } from 'vue'; export type { RenderContext } from '@storybook/types'; @@ -12,6 +12,15 @@ export type StoryFnVueReturnType = ConcreteComponent; export type StoryContext = StoryContextBase; +export type VueStoryComponent = ConcreteComponent & { + render: (h: any) => any; + props: VNodeProps; + slots: Slots; + tag?: string; + name?: string; + __name?: string; +}; + /** * @deprecated Use `VueRenderer` instead. */ From c0ffbe0aea5cb63f31eb5d053c0ee86ce164d272 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sun, 5 Mar 2023 23:54:13 +0400 Subject: [PATCH 026/112] string double quote isse --- .../vue3/src/docs/sourceDecorator.test.ts | 232 +++++++++--------- .../vue3/src/docs/sourceDecorator.ts | 2 +- 2 files changed, 117 insertions(+), 117 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index 78616e4d06a2..563dd5258056 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -272,128 +272,128 @@ describe('Vue3: sourceDecorator->generateAttributesSource()', () => { ).toMatchInlineSnapshot(`camel-case-string-arg='foo'`); }); - test('camelCase boolean, string, and number Args', () => { - expect( - generateAttributesSource( - mapAttributesAndDirectives({ - camelCaseBooleanArg: true, - camelCaseStringArg: 'foo', - cameCaseNumberArg: 2023, - }), - { - camelCaseBooleanArg: true, - camelCaseStringArg: 'foo', - cameCaseNumberArg: 2023, - }, - [] as ArgsType - ) - ).toMatchInlineSnapshot( - `:camel-case-boolean-arg='true' camel-case-string-arg='foo' :came-case-number-arg='2023'` - ); - }); -}); + // test('camelCase boolean, string, and number Args', () => { + // expect( + // generateAttributesSource( + // mapAttributesAndDirectives({ + // camelCaseBooleanArg: true, + // camelCaseStringArg: 'foo', + // cameCaseNumberArg: 2023, + // }), + // { + // camelCaseBooleanArg: true, + // camelCaseStringArg: 'foo', + // cameCaseNumberArg: 2023, + // }, + // [] as ArgsType + // ) + // ).toMatchInlineSnapshot( + // `:camel-case-boolean-arg='true' camel-case-string-arg='foo' :came-case-number-arg='2023'` + // ); + // }); + // }); -describe('Vue3: generateSource() snippet', () => { - test('template component camelCase string Arg', () => { - expect( - generateForArgs( - { - camelCaseStringArg: 'foo', - }, - [] as ArgsType, - `` - ) - ).toMatchInlineSnapshot(``); - }); + // describe('Vue3: generateSource() snippet', () => { + // test('template component camelCase string Arg', () => { + // expect( + // generateForArgs( + // { + // camelCaseStringArg: 'foo', + // }, + // [] as ArgsType, + // `` + // ) + // ).toMatchInlineSnapshot(``); + // }); - test('template component camelCase bool Arg', () => { - expect( - generateForArgs( - { - camelCaseBooleanArg: true, - }, - [] as ArgsType, - `` - ) - ).toMatchInlineSnapshot(``); - }); + // test('template component camelCase bool Arg', () => { + // expect( + // generateForArgs( + // { + // camelCaseBooleanArg: true, + // }, + // [] as ArgsType, + // `` + // ) + // ).toMatchInlineSnapshot(``); + // }); - test('template component camelCase bool, string Arg', () => { - expect( - generateForArgs( - { - camelCaseBooleanArg: true, - camelCaseStringArg: 'foo', - }, - [] as ArgsType, - `` - ) - ).toMatchInlineSnapshot( - `` - ); - }); + // test('template component camelCase bool, string Arg', () => { + // expect( + // generateForArgs( + // { + // camelCaseBooleanArg: true, + // camelCaseStringArg: 'foo', + // }, + // [] as ArgsType, + // `` + // ) + // ).toMatchInlineSnapshot( + // `` + // ); + // }); - test('template component camelCase object Arg', () => { - expect( - generateForArgs( - { - camelCaseObjectArg: { foo: 'bar' }, - }, - [] as ArgsType, - `` - ) - ).toMatchInlineSnapshot(``); - }); + // test('template component camelCase object Arg', () => { + // expect( + // generateForArgs( + // { + // camelCaseObjectArg: { foo: 'bar' }, + // }, + // [] as ArgsType, + // `` + // ) + // ).toMatchInlineSnapshot(``); + // }); - test('template component camelCase object Arg and Slot', () => { - expect( - generateForArgs( - { - camelCaseObjectArg: { foo: 'bar' }, - }, - [] as ArgsType, - ` SLOT ` - ) - ).toMatchInlineSnapshot(` SLOT `); - }); + // test('template component camelCase object Arg and Slot', () => { + // expect( + // generateForArgs( + // { + // camelCaseObjectArg: { foo: 'bar' }, + // }, + // [] as ArgsType, + // ` SLOT ` + // ) + // ).toMatchInlineSnapshot(` SLOT `); + // }); - test('template component camelCase object Arg and dynamic Slot content', () => { - expect( - generateForArgs( - { - camelCaseObjectArg: { foo: 'bar' }, - camelCaseStringSlotArg: 'foo', - }, - [] as ArgsType, - ` SLOT {{args.camelCaseStringSlotArg}}` - ) - ).toMatchInlineSnapshot( - ` SLOT foo` - ); - }); -}); + // test('template component camelCase object Arg and dynamic Slot content', () => { + // expect( + // generateForArgs( + // { + // camelCaseObjectArg: { foo: 'bar' }, + // camelCaseStringSlotArg: 'foo', + // }, + // [] as ArgsType, + // ` SLOT {{args.camelCaseStringSlotArg}}` + // ) + // ).toMatchInlineSnapshot( + // ` SLOT foo` + // ); + // }); + // }); -describe('Vue3: sourceDecorator->attributeSoure()', () => { - test('camelCase boolean Arg', () => { - expect(attributeSource('stringArg', 'foo')).toMatchInlineSnapshot(`stringArg='foo'`); - }); + // describe('Vue3: sourceDecorator->attributeSoure()', () => { + // test('camelCase boolean Arg', () => { + // expect(attributeSource('stringArg', 'foo')).toMatchInlineSnapshot(`stringArg='foo'`); + // }); - test('html event attribute should convert to vue event directive', () => { - expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); - expect(attributeSource('onclick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); - }); - test('normal html attribute should not convert to vue event directive', () => { - expect(attributeSource('on-click', () => {})).toMatchInlineSnapshot(`on-click='()=>({})'`); - }); - test('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('html event attribute should convert to vue event directive', () => { + // expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); + // expect(attributeSource('onclick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); + // }); + // test('normal html attribute should not convert to vue event directive', () => { + // expect(attributeSource('on-click', () => {})).toMatchInlineSnapshot(`on-click='()=>({})'`); + // }); + // test('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`); + // }); }); diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 97c7a71f975a..a963a46389e9 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -95,7 +95,7 @@ export function generateAttributesSource( return argKey ? attributeSource(argKey, argValue) - : tempArgs[key].loc.source.replace(`"${argExpValue}"`, `"${argValue}"`) ?? + : tempArgs[key].loc.source.replace(`${argExpValue}`, `${argValue}`) ?? toDisplayString(argExpValue); } From 7183154bd59d34e892d9b05131bae8e3a6d2d195 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 6 Mar 2023 13:38:18 +0400 Subject: [PATCH 027/112] v-bind , style display as string --- .../vue3/src/docs/sourceDecorator.test.ts | 252 +++++++++--------- .../vue3/src/docs/sourceDecorator.ts | 44 +-- 2 files changed, 149 insertions(+), 147 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index 563dd5258056..75e3b1e96745 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -49,7 +49,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: :camel-case-boolean-arg='true', + source: :camel-case-boolean-arg="true", }, modifiers: Array [ , @@ -77,7 +77,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: camel-case-string-arg='foo', + source: camel-case-string-arg="foo", }, modifiers: Array [ , @@ -105,7 +105,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: :booleanarg='true', + source: :booleanarg="true", }, modifiers: Array [ , @@ -133,7 +133,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: stringarg='bar', + source: stringarg="bar", }, modifiers: Array [ , @@ -161,7 +161,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: :numberarg='2023', + source: :numberarg="2023", }, modifiers: Array [ , @@ -195,7 +195,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: :camel-case-boolean-arg='true', + source: :camel-case-boolean-arg="true", }, modifiers: Array [ , @@ -217,7 +217,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: camel-case-string-arg='foo', + source: camel-case-string-arg="foo", }, modifiers: Array [ , @@ -239,7 +239,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: :came-case-number-arg='2023', + source: :came-case-number-arg="2023", }, modifiers: Array [ , @@ -260,7 +260,7 @@ describe('Vue3: sourceDecorator->generateAttributesSource()', () => { { camelCaseBooleanArg: true }, [{ camelCaseBooleanArg: { type: 'boolean' } }] as ArgsType ) - ).toMatchInlineSnapshot(`:camel-case-boolean-arg='true'`); + ).toMatchInlineSnapshot(`:camel-case-boolean-arg="true"`); }); test('camelCase string Arg', () => { expect( @@ -269,131 +269,131 @@ describe('Vue3: sourceDecorator->generateAttributesSource()', () => { { camelCaseStringArg: 'foo' }, [{ camelCaseStringArg: { type: 'string' } }] as ArgsType ) - ).toMatchInlineSnapshot(`camel-case-string-arg='foo'`); + ).toMatchInlineSnapshot(`camel-case-string-arg="foo"`); }); - // test('camelCase boolean, string, and number Args', () => { - // expect( - // generateAttributesSource( - // mapAttributesAndDirectives({ - // camelCaseBooleanArg: true, - // camelCaseStringArg: 'foo', - // cameCaseNumberArg: 2023, - // }), - // { - // camelCaseBooleanArg: true, - // camelCaseStringArg: 'foo', - // cameCaseNumberArg: 2023, - // }, - // [] as ArgsType - // ) - // ).toMatchInlineSnapshot( - // `:camel-case-boolean-arg='true' camel-case-string-arg='foo' :came-case-number-arg='2023'` - // ); - // }); - // }); + test('camelCase boolean, string, and number Args', () => { + expect( + generateAttributesSource( + mapAttributesAndDirectives({ + camelCaseBooleanArg: true, + camelCaseStringArg: 'foo', + cameCaseNumberArg: 2023, + }), + { + camelCaseBooleanArg: true, + camelCaseStringArg: 'foo', + cameCaseNumberArg: 2023, + }, + [] as ArgsType + ) + ).toMatchInlineSnapshot( + `:camel-case-boolean-arg="true" camel-case-string-arg="foo" :came-case-number-arg="2023"` + ); + }); +}); - // describe('Vue3: generateSource() snippet', () => { - // test('template component camelCase string Arg', () => { - // expect( - // generateForArgs( - // { - // camelCaseStringArg: 'foo', - // }, - // [] as ArgsType, - // `` - // ) - // ).toMatchInlineSnapshot(``); - // }); +describe('Vue3: generateSource() snippet', () => { + test('template component camelCase string Arg', () => { + expect( + generateForArgs( + { + camelCaseStringArg: 'foo', + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot(``); + }); - // test('template component camelCase bool Arg', () => { - // expect( - // generateForArgs( - // { - // camelCaseBooleanArg: true, - // }, - // [] as ArgsType, - // `` - // ) - // ).toMatchInlineSnapshot(``); - // }); + test('template component camelCase bool Arg', () => { + expect( + generateForArgs( + { + camelCaseBooleanArg: true, + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot(``); + }); - // test('template component camelCase bool, string Arg', () => { - // expect( - // generateForArgs( - // { - // camelCaseBooleanArg: true, - // camelCaseStringArg: 'foo', - // }, - // [] as ArgsType, - // `` - // ) - // ).toMatchInlineSnapshot( - // `` - // ); - // }); + test('template component camelCase bool, string Arg', () => { + expect( + generateForArgs( + { + camelCaseBooleanArg: true, + camelCaseStringArg: 'foo', + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot( + `` + ); + }); - // test('template component camelCase object Arg', () => { - // expect( - // generateForArgs( - // { - // camelCaseObjectArg: { foo: 'bar' }, - // }, - // [] as ArgsType, - // `` - // ) - // ).toMatchInlineSnapshot(``); - // }); + test('template component camelCase object Arg', () => { + expect( + generateForArgs( + { + camelCaseObjectArg: { foo: 'bar' }, + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot(``); + }); - // test('template component camelCase object Arg and Slot', () => { - // expect( - // generateForArgs( - // { - // camelCaseObjectArg: { foo: 'bar' }, - // }, - // [] as ArgsType, - // ` SLOT ` - // ) - // ).toMatchInlineSnapshot(` SLOT `); - // }); + test('template component camelCase object Arg and Slot', () => { + expect( + generateForArgs( + { + camelCaseObjectArg: { foo: 'bar' }, + }, + [] as ArgsType, + ` SLOT ` + ) + ).toMatchInlineSnapshot(` SLOT `); + }); - // test('template component camelCase object Arg and dynamic Slot content', () => { - // expect( - // generateForArgs( - // { - // camelCaseObjectArg: { foo: 'bar' }, - // camelCaseStringSlotArg: 'foo', - // }, - // [] as ArgsType, - // ` SLOT {{args.camelCaseStringSlotArg}}` - // ) - // ).toMatchInlineSnapshot( - // ` SLOT foo` - // ); - // }); - // }); + test('template component camelCase object Arg and dynamic Slot content', () => { + expect( + generateForArgs( + { + camelCaseObjectArg: { foo: 'bar' }, + camelCaseStringSlotArg: 'foo', + }, + [] as ArgsType, + ` SLOT {{args.camelCaseStringSlotArg}}` + ) + ).toMatchInlineSnapshot( + ` SLOT foo` + ); + }); +}); - // describe('Vue3: sourceDecorator->attributeSoure()', () => { - // test('camelCase boolean Arg', () => { - // expect(attributeSource('stringArg', 'foo')).toMatchInlineSnapshot(`stringArg='foo'`); - // }); +describe('Vue3: sourceDecorator->attributeSoure()', () => { + test('camelCase boolean Arg', () => { + expect(attributeSource('stringArg', 'foo')).toMatchInlineSnapshot(`stringArg="foo"`); + }); - // test('html event attribute should convert to vue event directive', () => { - // expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); - // expect(attributeSource('onclick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); - // }); - // test('normal html attribute should not convert to vue event directive', () => { - // expect(attributeSource('on-click', () => {})).toMatchInlineSnapshot(`on-click='()=>({})'`); - // }); - // test('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('html event attribute should convert to vue event directive', () => { + expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); + expect(attributeSource('onclick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); + }); + test('normal html attribute should not convert to vue event directive', () => { + expect(attributeSource('on-click', () => {})).toMatchInlineSnapshot(`on-click='()=>({})'`); + }); + test('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`); + }); }); diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index a963a46389e9..86b5783bc945 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -41,9 +41,12 @@ const skipSourceRender = (context: StoryContext) => { const omitEvent = (args: Args): Args => Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); -const displayObject = (obj: Args) => { - const a = Object.keys(obj).map((key) => `${key}:'${obj[key]}'`); - return `{${a.join(',')}}`; +const displayObject = (obj: any) => { + if (typeof obj === 'object' && obj !== null && (obj as { [key: string]: unknown })) { + const a = Object.keys(obj).map((key) => `${key}:'${obj[key]}'`); + return `{${a.join(',')}}`; + } + return obj; }; const htmlEventAttributeToVueEventAttribute = (key: string) => { return /^on[A-Za-z]/.test(key) ? key.replace(/^on/, 'v-on:').toLowerCase() : key; @@ -52,12 +55,12 @@ const htmlEventAttributeToVueEventAttribute = (key: string) => { const directiveSource = (key: string, value: unknown) => key.includes('on') ? `${htmlEventAttributeToVueEventAttribute(key)}='()=>({})'` - : `${key}='${value}'`; + : `${key}="${value}"`; const attributeSource = (key: string, value: unknown) => // convert html event key to vue event key ['boolean', 'number', 'object'].includes(typeof value) - ? `:${key}='${value && typeof value === 'object' ? displayObject(value) : value}'` + ? `:${key}="${displayObject(value)}"` : directiveSource(key, value); /** @@ -82,21 +85,11 @@ export function generateAttributesSource( const argKey = argName ? argName?.loc.source : undefined; const argExpValue = exp?.loc.source ?? (exp as any).content; const propValue = args[camelCase(argKey)]; - const argValue = argKey ? propValue ?? argExpValue : displayObject(omitEvent(args)); - - if (argKey === 'style') { - let style = argValue; - Object.keys(args).forEach((akey) => { - const regex = new RegExp(`(\\w+)\\.${akey}`, 'g'); - style = style.replace(regex, args[akey]); - }); - return `:style="${style}"`; - } - - return argKey - ? attributeSource(argKey, argValue) - : tempArgs[key].loc.source.replace(`${argExpValue}`, `${argValue}`) ?? - toDisplayString(argExpValue); + const argValue = argKey + ? propValue ?? evalExp(argExpValue, args) + : displayObject(omitEvent(args)); + + return argKey ? attributeSource(argKey, argValue) : `v-bind="${argValue}"`; } return tempArgs[key].loc.source; @@ -293,7 +286,7 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = const withScript = context?.parameters?.docs?.source?.withScriptSetup || false; const generatedScript = withScript ? generateScriptSetup(args, argTypes, components) : ''; const generatedTemplate = generateSource(storyComponent, args, argTypes, withScript); - + console.log('generatedTemplate -------\r\n\n\n', generatedTemplate, '\n\n'); if (generatedTemplate) { source = `${generatedScript}\n `; } @@ -323,3 +316,12 @@ export { attributeSource, htmlEventAttributeToVueEventAttribute, }; + +function evalExp(argExpValue: any, args: Args): any { + let evalVal = argExpValue; + Object.keys(args).forEach((akey) => { + const regex = new RegExp(`(\\w+)\\.${akey}`, 'g'); + evalVal = evalVal.replace(regex, args[akey]); + }); + return evalVal; +} From db908d3a86287c877c5240306c4a907800a131a4 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 6 Mar 2023 13:52:48 +0400 Subject: [PATCH 028/112] style should be dynamic in source --- code/renderers/vue3/src/docs/sourceDecorator.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 86b5783bc945..a77e59d9f466 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -57,9 +57,9 @@ const directiveSource = (key: string, value: unknown) => ? `${htmlEventAttributeToVueEventAttribute(key)}='()=>({})'` : `${key}="${value}"`; -const attributeSource = (key: string, value: unknown) => +const attributeSource = (key: string, value: unknown, dynamic?: boolean) => // convert html event key to vue event key - ['boolean', 'number', 'object'].includes(typeof value) + ['boolean', 'number', 'object'].includes(typeof value) || dynamic ? `:${key}="${displayObject(value)}"` : directiveSource(key, value); @@ -89,7 +89,7 @@ export function generateAttributesSource( ? propValue ?? evalExp(argExpValue, args) : displayObject(omitEvent(args)); - return argKey ? attributeSource(argKey, argValue) : `v-bind="${argValue}"`; + return argKey ? attributeSource(argKey, argValue, true) : `v-bind="${argValue}"`; } return tempArgs[key].loc.source; @@ -286,7 +286,7 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = const withScript = context?.parameters?.docs?.source?.withScriptSetup || false; const generatedScript = withScript ? generateScriptSetup(args, argTypes, components) : ''; const generatedTemplate = generateSource(storyComponent, args, argTypes, withScript); - console.log('generatedTemplate -------\r\n\n\n', generatedTemplate, '\n\n'); + if (generatedTemplate) { source = `${generatedScript}\n `; } From 6d4d3aa46a4c21a18e924b0235abdec5fa41478a Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 6 Mar 2023 14:53:41 +0400 Subject: [PATCH 029/112] cleanup after testing. --- code/renderers/vue3/src/docs/sourceDecorator.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index a77e59d9f466..e85ec53b1d98 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -14,9 +14,8 @@ import type { TemplateChildNode, } from '@vue/compiler-core'; import { baseParse } from '@vue/compiler-core'; -import { h, toDisplayString } from 'vue'; +import { h } from 'vue'; import { camelCase, kebabCase } from 'lodash'; - import type { VueStoryComponent } from '../types'; /** @@ -41,11 +40,13 @@ const skipSourceRender = (context: StoryContext) => { const omitEvent = (args: Args): Args => Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); -const displayObject = (obj: any) => { - if (typeof obj === 'object' && obj !== null && (obj as { [key: string]: unknown })) { - const a = Object.keys(obj).map((key) => `${key}:'${obj[key]}'`); - return `{${a.join(',')}}`; +const displayObject = (obj: any): string => { + if (typeof obj === 'object') { + return `{${Object.keys(obj) + .map((key) => `${key}:${displayObject(obj[key])}`) + .join(',')}}`; } + if (typeof obj === 'string') return `'${obj}'`; return obj; }; const htmlEventAttributeToVueEventAttribute = (key: string) => { @@ -59,7 +60,8 @@ const directiveSource = (key: string, value: unknown) => const attributeSource = (key: string, value: unknown, dynamic?: boolean) => // convert html event key to vue event key - ['boolean', 'number', 'object'].includes(typeof value) || dynamic + ['boolean', 'number', 'object'].includes(typeof value) || // dynamic value + (dynamic && ['style', 'class'].includes(key)) // dynamic style or class ? `:${key}="${displayObject(value)}"` : directiveSource(key, value); From 13699fe6a77e724c1bed71e91631afe12e3d8b89 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 6 Mar 2023 15:16:36 +0400 Subject: [PATCH 030/112] refactor add utils to docs --- .../vue3/src/docs/sourceDecorator.ts | 44 +++---------------- 1 file changed, 7 insertions(+), 37 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index e85ec53b1d98..317e098345d5 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -16,6 +16,13 @@ import type { import { baseParse } from '@vue/compiler-core'; import { h } from 'vue'; import { camelCase, kebabCase } from 'lodash'; +import { + attributeSource, + htmlEventAttributeToVueEventAttribute, + omitEvent, + displayObject, + evalExp, +} from './utils'; import type { VueStoryComponent } from '../types'; /** @@ -37,34 +44,6 @@ const skipSourceRender = (context: StoryContext) => { return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE; }; -const omitEvent = (args: Args): Args => - Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); - -const displayObject = (obj: any): string => { - if (typeof obj === 'object') { - return `{${Object.keys(obj) - .map((key) => `${key}:${displayObject(obj[key])}`) - .join(',')}}`; - } - if (typeof obj === 'string') return `'${obj}'`; - return obj; -}; -const htmlEventAttributeToVueEventAttribute = (key: string) => { - return /^on[A-Za-z]/.test(key) ? key.replace(/^on/, 'v-on:').toLowerCase() : key; -}; - -const directiveSource = (key: string, value: unknown) => - key.includes('on') - ? `${htmlEventAttributeToVueEventAttribute(key)}='()=>({})'` - : `${key}="${value}"`; - -const attributeSource = (key: string, value: unknown, dynamic?: boolean) => - // convert html event key to vue event key - ['boolean', 'number', 'object'].includes(typeof value) || // dynamic value - (dynamic && ['style', 'class'].includes(key)) // dynamic style or class - ? `:${key}="${displayObject(value)}"` - : directiveSource(key, value); - /** * * @param _args @@ -318,12 +297,3 @@ export { attributeSource, htmlEventAttributeToVueEventAttribute, }; - -function evalExp(argExpValue: any, args: Args): any { - let evalVal = argExpValue; - Object.keys(args).forEach((akey) => { - const regex = new RegExp(`(\\w+)\\.${akey}`, 'g'); - evalVal = evalVal.replace(regex, args[akey]); - }); - return evalVal; -} From 311a69e3e2f3b15cf80bf1dd0d5a3ba91e34f97c Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 6 Mar 2023 15:18:13 +0400 Subject: [PATCH 031/112] add utils --- code/renderers/vue3/src/docs/utils.ts | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 code/renderers/vue3/src/docs/utils.ts diff --git a/code/renderers/vue3/src/docs/utils.ts b/code/renderers/vue3/src/docs/utils.ts new file mode 100644 index 000000000000..05d72791e015 --- /dev/null +++ b/code/renderers/vue3/src/docs/utils.ts @@ -0,0 +1,47 @@ +import type { Args } from '@storybook/types'; + +const omitEvent = (args: Args): Args => + Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); + +const displayObject = (obj: any): string | boolean | number => { + if (typeof obj === 'object') { + return `{${Object.keys(obj) + .map((key) => `${key}:${displayObject(obj[key])}`) + .join(',')}}`; + } + if (typeof obj === 'string') return `'${obj}'`; + return obj; +}; +const htmlEventAttributeToVueEventAttribute = (key: string) => { + return /^on[A-Za-z]/.test(key) ? key.replace(/^on/, 'v-on:').toLowerCase() : key; +}; + +const directiveSource = (key: string, value: unknown) => + key.includes('on') + ? `${htmlEventAttributeToVueEventAttribute(key)}='()=>({})'` + : `${key}="${value}"`; + +const attributeSource = (key: string, value: unknown, dynamic?: boolean) => + // convert html event key to vue event key + ['boolean', 'number', 'object'].includes(typeof value) || // dynamic value + (dynamic && ['style', 'class'].includes(key)) // dynamic style or class + ? `:${key}="${displayObject(value)}"` + : directiveSource(key, value); + +const evalExp = (argExpValue: any, args: Args): any => { + let evalVal = argExpValue; + Object.keys(args).forEach((akey) => { + const regex = new RegExp(`(\\w+)\\.${akey}`, 'g'); + evalVal = evalVal.replace(regex, args[akey]); + }); + return evalVal; +}; + +export { + omitEvent, + displayObject, + htmlEventAttributeToVueEventAttribute, + directiveSource, + attributeSource, + evalExp, +}; From d38c8ac7b9e30c8fffb408553038481462c5d0f7 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Tue, 7 Mar 2023 05:09:41 +0400 Subject: [PATCH 032/112] use vue watch() instead of useEffect() --- .../vue3/src/docs/sourceDecorator.test.ts | 4 +- .../vue3/src/docs/sourceDecorator.ts | 68 +++++++------------ code/renderers/vue3/src/render.ts | 5 +- 3 files changed, 29 insertions(+), 48 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index 75e3b1e96745..5a239de38120 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -3,7 +3,7 @@ import type { Args } from '@storybook/types'; import type { ArgsType } from 'jest-mock'; import { - generateSource, + generateTemplateSource, getComponentsFromTemplate, mapAttributesAndDirectives, generateAttributesSource, @@ -28,7 +28,7 @@ function generateForArgs( template = '' ) { const components = getComponentsFromTemplate(template); - return generateSource(components, args, generateArgTypes(args, slotProps), true); + return generateTemplateSource(components, args, generateArgTypes(args, slotProps), true); } describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 317e098345d5..e0689ad886aa 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -1,6 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable no-underscore-dangle */ -import { addons, useEffect } from '@storybook/preview-api'; +import { addons } from '@storybook/preview-api'; import type { ArgTypes, Args, StoryContext, Renderer } from '@storybook/types'; import { getDocgenSection, SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; @@ -14,7 +14,7 @@ import type { TemplateChildNode, } from '@vue/compiler-core'; import { baseParse } from '@vue/compiler-core'; -import { h } from 'vue'; +import { h, watch } from 'vue'; import { camelCase, kebabCase } from 'lodash'; import { attributeSource, @@ -138,23 +138,23 @@ function generateScriptSetup(args: Args, argTypes: ArgTypes, components: any[]): return ``; } /** - * get component templates one or more + * get template components one or more * @param renderFn */ -function getComponentsFromRenderFn( +function getTemplateComponents( renderFn: any, context?: StoryContext ): TemplateChildNode[] { try { const { template } = context ? renderFn(context.args, context) : renderFn(); if (!template) return []; - return getComponentsFromTemplate(template); + return getComponents(template); } catch (e) { return []; } } -function getComponentsFromTemplate(template: string): TemplateChildNode[] { +function getComponents(template: string): TemplateChildNode[] { const ast = baseParse(template); const components = ast?.children; if (!components) return []; @@ -170,7 +170,7 @@ function getComponentsFromTemplate(template: string): TemplateChildNode[] { * @param slotProp Prop used to simulate a slot */ -export function generateSource( +export function generateTemplateSource( componentOrNodes: (VueStoryComponent | TemplateChildNode)[] | TemplateChildNode, args: Args, argTypes: ArgTypes, @@ -226,7 +226,6 @@ export function generateSource( ? `<${name} ${propsSource}/>` : `<${name} ${propsSource}>${childSources}`; } - return null; }; @@ -243,46 +242,31 @@ export function generateSource( * @param context StoryContext */ export const sourceDecorator = (storyFn: any, context: StoryContext) => { - const channel = addons.getChannel(); const skip = skipSourceRender(context); const story = storyFn(); - let source: string; - useEffect(() => { - if (!skip && source) { - const { id, args } = context; - channel.emit(SNIPPET_RENDERED, { id, args, source, format: 'vue' }); - } - }); - - if (skip) { - return story; - } - - const { args = {}, component: ctxtComponent, argTypes = {} } = context || {}; - - const components = getComponentsFromRenderFn(context?.originalStoryFn, context); + watch( + () => context.args, + () => { + if (!skip) { + generateSource(context); + } + }, + { immediate: true, deep: true } + ); + return story; +}; +export function generateSource(context: StoryContext) { + const channel = addons.getChannel(); + const { args = {}, component: ctxtComponent, argTypes = {}, id } = context || {}; + const components = getTemplateComponents(context?.originalStoryFn, context); const storyComponent = components.length ? components : (ctxtComponent as TemplateChildNode); const withScript = context?.parameters?.docs?.source?.withScriptSetup || false; const generatedScript = withScript ? generateScriptSetup(args, argTypes, components) : ''; - const generatedTemplate = generateSource(storyComponent, args, argTypes, withScript); - - if (generatedTemplate) { - source = `${generatedScript}\n `; - } - - return story; -}; - -export function getTemplateSource(context: StoryContext) { - const channel = addons.getChannel(); - const components = getComponentsFromRenderFn(context?.originalStoryFn, context); - const storyComponent = components.length ? components : (context.component as TemplateChildNode); - const generatedTemplate = generateSource(storyComponent, context.args, context.argTypes); + const generatedTemplate = generateTemplateSource(storyComponent, context.args, context.argTypes); if (generatedTemplate) { - const source = ``; - const { id, args } = context; + const source = `${generatedScript}\n `; channel.emit(SNIPPET_RENDERED, { id, args, source, format: 'vue' }); return source; } @@ -291,8 +275,8 @@ export function getTemplateSource(context: StoryContext) { // export local function for testing purpose export { generateScriptSetup, - getComponentsFromRenderFn, - getComponentsFromTemplate, + getTemplateComponents as getComponentsFromRenderFn, + getComponents as getComponentsFromTemplate, mapAttributesAndDirectives, attributeSource, htmlEventAttributeToVueEventAttribute, diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 5877135a98bb..e61c2967ab99 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,10 +1,9 @@ /* eslint-disable no-param-reassign */ import { createApp, h, isReactive, reactive, watch } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; -import type { Globals, Args, StoryContext, Renderer } from '@storybook/csf'; +import type { Globals, Args, StoryContext } from '@storybook/csf'; import { global as globalThis } from '@storybook/global'; import type { StoryFnVueReturnType, VueRenderer } from './types'; -import { getTemplateSource as generateTemplateSource } from './docs/sourceDecorator'; export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; @@ -43,9 +42,7 @@ export function renderToCanvas( // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { if (reactiveState) reactiveState.globals = storyContext.globals; - updateArgs(existingApp.reactiveArgs, storyContext.args); - generateTemplateSource(storyContext as StoryContext); return () => { teardown(existingApp.vueApp, canvasElement); }; From 51cf25286b8a8c8a00651720af6f86bd99d4f0ce Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Tue, 7 Mar 2023 05:57:03 +0400 Subject: [PATCH 033/112] just to rerun the ci, it did pass on server y?? --- code/renderers/vue3/src/docs/utils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/renderers/vue3/src/docs/utils.ts b/code/renderers/vue3/src/docs/utils.ts index 05d72791e015..7c5b3f19675b 100644 --- a/code/renderers/vue3/src/docs/utils.ts +++ b/code/renderers/vue3/src/docs/utils.ts @@ -1,5 +1,9 @@ import type { Args } from '@storybook/types'; +/** + * omit event args + * @param args + */ const omitEvent = (args: Args): Args => Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); From 6c806b6409e6c50377f56e4028e5ef812e70ac3a Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 9 Mar 2023 03:00:29 +0400 Subject: [PATCH 034/112] fix slots reactivity without render function temp --- code/renderers/vue3/src/decorateStory.ts | 1 + code/renderers/vue3/src/render.ts | 33 +++++++++++++++--------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 17c3326abb90..164feec6be20 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -42,6 +42,7 @@ function prepare( return { render() { + console.log('story render', story, this.$slots); return h(story); }, renderTracked(event) { diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index e61c2967ab99..a6f45768e62b 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -13,7 +13,8 @@ export const render: ArgsStoryFn = (props, context) => { ); } - return h(Component, props, getSlots(props, context)); + const slots = getSlots(context); + return h(Component, props, slots); }; let setupFunction = (_app: any) => {}; @@ -74,6 +75,12 @@ export function renderToCanvas( reactiveArgs, }); }, + renderTracked(event) { + console.log('vueApp renderTracked', event); // this works only in dev mode + }, + renderTriggered(event) { + console.log('vueApp renderTriggered', event); // this works only in dev mode + }, }); vueApp.config.errorHandler = (e: unknown) => showException(e as Error); setupFunction(vueApp); @@ -85,19 +92,21 @@ export function renderToCanvas( }; } -/** - * get the slots as functions to be rendered - * @param props - * @param context - */ - -function getSlots(props: Args, context: StoryContext) { +function getSlots(context: StoryContext) { const { argTypes } = context; - const slots = Object.entries(props) + const slots = Object.entries(argTypes) .filter(([key, value]) => argTypes[key]?.table?.category === 'slots') - .map(([key, value]) => [key, typeof value === 'function' ? value : () => value]); - - return Object.fromEntries(slots); + .map(([key, value]) => [ + key, + () => { + if (typeof context.args[key] === 'function') return h(context.args[key]); + if (typeof context.args[key] === 'object') return JSON.stringify(context.args[key]); + if (typeof context.args[key] === 'string') return context.args[key]; + return context.args[key]; + }, + ]); + + return reactive(Object.fromEntries(slots)); } /** From a75e222bbaf902bb866ef00d8eb2b770baa46032 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 9 Mar 2023 03:26:24 +0400 Subject: [PATCH 035/112] fix undefined attr + cleanup --- code/renderers/vue3/src/decorateStory.ts | 10 +++++----- code/renderers/vue3/src/docs/utils.ts | 2 +- code/renderers/vue3/src/render.ts | 10 ++++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 164feec6be20..13ed507a822a 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -32,24 +32,24 @@ function prepare( ...normalizeFunctionalComponent(story), components: { ...(story.components || {}), story: innerStory }, renderTracked(event) { - console.log('innerStory renderTracked', event); // this works only in dev mode + // console.log('innerStory renderTracked', event); // this works only in dev mode }, renderTriggered(event) { - console.log('innerStory renderTriggered', event); + // console.log('innerStory renderTriggered', event); }, }; } return { render() { - console.log('story render', story, this.$slots); + // console.log('story render', story, this.$slots); return h(story); }, renderTracked(event) { - console.log('story renderTracked', event); // this works only in dev mode + // console.log('story renderTracked', event); // this works only in dev mode }, renderTriggered(event) { - console.log('story renderTriggered', event); + // console.log('story renderTriggered', event); }, }; } diff --git a/code/renderers/vue3/src/docs/utils.ts b/code/renderers/vue3/src/docs/utils.ts index 7c5b3f19675b..538a235fbeb0 100644 --- a/code/renderers/vue3/src/docs/utils.ts +++ b/code/renderers/vue3/src/docs/utils.ts @@ -8,7 +8,7 @@ const omitEvent = (args: Args): Args => Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); const displayObject = (obj: any): string | boolean | number => { - if (typeof obj === 'object') { + if (obj && typeof obj === 'object') { return `{${Object.keys(obj) .map((key) => `${key}:${displayObject(obj[key])}`) .join(',')}}`; diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index a6f45768e62b..c0dd91e36ddc 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,5 +1,5 @@ /* eslint-disable no-param-reassign */ -import { createApp, h, isReactive, reactive, watch } from 'vue'; +import { createApp, h, isReactive, isVNode, reactive, watch } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { Globals, Args, StoryContext } from '@storybook/csf'; import { global as globalThis } from '@storybook/global'; @@ -76,10 +76,10 @@ export function renderToCanvas( }); }, renderTracked(event) { - console.log('vueApp renderTracked', event); // this works only in dev mode + // console.log('vueApp renderTracked', event); // this works only in dev mode }, renderTriggered(event) { - console.log('vueApp renderTriggered', event); // this works only in dev mode + // console.log('vueApp renderTriggered', event); // this works only in dev mode }, }); vueApp.config.errorHandler = (e: unknown) => showException(e as Error); @@ -99,7 +99,9 @@ function getSlots(context: StoryContext) { .map(([key, value]) => [ key, () => { - if (typeof context.args[key] === 'function') return h(context.args[key]); + if (typeof context.args[key] === 'function' || isVNode(context.args[key])) + return h(context.args[key]); + if (Array.isArray(context.args[key])) return context.args[key].map((item: any) => h(item)); if (typeof context.args[key] === 'object') return JSON.stringify(context.args[key]); if (typeof context.args[key] === 'string') return context.args[key]; return context.args[key]; From fc833f4871cbf978d1974bac8cfd2d285815bf5b Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 9 Mar 2023 19:21:13 +0400 Subject: [PATCH 036/112] run decorators in renderToCanvas save reactivity --- code/lib/store/template/stories/globals.stories.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/code/lib/store/template/stories/globals.stories.ts b/code/lib/store/template/stories/globals.stories.ts index 216551d9fb5b..625c14829e9e 100644 --- a/code/lib/store/template/stories/globals.stories.ts +++ b/code/lib/store/template/stories/globals.stories.ts @@ -29,6 +29,7 @@ export const Events = { ], play: async ({ canvasElement }: PlayFunctionContext) => { const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; + await channel.emit('updateGlobals', { globals: { foo: 'fooValue' } }); await within(canvasElement).findByText('fooValue'); await channel.emit('updateGlobals', { globals: { foo: 'updated' } }); From a46d316e35dcf489fa0f3d2227e9dd75e58ff365 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 9 Mar 2023 19:22:37 +0400 Subject: [PATCH 037/112] great reasult finally --- code/renderers/vue3/src/decorateStory.ts | 3 +- code/renderers/vue3/src/render.ts | 71 ++++++++++++++++++++---- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 13ed507a822a..47a0d325efc7 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -12,6 +12,7 @@ import type { VueRenderer } from './types'; The concept is taken from Vue 3's `defineComponent` but changed from creating a `setup` method on the ComponentOptions so end-users don't need to specify a "thunk" as a decorator. */ + function normalizeFunctionalComponent(options: ConcreteComponent): ComponentOptions { return typeof options === 'function' ? { render: options, name: options.name } : options; } @@ -92,7 +93,7 @@ export function decorateStory( * @param context * @param update */ -function updateReactiveContext( +export function updateReactiveContext( context: StoryContext, update: StoryContextUpdate> | undefined ) { diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index c0dd91e36ddc..09608202227a 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,9 +1,16 @@ /* eslint-disable no-param-reassign */ import { createApp, h, isReactive, isVNode, reactive, watch } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; -import type { Globals, Args, StoryContext } from '@storybook/csf'; -import { global as globalThis } from '@storybook/global'; +import type { + Globals, + Args, + StoryContext, + DecoratorFunction, + PartialStoryFn, +} from '@storybook/csf'; + import type { StoryFnVueReturnType, VueRenderer } from './types'; +import { updateReactiveContext } from './decorateStory'; export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; @@ -13,7 +20,7 @@ export const render: ArgsStoryFn = (props, context) => { ); } - const slots = getSlots(context); + const slots = generateSlots(context); return h(Component, props, slots); }; @@ -31,6 +38,7 @@ const map = new Map< >(); let reactiveState: { globals: Globals; + changed: boolean; }; export function renderToCanvas( { storyFn, forceRemount, showMain, showException, storyContext, id }: RenderContext, @@ -42,7 +50,8 @@ export function renderToCanvas( // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { - if (reactiveState) reactiveState.globals = storyContext.globals; + updateGlobals(storyContext); + updateContextDecorator(storyFn, storyContext); updateArgs(existingApp.reactiveArgs, storyContext.args); return () => { teardown(existingApp.vueApp, canvasElement); @@ -54,18 +63,19 @@ export function renderToCanvas( const vueApp = createApp({ setup() { storyContext.args = reactiveArgs; - reactiveState = reactive({ globals: storyContext.globals }); - const rootElement: StoryFnVueReturnType = storyFn(); + reactiveState = reactive({ globals: storyContext.globals, changed: false }); + let rootElement: StoryFnVueReturnType = storyFn(); watch( () => reactiveState.globals, - (newVal) => { - const channel = (globalThis as any).__STORYBOOK_ADDONS_CHANNEL__; - channel.emit('forceRemount', { storyId: id }); + () => { + reactiveState.changed = true; } ); return () => { + storyContext.globals = reactiveState.globals; + rootElement = reactiveState.changed ? storyFn() : rootElement; return h(rootElement, reactiveArgs); }; }, @@ -92,7 +102,12 @@ export function renderToCanvas( }; } -function getSlots(context: StoryContext) { +/** + * generate slots for default story without render function template + * @param context + */ + +function generateSlots(context: StoryContext) { const { argTypes } = context; const slots = Object.entries(argTypes) .filter(([key, value]) => argTypes[key]?.table?.category === 'slots') @@ -110,6 +125,39 @@ function getSlots(context: StoryContext) { return reactive(Object.fromEntries(slots)); } +/** + * update vue reactive state for globals to be able to dectect changes and re-render the story + * @param storyContext + */ +function updateGlobals(storyContext: StoryContext) { + if (reactiveState) { + reactiveState.changed = false; + reactiveState.globals = storyContext.globals; + } +} + +/** + * update the context args in case of decorators that change args + * @param storyFn + * @param storyContext + */ + +function updateContextDecorator( + storyFn: PartialStoryFn, + storyContext: StoryContext +) { + const storyDecorators = storyContext.moduleExport?.decorators; + if (storyDecorators && storyDecorators.length > 0) { + storyDecorators.forEach((decorator: DecoratorFunction) => { + if (typeof decorator === 'function') { + decorator((update) => { + if (update) updateReactiveContext(storyContext, update); + return storyFn(); + }, storyContext); + } + }); + } +} /** * update the reactive args @@ -118,8 +166,10 @@ function getSlots(context: StoryContext) { * @returns */ export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string[]) { + if (Object.keys(nextArgs).length === 0) return; const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs); const notMappedArgs = { ...nextArgs }; + Object.keys(currentArgs).forEach((key) => { const componentArg = currentArgs[key]; // if the arg is an object, we need to update the object @@ -128,6 +178,7 @@ export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string if (nextArgs[aKey] && (argNames?.includes(aKey) || !argNames)) { currentArgs[key][aKey] = nextArgs[aKey]; delete notMappedArgs[aKey]; + delete notMappedArgs[key]; } }); } else { From 069d6155f6c10a3dcd986e76f1f474a4a66f875e Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 9 Mar 2023 21:39:28 +0400 Subject: [PATCH 038/112] add try catch in case decorator are react Fn use --- code/renderers/vue3/src/render.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 09608202227a..0b0c8535b9e3 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -149,11 +149,18 @@ function updateContextDecorator( const storyDecorators = storyContext.moduleExport?.decorators; if (storyDecorators && storyDecorators.length > 0) { storyDecorators.forEach((decorator: DecoratorFunction) => { - if (typeof decorator === 'function') { - decorator((update) => { - if (update) updateReactiveContext(storyContext, update); - return storyFn(); - }, storyContext); + try { + if (typeof decorator === 'function') { + decorator((update) => { + if (update) updateReactiveContext(storyContext, update); + return storyFn(); + }, storyContext); + } + } catch (e) { + console.error(e); + // in case the decorator throws an error, we need to re-render the story + // mostly because of react hooks that are not allowed to be called conditionally + reactiveState.changed = true; } }); } From 8a3ea0a0b5bc3c623e9cfc48d9bf75b993744d68 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Fri, 10 Mar 2023 07:15:50 +0400 Subject: [PATCH 039/112] slots properly rendered , and source generated --- code/renderers/vue3/src/render.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 0b0c8535b9e3..653dd38f8d7e 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -75,6 +75,7 @@ export function renderToCanvas( return () => { storyContext.globals = reactiveState.globals; + console.log('---******---- render', reactiveState.changed, rootElement); rootElement = reactiveState.changed ? storyFn() : rootElement; return h(rootElement, reactiveArgs); }; From 4408459fee84e9b4788ac0cd381de7c4e77d530e Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Fri, 10 Mar 2023 07:38:30 +0400 Subject: [PATCH 040/112] cleanup --- code/renderers/vue3/src/render.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 653dd38f8d7e..36d4ae7b974c 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -38,7 +38,6 @@ const map = new Map< >(); let reactiveState: { globals: Globals; - changed: boolean; }; export function renderToCanvas( { storyFn, forceRemount, showMain, showException, storyContext, id }: RenderContext, @@ -63,20 +62,18 @@ export function renderToCanvas( const vueApp = createApp({ setup() { storyContext.args = reactiveArgs; - reactiveState = reactive({ globals: storyContext.globals, changed: false }); + reactiveState = reactive({ globals: storyContext.globals }); let rootElement: StoryFnVueReturnType = storyFn(); watch( () => reactiveState.globals, () => { - reactiveState.changed = true; + storyContext.globals = reactiveState.globals; + rootElement = storyFn(); } ); return () => { - storyContext.globals = reactiveState.globals; - console.log('---******---- render', reactiveState.changed, rootElement); - rootElement = reactiveState.changed ? storyFn() : rootElement; return h(rootElement, reactiveArgs); }; }, @@ -132,7 +129,6 @@ function generateSlots(context: StoryContext) { */ function updateGlobals(storyContext: StoryContext) { if (reactiveState) { - reactiveState.changed = false; reactiveState.globals = storyContext.globals; } } @@ -161,7 +157,7 @@ function updateContextDecorator( console.error(e); // in case the decorator throws an error, we need to re-render the story // mostly because of react hooks that are not allowed to be called conditionally - reactiveState.changed = true; + reactiveState.globals = { ...storyContext.globals, change: Math.random() }; } }); } From 4b3ee6704be5540733beb170553d11bb3bd4711e Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Fri, 10 Mar 2023 07:45:56 +0400 Subject: [PATCH 041/112] source generating for slots with recusivity with --- .../vue3/src/docs/sourceDecorator.ts | 112 +++++++++++------- .../vue3/template/components/BaseLayout.vue | 17 +++ 2 files changed, 84 insertions(+), 45 deletions(-) create mode 100644 code/renderers/vue3/template/components/BaseLayout.vue diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index e0689ad886aa..a6e0f67e3be1 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -3,7 +3,7 @@ import { addons } from '@storybook/preview-api'; import type { ArgTypes, Args, StoryContext, Renderer } from '@storybook/types'; -import { getDocgenSection, SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; +import { SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; import type { ElementNode, @@ -14,7 +14,8 @@ import type { TemplateChildNode, } from '@vue/compiler-core'; import { baseParse } from '@vue/compiler-core'; -import { h, watch } from 'vue'; +import type { ConcreteComponent, FunctionalComponent, VNode } from 'vue'; +import { h, isVNode, watch } from 'vue'; import { camelCase, kebabCase } from 'lodash'; import { attributeSource, @@ -23,7 +24,6 @@ import { displayObject, evalExp, } from './utils'; -import type { VueStoryComponent } from '../types'; /** * Check if the sourcecode should be generated. @@ -99,16 +99,28 @@ function mapAttributesAndDirectives(props: Args) { * map slots * @param slotsProps */ -function mapSlots(slotsProps: Args): TextNode[] { +function mapSlots(slotsProps: Args, generateComponentSource: (v: VNode) => string): TextNode[] { return Object.keys(slotsProps).map((key) => { const slot = slotsProps[key]; let slotContent = ''; - if (typeof slot === 'function') slotContent = ``; - slotContent = ``; - if (key === 'default') { + + if (typeof slot === 'function') { + const slotVNode = slot(); + if (isVNode(slotVNode)) { + slotContent = generateComponentSource(h(slotVNode)); + } + } + + if (typeof slot === 'object' && !isVNode(slot)) { slotContent = JSON.stringify(slot); } + if (typeof slot === 'string') { + slotContent = slot; + } + + slotContent = slot && slotContent.trim() ? `` : ``; + return { type: 2, content: slotContent, @@ -144,17 +156,20 @@ function generateScriptSetup(args: Args, argTypes: ArgTypes, components: any[]): function getTemplateComponents( renderFn: any, context?: StoryContext -): TemplateChildNode[] { +): (TemplateChildNode | VNode)[] { try { - const { template } = context ? renderFn(context.args, context) : renderFn(); - if (!template) return []; + const originalStoryFn = renderFn; + const story = originalStoryFn ? originalStoryFn(context?.args, context) : context?.component; + const { template } = story; + + if (!template) return [h(story, context?.args)]; return getComponents(template); } catch (e) { return []; } } -function getComponents(template: string): TemplateChildNode[] { +function getComponents(template: string): (TemplateChildNode | VNode)[] { const ast = baseParse(template); const components = ast?.children; if (!components) return []; @@ -171,22 +186,24 @@ function getComponents(template: string): TemplateChildNode[] { */ export function generateTemplateSource( - componentOrNodes: (VueStoryComponent | TemplateChildNode)[] | TemplateChildNode, + componentOrNodes: (ConcreteComponent | TemplateChildNode)[] | TemplateChildNode | VNode, args: Args, argTypes: ArgTypes, byRef = false ) { - const isComponent = (component: any) => component && typeof component.render === 'function'; 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: VueStoryComponent | TemplateChildNode) => { + const generateComponentSource = ( + componentOrNode: ConcreteComponent | TemplateChildNode | VNode + ) => { if (isElementNode(componentOrNode)) { const { tag: name, props: attributes, children } = componentOrNode as ElementNode; - const childSources: string = children - .map((child: TemplateChildNode) => generateComponentSource(child)) - .join(''); + const childSources: string = + typeof children === 'string' + ? children + : children.map((child: TemplateChildNode) => generateComponentSource(child)).join(''); const props = generateAttributesSource(attributes, args, argTypes, byRef); return childSources === '' @@ -197,35 +214,39 @@ export function generateTemplateSource( if (isInterpolationNode(componentOrNode) || isTextNode(componentOrNode)) { const { content } = componentOrNode as InterpolationNode | TextNode; // eslint-disable-next-line no-eval - if (typeof content !== 'string') return eval(content.loc.source); // it's a binding safe to eval + if (content && typeof content !== 'string') return eval(content.loc.source); // it's a binding safe to eval return content; } - - if (isComponent(componentOrNode)) { - const concreteComponent = componentOrNode as VueStoryComponent; - const vnode = h(componentOrNode, args); - const { props } = vnode; - const { slots } = getDocgenSection(concreteComponent, 'slots') || {}; - const slotsProps = {} as Args; - const attrsProps = { ...props }; - if (slots && props) - Object.keys(props).forEach((prop: any) => { - const isSlot = slots.find(({ name: slotName }: { name: string }) => slotName === prop); - if (isSlot?.name) { - slotsProps[prop] = props[prop]; - delete attrsProps[prop]; - } - }); - const attributes = mapAttributesAndDirectives(attrsProps); - const childSources: string = mapSlots(slotsProps) - .map((child) => generateComponentSource(child)) - .join(''); - const name = concreteComponent.tag || concreteComponent.name || concreteComponent.__name; + if (isVNode(componentOrNode)) { + const vnode = componentOrNode as VNode; + const { props, type, children } = vnode; + const slotsProps = typeof children === 'string' ? undefined : (children as Args); + 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 ?? {}); + // eslint-disable-next-line no-nested-ternary + const childSources: string = children + ? typeof children === 'string' + ? children + : mapSlots(children as Args, generateComponentSource) + .map((child) => child.content) + .join('') + : ''; + const name = + typeof type === 'string' + ? type + : (type as FunctionalComponent).name || (type as ConcreteComponent).__name; const propsSource = generateAttributesSource(attributes, args, argTypes, byRef); return childSources.trim() === '' ? `<${name} ${propsSource}/>` : `<${name} ${propsSource}>${childSources}`; } + return null; }; @@ -244,6 +265,7 @@ export function generateTemplateSource( export const sourceDecorator = (storyFn: any, context: StoryContext) => { const skip = skipSourceRender(context); const story = storyFn(); + generateSource(context); watch( () => context.args, () => { @@ -251,20 +273,20 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = generateSource(context); } }, - { immediate: true, deep: true } + { deep: true } ); return story; }; export function generateSource(context: StoryContext) { const channel = addons.getChannel(); - const { args = {}, component: ctxtComponent, argTypes = {}, id } = context || {}; - const components = getTemplateComponents(context?.originalStoryFn, context); - const storyComponent = components.length ? components : (ctxtComponent as TemplateChildNode); + const { args = {}, argTypes = {}, id } = context || {}; + const storyComponents = getTemplateComponents(context?.originalStoryFn, context); const withScript = context?.parameters?.docs?.source?.withScriptSetup || false; - const generatedScript = withScript ? generateScriptSetup(args, argTypes, components) : ''; - const generatedTemplate = generateTemplateSource(storyComponent, context.args, context.argTypes); + const generatedScript = withScript ? generateScriptSetup(args, argTypes, storyComponents) : ''; + const generatedTemplate = generateTemplateSource(storyComponents, context.args, context.argTypes); + if (generatedTemplate) { const source = `${generatedScript}\n `; channel.emit(SNIPPET_RENDERED, { id, args, source, format: 'vue' }); diff --git a/code/renderers/vue3/template/components/BaseLayout.vue b/code/renderers/vue3/template/components/BaseLayout.vue new file mode 100644 index 000000000000..3e546165f187 --- /dev/null +++ b/code/renderers/vue3/template/components/BaseLayout.vue @@ -0,0 +1,17 @@ + + + From d8c87af2a03a5e6023b3a88baf664295392bb788 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 13 Mar 2023 07:00:40 +0400 Subject: [PATCH 042/112] refactory and cleanup --- code/renderers/vue3/src/decorateStory.ts | 7 +- .../vue3/src/docs/sourceDecorator.ts | 20 +----- code/renderers/vue3/src/docs/utils.ts | 10 ++- code/renderers/vue3/src/render.ts | 67 ++++++------------- 4 files changed, 33 insertions(+), 71 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 47a0d325efc7..9c45525d11fd 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -1,5 +1,5 @@ import type { ConcreteComponent, Component, ComponentOptions } from 'vue'; -import { reactive, h } from 'vue'; +import { h } from 'vue'; import type { DecoratorFunction, StoryContext, LegacyStoryFn } from '@storybook/types'; import { sanitizeStoryContextUpdate } from '@storybook/preview-api'; @@ -82,8 +82,8 @@ export function decorateStory( return story; } - const storyFuntion = () => h(story ?? 'story', context.args); - return prepare(decoratedStory, storyFuntion) as VueRenderer['storyResult']; + const innerStory = () => (story ? h(story, context.args) : null); + return prepare(decoratedStory, innerStory) as VueRenderer['storyResult']; }, (context) => prepare(storyFn(context)) as LegacyStoryFn ); @@ -97,7 +97,6 @@ export function updateReactiveContext( context: StoryContext, update: StoryContextUpdate> | undefined ) { - context.args = reactive(context.args); // get reference to reactiveArgs or create a new one; in case was destructured by decorator if (update) { const { args, argTypes } = update; if (args && !argTypes) { diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index a6e0f67e3be1..7092a0182a18 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -16,12 +16,11 @@ import type { import { baseParse } from '@vue/compiler-core'; import type { ConcreteComponent, FunctionalComponent, VNode } from 'vue'; import { h, isVNode, watch } from 'vue'; -import { camelCase, kebabCase } from 'lodash'; +import { kebabCase } from 'lodash'; import { attributeSource, htmlEventAttributeToVueEventAttribute, omitEvent, - displayObject, evalExp, } from './utils'; @@ -58,22 +57,7 @@ export function generateAttributesSource( ): string { return Object.keys(tempArgs) .map((key: any) => { - const arg = tempArgs[key]; - - if (arg.type === 7) { - // AttributeNode binding type - const { exp, arg: argName } = arg; - const argKey = argName ? argName?.loc.source : undefined; - const argExpValue = exp?.loc.source ?? (exp as any).content; - const propValue = args[camelCase(argKey)]; - const argValue = argKey - ? propValue ?? evalExp(argExpValue, args) - : displayObject(omitEvent(args)); - - return argKey ? attributeSource(argKey, argValue, true) : `v-bind="${argValue}"`; - } - - return tempArgs[key].loc.source; + return evalExp(tempArgs[key].loc.source.replace(/\$props/g, 'args'), omitEvent(args)); }) .join(' '); } diff --git a/code/renderers/vue3/src/docs/utils.ts b/code/renderers/vue3/src/docs/utils.ts index 538a235fbeb0..f4d9a2000883 100644 --- a/code/renderers/vue3/src/docs/utils.ts +++ b/code/renderers/vue3/src/docs/utils.ts @@ -5,7 +5,9 @@ import type { Args } from '@storybook/types'; * @param args */ const omitEvent = (args: Args): Args => - Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); + args + ? Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))) + : {}; const displayObject = (obj: any): string | boolean | number => { if (obj && typeof obj === 'object') { @@ -34,13 +36,17 @@ const attributeSource = (key: string, value: unknown, dynamic?: boolean) => const evalExp = (argExpValue: any, args: Args): any => { let evalVal = argExpValue; + if (/v-bind="(\w+)"/.test(evalVal)) + return evalVal.replace(/"(\w+)"/g, `"${displayObject(args)}"`); Object.keys(args).forEach((akey) => { const regex = new RegExp(`(\\w+)\\.${akey}`, 'g'); - evalVal = evalVal.replace(regex, args[akey]); + evalVal = evalVal.replace(regex, displayObject(args[akey])); }); return evalVal; }; +// regExp match a word without dots with double quotes expression (e.g. "args") + export { omitEvent, displayObject, diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 36d4ae7b974c..7ccc47f6f45b 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -9,8 +9,8 @@ import type { PartialStoryFn, } from '@storybook/csf'; +import type { HooksContext } from 'lib/preview-api/src'; import type { StoryFnVueReturnType, VueRenderer } from './types'; -import { updateReactiveContext } from './decorateStory'; export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; @@ -20,8 +20,7 @@ export const render: ArgsStoryFn = (props, context) => { ); } - const slots = generateSlots(context); - return h(Component, props, slots); + return h(Component, props, generateSlots(context)); }; let setupFunction = (_app: any) => {}; @@ -49,7 +48,6 @@ export function renderToCanvas( // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { - updateGlobals(storyContext); updateContextDecorator(storyFn, storyContext); updateArgs(existingApp.reactiveArgs, storyContext.args); return () => { @@ -112,26 +110,17 @@ function generateSlots(context: StoryContext) { .map(([key, value]) => [ key, () => { - if (typeof context.args[key] === 'function' || isVNode(context.args[key])) - return h(context.args[key]); - if (Array.isArray(context.args[key])) return context.args[key].map((item: any) => h(item)); - if (typeof context.args[key] === 'object') return JSON.stringify(context.args[key]); - if (typeof context.args[key] === 'string') return context.args[key]; - return context.args[key]; + const slotValue = context.args[key]; + if (typeof slotValue === 'function' || isVNode(slotValue)) return h(slotValue); + if (Array.isArray(slotValue)) return slotValue.map((item: any) => h(item)); + if (typeof slotValue === 'object') return JSON.stringify(slotValue); + if (typeof slotValue === 'string') return slotValue; + return slotValue; }, ]); return reactive(Object.fromEntries(slots)); } -/** - * update vue reactive state for globals to be able to dectect changes and re-render the story - * @param storyContext - */ -function updateGlobals(storyContext: StoryContext) { - if (reactiveState) { - reactiveState.globals = storyContext.globals; - } -} /** * update the context args in case of decorators that change args @@ -143,21 +132,24 @@ function updateContextDecorator( storyFn: PartialStoryFn, storyContext: StoryContext ) { - const storyDecorators = storyContext.moduleExport?.decorators; - if (storyDecorators && storyDecorators.length > 0) { + const storyDecorators: Set> = ( + storyContext.hooks as HooksContext + ).mountedDecorators; + + if (storyDecorators && storyDecorators.size > 0) { storyDecorators.forEach((decorator: DecoratorFunction) => { try { if (typeof decorator === 'function') { - decorator((update) => { - if (update) updateReactiveContext(storyContext, update); - return storyFn(); + decorator((u) => { + if (u && u.args && !u.globals) return storyFn(); + return () => {}; }, storyContext); } } catch (e) { - console.error(e); + console.log(' issue with decorator ', decorator.name); // in case the decorator throws an error, we need to re-render the story // mostly because of react hooks that are not allowed to be called conditionally - reactiveState.globals = { ...storyContext.globals, change: Math.random() }; + reactiveState.globals = storyContext.globals; // { ...storyContext.globals }; } }); } @@ -169,29 +161,10 @@ function updateContextDecorator( * @param nextArgs * @returns */ -export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string[]) { +export function updateArgs(reactiveArgs: Args, nextArgs: Args) { if (Object.keys(nextArgs).length === 0) return; const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs); - const notMappedArgs = { ...nextArgs }; - - Object.keys(currentArgs).forEach((key) => { - const componentArg = currentArgs[key]; - // if the arg is an object, we need to update the object - if (typeof componentArg === 'object') { - Object.keys(componentArg).forEach((aKey) => { - if (nextArgs[aKey] && (argNames?.includes(aKey) || !argNames)) { - currentArgs[key][aKey] = nextArgs[aKey]; - delete notMappedArgs[aKey]; - delete notMappedArgs[key]; - } - }); - } else { - currentArgs[key] = nextArgs[key]; - } - }); - Object.keys(notMappedArgs).forEach((key) => { - currentArgs[key] = notMappedArgs[key]; - }); + Object.assign(currentArgs, nextArgs); } /** From 5643df23093432c0a2350bcc3c4f4014a735a789 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 13 Mar 2023 11:35:24 +0400 Subject: [PATCH 043/112] fix the units tests --- .../vue3/src/docs/sourceDecorator.test.ts | 4 ++-- code/renderers/vue3/src/render.test.ts | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index 5a239de38120..43bd1c7476c3 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -303,7 +303,7 @@ describe('Vue3: generateSource() snippet', () => { [] as ArgsType, `` ) - ).toMatchInlineSnapshot(``); + ).toMatchInlineSnapshot(``); }); test('template component camelCase bool Arg', () => { @@ -329,7 +329,7 @@ describe('Vue3: generateSource() snippet', () => { `` ) ).toMatchInlineSnapshot( - `` + `` ); }); diff --git a/code/renderers/vue3/src/render.test.ts b/code/renderers/vue3/src/render.test.ts index 5c39ef32a6b2..af4681d58354 100644 --- a/code/renderers/vue3/src/render.test.ts +++ b/code/renderers/vue3/src/render.test.ts @@ -23,17 +23,21 @@ describe('Render Story', () => { const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; updateArgs(reactiveArgs, newArgs); expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string; argBar: string } }>(); - expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo2', argBar: 'bar2' } }); + expect(reactiveArgs).toEqual({ + objectArg: { argFoo: 'foo', argBar: 'bar' }, + argFoo: 'foo2', + argBar: 'bar2', + }); }); - test('update reactive Args component inherit objectArg only argName argName()', () => { + test('update reactive Args component inherit objectArg', () => { const reactiveArgs = reactive({ objectArg: { argFoo: 'foo' } }); // get reference to reactiveArgs or create a new one; expectTypeOf(reactiveArgs).toMatchTypeOf>(); expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string } }>(); const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; - updateArgs(reactiveArgs, newArgs, ['argFoo']); - expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo2' }, argBar: 'bar2' }); + updateArgs(reactiveArgs, newArgs); + expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo' }, argFoo: 'foo2', argBar: 'bar2' }); }); test('update reactive Args component 2 object args -> updateArgs()', () => { @@ -51,8 +55,10 @@ describe('Render Story', () => { updateArgs(reactiveArgs, newArgs); expect(reactiveArgs).toEqual({ - objectArg: { argFoo: 'foo2' }, - objectArg2: { argBar: 'bar2' }, + argFoo: 'foo2', + argBar: 'bar2', + objectArg: { argFoo: 'foo' }, + objectArg2: { argBar: 'bar' }, }); }); From 4634f30ac5301035be993626a2e7703c31b2ee1f Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 13 Mar 2023 12:54:42 +0400 Subject: [PATCH 044/112] remove console logs --- code/renderers/vue3/src/render.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 7ccc47f6f45b..2a6a5e861c65 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -146,7 +146,6 @@ function updateContextDecorator( }, storyContext); } } catch (e) { - console.log(' issue with decorator ', decorator.name); // in case the decorator throws an error, we need to re-render the story // mostly because of react hooks that are not allowed to be called conditionally reactiveState.globals = storyContext.globals; // { ...storyContext.globals }; From 12dd5fb2a9348d6ac61dc7a095de1ef0d479e679 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 13 Mar 2023 13:07:05 +0400 Subject: [PATCH 045/112] cleanup oode and try to solve yn pkg issue --- code/renderers/vue3/src/decorateStory.ts | 1 - code/renderers/vue3/src/docs/utils.ts | 2 -- code/renderers/vue3/src/types.ts | 9 --------- 3 files changed, 12 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 9c45525d11fd..e88495936183 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -43,7 +43,6 @@ function prepare( return { render() { - // console.log('story render', story, this.$slots); return h(story); }, renderTracked(event) { diff --git a/code/renderers/vue3/src/docs/utils.ts b/code/renderers/vue3/src/docs/utils.ts index f4d9a2000883..91fc1b42af06 100644 --- a/code/renderers/vue3/src/docs/utils.ts +++ b/code/renderers/vue3/src/docs/utils.ts @@ -45,8 +45,6 @@ const evalExp = (argExpValue: any, args: Args): any => { return evalVal; }; -// regExp match a word without dots with double quotes expression (e.g. "args") - export { omitEvent, displayObject, diff --git a/code/renderers/vue3/src/types.ts b/code/renderers/vue3/src/types.ts index f5b4576cedf7..fa17d9b0eeae 100644 --- a/code/renderers/vue3/src/types.ts +++ b/code/renderers/vue3/src/types.ts @@ -12,15 +12,6 @@ export type StoryFnVueReturnType = ConcreteComponent; export type StoryContext = StoryContextBase; -export type VueStoryComponent = ConcreteComponent & { - render: (h: any) => any; - props: VNodeProps; - slots: Slots; - tag?: string; - name?: string; - __name?: string; -}; - /** * @deprecated Use `VueRenderer` instead. */ From f2a8cc7885238fdef98298d6d0b4f3dc3f0d571f Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 13 Mar 2023 17:03:47 +0400 Subject: [PATCH 046/112] cleanup and refactory --- code/renderers/vue3/src/decorateStory.ts | 14 +------------- code/renderers/vue3/src/render.ts | 6 ------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index e88495936183..c6f18361f6da 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -32,12 +32,6 @@ function prepare( // Normalize so we can always spread an object ...normalizeFunctionalComponent(story), components: { ...(story.components || {}), story: innerStory }, - renderTracked(event) { - // console.log('innerStory renderTracked', event); // this works only in dev mode - }, - renderTriggered(event) { - // console.log('innerStory renderTriggered', event); - }, }; } @@ -45,12 +39,6 @@ function prepare( render() { return h(story); }, - renderTracked(event) { - // console.log('story renderTracked', event); // this works only in dev mode - }, - renderTriggered(event) { - // console.log('story renderTriggered', event); - }, }; } @@ -99,7 +87,7 @@ export function updateReactiveContext( if (update) { const { args, argTypes } = update; if (args && !argTypes) { - const deepCopy = JSON.parse(JSON.stringify(args)); // avoid reference to args + const deepCopy = JSON.parse(JSON.stringify(args)); // avoid reference to args we assume it's serializable Object.keys(context.args).forEach((key) => { delete context.args[key]; }); diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 2a6a5e861c65..1c4ff919f4ef 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -81,12 +81,6 @@ export function renderToCanvas( reactiveArgs, }); }, - renderTracked(event) { - // console.log('vueApp renderTracked', event); // this works only in dev mode - }, - renderTriggered(event) { - // console.log('vueApp renderTriggered', event); // this works only in dev mode - }, }); vueApp.config.errorHandler = (e: unknown) => showException(e as Error); setupFunction(vueApp); From 9c8ae2c1ae3e61fd96f28924a87e1e110c7f6fd5 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 13 Mar 2023 22:25:16 +0400 Subject: [PATCH 047/112] fix prep, pass tests --- code/renderers/vue3/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/renderers/vue3/src/types.ts b/code/renderers/vue3/src/types.ts index fa17d9b0eeae..15809f9094e2 100644 --- a/code/renderers/vue3/src/types.ts +++ b/code/renderers/vue3/src/types.ts @@ -1,5 +1,5 @@ import type { StoryContext as StoryContextBase, WebRenderer } from '@storybook/types'; -import type { ConcreteComponent, Slots, VNodeProps } from 'vue'; +import type { ConcreteComponent } from 'vue'; export type { RenderContext } from '@storybook/types'; From 3add1df064ae82f28166c5303d93ef94361ba9cf Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 13 Mar 2023 22:59:17 +0400 Subject: [PATCH 048/112] refactory get slots --- code/renderers/vue3/src/render.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 1c4ff919f4ef..ad12b9e6d0a4 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,5 +1,5 @@ /* eslint-disable no-param-reassign */ -import { createApp, h, isReactive, isVNode, reactive, watch } from 'vue'; +import { createApp, h, isReactive, reactive, watch } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { Globals, @@ -105,11 +105,7 @@ function generateSlots(context: StoryContext) { key, () => { const slotValue = context.args[key]; - if (typeof slotValue === 'function' || isVNode(slotValue)) return h(slotValue); - if (Array.isArray(slotValue)) return slotValue.map((item: any) => h(item)); - if (typeof slotValue === 'object') return JSON.stringify(slotValue); - if (typeof slotValue === 'string') return slotValue; - return slotValue; + return typeof slotValue === 'function' ? h(slotValue) : slotValue; }, ]); From f274f05fae3e92e6def48100d49fb607bc0be5a8 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Tue, 14 Mar 2023 00:06:54 +0400 Subject: [PATCH 049/112] fix args updates and correct unit tests --- code/renderers/vue3/src/render.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/code/renderers/vue3/src/render.test.ts b/code/renderers/vue3/src/render.test.ts index af4681d58354..d1d04cf40638 100644 --- a/code/renderers/vue3/src/render.test.ts +++ b/code/renderers/vue3/src/render.test.ts @@ -24,7 +24,6 @@ describe('Render Story', () => { updateArgs(reactiveArgs, newArgs); expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string; argBar: string } }>(); expect(reactiveArgs).toEqual({ - objectArg: { argFoo: 'foo', argBar: 'bar' }, argFoo: 'foo2', argBar: 'bar2', }); @@ -37,7 +36,7 @@ describe('Render Story', () => { const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; updateArgs(reactiveArgs, newArgs); - expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo' }, argFoo: 'foo2', argBar: 'bar2' }); + expect(reactiveArgs).toEqual({ argFoo: 'foo2', argBar: 'bar2' }); }); test('update reactive Args component 2 object args -> updateArgs()', () => { @@ -57,8 +56,6 @@ describe('Render Story', () => { expect(reactiveArgs).toEqual({ argFoo: 'foo2', argBar: 'bar2', - objectArg: { argFoo: 'foo' }, - objectArg2: { argBar: 'bar' }, }); }); From 24aa49298d3cb6d499415a7bd8c932283f3cf778 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Tue, 14 Mar 2023 00:08:12 +0400 Subject: [PATCH 050/112] fix args update --- code/renderers/vue3/src/render.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index ad12b9e6d0a4..64fe3cacfc1e 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -153,6 +153,13 @@ function updateContextDecorator( export function updateArgs(reactiveArgs: Args, nextArgs: Args) { if (Object.keys(nextArgs).length === 0) return; const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs); + // delete all args in currentArgs that are not in nextArgs + Object.keys(currentArgs).forEach((key) => { + if (!(key in nextArgs)) { + delete currentArgs[key]; + } + }); + // update currentArgs with nextArgs Object.assign(currentArgs, nextArgs); } From d62fa0fd9e831e8917cca30bebb7b110357886a6 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Tue, 14 Mar 2023 20:01:09 +0400 Subject: [PATCH 051/112] fix hooks story tests, by triggering render all tree --- code/renderers/vue3/src/render.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 64fe3cacfc1e..c39fe14d735e 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -138,7 +138,7 @@ function updateContextDecorator( } catch (e) { // in case the decorator throws an error, we need to re-render the story // mostly because of react hooks that are not allowed to be called conditionally - reactiveState.globals = storyContext.globals; // { ...storyContext.globals }; + reactiveState.globals = { ...storyContext.globals, update: !decorator.name }; // { ...storyContext.globals }; } }); } From 69ac8138dcfdfd2874f070500665620bbeabfdb1 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Wed, 15 Mar 2023 00:33:12 +0400 Subject: [PATCH 052/112] add slots test story to vue3 renderer templates --- .../{components => stories}/BaseLayout.vue | 6 +-- .../template/stories/ReactiveSlots.stories.js | 49 +++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) rename code/renderers/vue3/template/{components => stories}/BaseLayout.vue (69%) create mode 100644 code/renderers/vue3/template/stories/ReactiveSlots.stories.js diff --git a/code/renderers/vue3/template/components/BaseLayout.vue b/code/renderers/vue3/template/stories/BaseLayout.vue similarity index 69% rename from code/renderers/vue3/template/components/BaseLayout.vue rename to code/renderers/vue3/template/stories/BaseLayout.vue index 3e546165f187..c8773b85f326 100644 --- a/code/renderers/vue3/template/components/BaseLayout.vue +++ b/code/renderers/vue3/template/stories/BaseLayout.vue @@ -4,13 +4,13 @@ defineProps<{ otherProp: boolean; }>();