From 73143ede9cd267d6515f544f87d88f34a65e433c Mon Sep 17 00:00:00 2001 From: xeho91 Date: Tue, 4 Jun 2024 21:38:59 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20refactor:=20Implement=20space=20?= =?UTF-8?q?to=20insert=20`description.story`=20&=20`source.code`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/compiler/plugin.ts | 19 +- src/compiler/transform/Story/description.ts | 204 ---------------- src/compiler/transform/Story/props.ts | 257 ++++++++++++++++++++ src/compiler/transform/Story/source.ts | 214 ---------------- 4 files changed, 268 insertions(+), 426 deletions(-) delete mode 100644 src/compiler/transform/Story/description.ts create mode 100644 src/compiler/transform/Story/props.ts delete mode 100644 src/compiler/transform/Story/source.ts diff --git a/src/compiler/plugin.ts b/src/compiler/plugin.ts index 286e3203..48ddf42c 100644 --- a/src/compiler/plugin.ts +++ b/src/compiler/plugin.ts @@ -18,7 +18,8 @@ import { createAppendix } from './transform/create-appendix.js'; import { removeExportDefault } from './transform/remove-export-default.js'; import { insertDefineMetaJSDocCommentAsDescription } from './transform/define-meta/description.js'; import { destructureMetaFromDefineMeta } from './transform/define-meta/destructure-meta.js'; -import { insertStoryHTMLCommentAsDescription } from './transform/Story/description.js'; +import { updateCompiledStoryProps } from './transform/Story/props.js'; + import { getSvelteAST } from '../parser/ast.js'; import { extractStoriesNodesFromExportDefaultFn } from '../parser/extract/compiled/stories.js'; import { extractCompiledASTNodes } from '../parser/extract/compiled/nodes.js'; @@ -46,7 +47,9 @@ export async function plugin(): Promise { let rawCode = fs.readFileSync(id).toString(); if (svelteConfig?.preprocess) { - const processed = await preprocess(rawCode, svelteConfig.preprocess, { filename: id }); + const processed = await preprocess(rawCode, svelteConfig.preprocess, { + filename: id, + }); rawCode = processed.code; } @@ -74,17 +77,17 @@ export async function plugin(): Promise { const svelteStories = [...svelteNodes.storyComponents].reverse(); const compiledStories = [...extractedCompiledStoriesNodes].reverse(); + // @ts-expect-error FIXME: This exists at runtime. + // Need to research if its documented somewhere + const originalCode = this.originalCode ?? fs.readFileSync(id).toString(); + for (const [index, compiled] of Object.entries(compiledStories)) { - insertStoryHTMLCommentAsDescription({ + updateCompiledStoryProps({ code: magicCompiledCode, nodes: { svelte: svelteStories[index], compiled }, filename: id, + originalCode, }); - // moveSourceAttributeToParameters({ - // code, - // nodes: { svelte: svelteStories[index], compiled }, - // filename, - // }); } await destructureMetaFromDefineMeta({ diff --git a/src/compiler/transform/Story/description.ts b/src/compiler/transform/Story/description.ts deleted file mode 100644 index e789bdd3..00000000 --- a/src/compiler/transform/Story/description.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { logger } from '@storybook/client-logger'; -import dedent from 'dedent'; -import type { ObjectExpression, Property, Statement } from 'estree'; -import { toJs } from 'estree-util-to-js'; -import type MagicString from 'magic-string'; -import type { Component } from 'svelte/compiler'; - -import type { extractStoriesNodesFromExportDefaultFn } from '../../../parser/extract/compiled/stories.js'; -import type { SvelteASTNodes } from '../../../parser/extract/svelte/nodes.js'; - -interface Params { - code: MagicString; - nodes: { - svelte: SvelteASTNodes['storyComponents'][number]; - compiled: Awaited>[number]; - }; - filename: string; -} - -export function insertStoryHTMLCommentAsDescription(params: Params) { - const { code, nodes, filename } = params; - const { svelte, compiled } = nodes; - const { component, comment } = svelte; - - if (!comment) { - return; - } - - const metaObjectExpression = getMetaObjectExpression(compiled); - - const newStoryProperty = createProperty('story', { - type: 'Literal', - value: dedent(comment.data), - }); - const newDescriptionProperty = createProperty( - 'description', - createObjectExpression([newStoryProperty]) - ); - const newDocsProperty = createProperty('docs', createObjectExpression([newDescriptionProperty])); - const newParametersProperty = createProperty( - 'parameters', - createObjectExpression([newDocsProperty]) - ); - - const currentParametersPropertyIndex = findAttributeIndex('parameters', component); - - if (currentParametersPropertyIndex === -1) { - metaObjectExpression.properties.push(newParametersProperty); - - return updateCompiledNode({ code, nodes, metaObjectExpression }); - } - - const currentParametersProperty = metaObjectExpression.properties[currentParametersPropertyIndex]; - - if ( - currentParametersProperty.type !== 'Property' || - currentParametersProperty.value.type !== 'ObjectExpression' - ) { - // TODO: Update error message - throw new Error(); - } - - const currentDocsPropertyIndex = findPropertyIndex( - 'docs', - currentParametersProperty.value[0].expression - ); - - if (currentDocsPropertyIndex === -1) { - currentParametersProperty.value.properties.push(newDocsProperty); - metaObjectExpression.properties[currentParametersPropertyIndex] = currentParametersProperty; - - return updateCompiledNode({ code, nodes, metaObjectExpression }); - } - - const currentDocsProperty = - currentParametersProperty.value[0].expression.properties[currentDocsPropertyIndex]; - - if ( - currentDocsProperty.type !== 'Property' || - currentDocsProperty.value.type !== 'ObjectExpression' - ) { - // TODO: Update error message - throw new Error(); - } - - const currentDescriptionPropertyIndex = findPropertyIndex( - 'description', - currentDocsProperty.value - ); - - if (currentDescriptionPropertyIndex === -1) { - currentDocsProperty.value.properties.push(newDescriptionProperty); - currentParametersProperty.value.properties[currentDocsPropertyIndex] = currentDocsProperty; - metaObjectExpression.properties[currentParametersPropertyIndex] = currentParametersProperty; - - return updateCompiledNode({ code, nodes, metaObjectExpression }); - } - - const currentDescriptionProperty = - currentDocsProperty.value.properties[currentDescriptionPropertyIndex]; - - if ( - currentDescriptionProperty.type !== 'Property' || - currentDescriptionProperty.value.type !== 'ObjectExpression' - ) { - throw new Error(); - } - - const currentStoryPropertyIndex = findPropertyIndex('story', currentDescriptionProperty.value); - - if (currentStoryPropertyIndex !== -1) { - logger.warn( - ` already has explictly set description. Ignoring the HTML comment above. Stories file: ${filename}` - ); - - return; - } - - currentDescriptionProperty.value.properties.push(newStoryProperty); - currentDocsProperty.value.properties[currentDescriptionPropertyIndex] = - currentDescriptionProperty; - currentParametersProperty.value.properties[currentDocsPropertyIndex] = currentDocsProperty; - metaObjectExpression.properties[currentParametersPropertyIndex] = currentParametersProperty; - - return updateCompiledNode({ code, nodes, metaObjectExpression }); -} - -function getMetaObjectExpression(node: Params['nodes']['compiled']): ObjectExpression { - if (node.type === 'CallExpression' && node.arguments[1].type === 'ObjectExpression') { - return node.arguments[1]; - } - - if ( - node.type === 'ExpressionStatement' && - node.expression.type === 'CallExpression' && - node.expression.arguments[1].type === 'ObjectExpression' - ) { - return node.expression.arguments[1]; - } - - throw new Error('Internal error in attempt to extract meta as object expression.'); -} - -interface UpdateCompiledNode extends Pick { - metaObjectExpression: ObjectExpression; -} - -function updateCompiledNode(params: UpdateCompiledNode) { - const { code, nodes, metaObjectExpression } = params; - const { compiled } = nodes; - - if (compiled.type === 'CallExpression') { - compiled.arguments[1] === metaObjectExpression; - } - - if (compiled.type === 'ExpressionStatement' && compiled.expression.type === 'CallExpression') { - compiled.expression.arguments[1] === metaObjectExpression; - } - - // @ts-expect-error FIXME: These keys exists at runtime, perhaps I missed some type extension from `estree`? - const { start, end } = compiled; - - code.update( - start, - end, - toJs({ - type: 'Program', - sourceType: 'module', - body: [compiled as unknown as Statement], - }).value - ); -} - -function createProperty(name: string, value: Property['value']): Property { - return { - type: 'Property', - kind: 'init', - computed: false, - method: false, - shorthand: false, - key: { - type: 'Identifier', - name, - }, - value, - }; -} - -function createObjectExpression(properties: Property[]): ObjectExpression { - return { - type: 'ObjectExpression', - properties, - }; -} - -function findAttributeIndex(name: string, node: Component) { - return node.attributes.findIndex((a) => a.type === 'Attribute' && a.name === name); -} - -function findPropertyIndex(name: string, node: ObjectExpression) { - return node.properties.findIndex( - (p) => p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === name - ); -} diff --git a/src/compiler/transform/Story/props.ts b/src/compiler/transform/Story/props.ts new file mode 100644 index 00000000..d1a5ed28 --- /dev/null +++ b/src/compiler/transform/Story/props.ts @@ -0,0 +1,257 @@ +import { logger } from '@storybook/client-logger'; +import dedent from 'dedent'; +import type { ObjectExpression, Property, Statement } from 'estree'; +import { toJs } from 'estree-util-to-js'; +import type MagicString from 'magic-string'; +import type { Comment, Component } from 'svelte/compiler'; + +import type { extractStoriesNodesFromExportDefaultFn } from '../../../parser/extract/compiled/stories.js'; +import type { SvelteASTNodes } from '../../../parser/extract/svelte/nodes.js'; + +interface Params { + code: MagicString; + nodes: { + svelte: SvelteASTNodes['storyComponents'][number]; + compiled: Awaited>[number]; + }; + filename: string; + originalCode: string; +} + +export function updateCompiledStoryProps(params: Params) { + const { code, nodes, filename, originalCode } = params; + const { svelte, compiled } = nodes; + const { component, comment } = svelte; + + const storyPropsObjectExpression = getStoryPropsObjectExpression(compiled); + + const findParametersIndex = () => findPropertyIndex('parameters', storyPropsObjectExpression); + + if (findParametersIndex() === -1) { + storyPropsObjectExpression.properties.push( + createProperty('parameters', createObjectExpression([])) + ); + } + + const currentParametersProperty = storyPropsObjectExpression.properties[ + findParametersIndex() + ] as Property; + + if (currentParametersProperty.value.type !== 'ObjectExpression') { + throw new Error('Invalid schema'); + } + + const findDocsIndex = () => + findPropertyIndex('docs', currentParametersProperty.value as ObjectExpression); + + if (findDocsIndex() === -1) { + currentParametersProperty.value.properties.push( + createProperty('docs', createObjectExpression([])) + ); + } + + const currentDocsProperty = currentParametersProperty.value.properties[ + findDocsIndex() + ] as Property; + + if (currentDocsProperty.value.type !== 'ObjectExpression') { + throw new Error('Invalid schema'); + } + + if (comment) { + insertDescriptionStory({ comment, currentDocsProperty }); + } + insertSourceCode({ component, currentDocsProperty, filename, originalCode }); + + return updateCompiledNode({ + code, + nodes, + metaObjectExpression: storyPropsObjectExpression, + }); +} + +function getStoryPropsObjectExpression( + node: Awaited>[number] +): ObjectExpression { + if (node.type === 'CallExpression' && node.arguments[1].type === 'ObjectExpression') { + return node.arguments[1]; + } + + if ( + node.type === 'ExpressionStatement' && + node.expression.type === 'CallExpression' && + node.expression.arguments[1].type === 'ObjectExpression' + ) { + return node.expression.arguments[1]; + } + + throw new Error('Internal error in attempt to extract meta as object expression.'); +} + +function updateCompiledNode({ + code, + nodes, + metaObjectExpression, +}: { + code: Params['code']; + nodes: Params['nodes']; + metaObjectExpression: ObjectExpression; +}) { + const { compiled } = nodes; + + if (compiled.type === 'CallExpression') { + compiled.arguments[1] === metaObjectExpression; + } + + if (compiled.type === 'ExpressionStatement' && compiled.expression.type === 'CallExpression') { + compiled.expression.arguments[1] === metaObjectExpression; + } + + // @ts-expect-error FIXME: These keys exists at runtime, perhaps I missed some type extension from `estree`? + const { start, end } = compiled; + + code.update( + start, + end, + toJs({ + type: 'Program', + sourceType: 'module', + body: [compiled as unknown as Statement], + }).value + ); +} + +function createProperty(name: string, value: Property['value']): Property { + return { + type: 'Property', + kind: 'init', + computed: false, + method: false, + shorthand: false, + key: { + type: 'Identifier', + name, + }, + value, + }; +} + +function createObjectExpression(properties: Property[]): ObjectExpression { + return { + type: 'ObjectExpression', + properties, + }; +} + +function findPropertyIndex(name: string, node: ObjectExpression) { + return node.properties.findIndex( + (p) => p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === name + ); +} + +function insertDescriptionStory({ + comment, + currentDocsProperty, + filename, +}: { + comment: Comment; + currentDocsProperty: Property; + filename?: string; +}) { + if (currentDocsProperty.value.type !== 'ObjectExpression') { + // TODO: Update error message + throw new Error('Invalid schema'); + } + + const findDescriptionIndex = () => + findPropertyIndex('description', currentDocsProperty.value as ObjectExpression); + + if (findDescriptionIndex() === -1) { + currentDocsProperty.value.properties.push( + createProperty('description', createObjectExpression([])) + ); + } + + const currentDescriptionProperty = currentDocsProperty.value.properties[ + findDescriptionIndex() + ] as Property; + + if (currentDescriptionProperty.value.type !== 'ObjectExpression') { + // TODO: Update error message + throw new Error('Invalid schema'); + } + + const findStoryIndexIndex = () => + findPropertyIndex('story', currentDescriptionProperty.value as ObjectExpression); + + if (findStoryIndexIndex() !== -1) { + logger.warn( + `One of component(s) already has explictly set description. Ignoring the HTML comment above. Stories file: ${filename}` + ); + + return; + } + + currentDescriptionProperty.value.properties.push( + createProperty('story', { + type: 'Literal', + value: dedent(comment.data), + }) + ); + + currentDocsProperty.value.properties[findDescriptionIndex()] = currentDescriptionProperty; +} + +function insertSourceCode({ + component, + currentDocsProperty, + filename, + originalCode, +}: { + component: Component; + currentDocsProperty: Property; + filename?: string; + originalCode: string; +}) { + if (currentDocsProperty.value.type !== 'ObjectExpression') { + // TODO: Update error message + throw new Error('Invalid schema'); + } + + const findSourceIndex = () => + findPropertyIndex('source', currentDocsProperty.value as ObjectExpression); + + if (findSourceIndex() === -1) { + currentDocsProperty.value.properties.push(createProperty('source', createObjectExpression([]))); + } + + const currentSourceProperty = currentDocsProperty.value.properties[findSourceIndex()] as Property; + + if (currentSourceProperty.value.type !== 'ObjectExpression') { + // TODO: Update error message + throw new Error('Invalid schema'); + } + + const findStoryIndex = () => + findPropertyIndex('story', currentSourceProperty.value as ObjectExpression); + + if (findStoryIndex() !== -1) { + // TODO: That's not unepected, curious or anything. No need for a warning, right? + return; + } + + const { fragment, start, end } = component; + // TODO: From there we need to start the logic for filling Story's children raw source. + // Temporarily used the whole raw soure fragment. + const { nodes } = fragment; + const codeValue = originalCode.slice(start, end); + + currentSourceProperty.value.properties.push( + createProperty('code', { + type: 'Literal', + value: codeValue, + }) + ); + + currentDocsProperty.value.properties[findStoryIndex()] = currentSourceProperty; +} diff --git a/src/compiler/transform/Story/source.ts b/src/compiler/transform/Story/source.ts deleted file mode 100644 index 83f197ae..00000000 --- a/src/compiler/transform/Story/source.ts +++ /dev/null @@ -1,214 +0,0 @@ -/** - * WARN: This is likely to be revamped/removed, as it stands out from the CSF format. - * Ref: https://github.com/storybookjs/addon-svelte-csf/pull/181#discussion_r1614834092 - * - * TODO: If we decide to preserve this 'feature', then we can refactor this module. - * For the time being in restoring this feature, I didn't see a good reason to deal with duplicate code. - */ -import { logger } from '@storybook/client-logger'; -import type { ObjectExpression, Property, Statement } from 'estree'; -import { toJs } from 'estree-util-to-js'; -import type MagicString from 'magic-string'; -import type { Attribute } from 'svelte/compiler'; - -import type { extractStoriesNodesFromExportDefaultFn } from '../../../parser/extract/compiled/stories.js'; -import type { SvelteASTNodes } from '../../../parser/extract/svelte/nodes.js'; -import { extractStoryAttributesNodes } from '../../../parser/extract/svelte/Story/attributes.js'; - -interface Params { - code: MagicString; - nodes: { - svelte: SvelteASTNodes['storyComponents'][number]; - compiled: Awaited>[number]; - }; - filename: string; -} - -export function moveSourceAttributeToParameters(params: Params) { - const { code, nodes, filename } = params; - const { svelte, compiled } = nodes; - const { component } = svelte; - - // @ts-expect-error FIXME: I know, I know... this doesn't exist in the `StoryObj` object. - const { source }: { source: Attribute | undefined } = extractStoryAttributesNodes({ - component, - // @ts-expect-error FIXME: I know, I know... this doesn't exist in the `StoryObj` object. - attributes: ['source'], - }); - - if (!source) { - return; - } - - const { value } = source; - - if (value === true) { - // TODO: Should we throw or just warn? - logger.warn(`Invalid value for attribute 'source', expected string. Stories file: ${filename}`); - return; - } - - let rawSource: string; - - if (value[0].type === 'Text') { - rawSource = value[0].data; - } else if ( - value[0].type === 'ExpressionTag' && - value[0].expression.type === 'Literal' && - typeof value[0].expression.value === 'string' - ) { - rawSource = value[0].expression.value; - } else { - // TODO: Should we throw or just warn? - logger.warn(`Invalid value for attribute 'source', expected string. Stories file: ${filename}`); - return; - } - - const compiledPropsObjectExpression = compiled.arguments[1]; - - if (compiledPropsObjectExpression.type !== 'ObjectExpression') { - throw new Error(`Invalid`); - } - - const newCodeProperty = createProperty('code', { - type: 'Literal', - value: rawSource, - }); - const newSourceProperty = createProperty('source', createObjectExpression([newCodeProperty])); - const newDocsProperty = createProperty('docs', createObjectExpression([newSourceProperty])); - const newParametersProperty = createProperty( - 'parameters', - createObjectExpression([newDocsProperty]) - ); - - const currentParametersPropertyIndex = findPropertyIndex( - 'parameters', - compiledPropsObjectExpression - ); - - if (currentParametersPropertyIndex === -1) { - compiledPropsObjectExpression.properties.push(newParametersProperty); - compiled.arguments[1] = compiledPropsObjectExpression; - - return updateCompiledNode(code, compiled); - } - - const currentParametersProperty = - compiledPropsObjectExpression.properties[currentParametersPropertyIndex]; - - if ( - currentParametersProperty.type !== 'Property' || - currentParametersProperty.value.type !== 'ObjectExpression' - ) { - // TODO: Update error message - throw new Error(`Invalid schema`); - } - - const currentDocsPropertyIndex = findPropertyIndex( - 'docs', - currentParametersProperty.value[0].expression - ); - - if (currentDocsPropertyIndex === -1) { - currentParametersProperty.value.properties.push(newDocsProperty); - compiledPropsObjectExpression.properties[currentParametersPropertyIndex] = - currentParametersProperty; - compiled.arguments[1] = compiledPropsObjectExpression; - - return updateCompiledNode(code, compiled); - } - - const currentDocsProperty = currentParametersProperty.value.properties[currentDocsPropertyIndex]; - - if ( - currentDocsProperty.type !== 'Property' || - currentDocsProperty.value.type !== 'ObjectExpression' - ) { - // TODO: Update error message - throw new Error(`Invalid schema`); - } - - const currentSourcePropertyIndex = findPropertyIndex('source', currentDocsProperty.value); - - if (currentSourcePropertyIndex === -1) { - currentDocsProperty.value.properties.push(newSourceProperty); - currentParametersProperty.value.properties[currentDocsPropertyIndex] = currentDocsProperty; - compiledPropsObjectExpression.properties[currentParametersPropertyIndex] = - currentParametersProperty; - compiled.arguments[1] = compiledPropsObjectExpression; - - return updateCompiledNode(code, compiled); - } - - const currentSourceProperty = currentDocsProperty.value.properties[currentDocsPropertyIndex]; - - if ( - currentSourceProperty.type !== 'Property' || - currentSourceProperty.value.type !== 'ObjectExpression' - ) { - // TODO: Update error message - throw new Error(`Invalid schema`); - } - - const currentCodePropertyIndex = findPropertyIndex('code', currentDocsProperty.value); - - if (currentCodePropertyIndex !== -1) { - logger.warn( - ` already has explictly set 'source' for the component in its parameters. Ignoring the 'source' prop. Stories file: ${filename}` - ); - - return; - } - - currentSourceProperty.value.properties.push(newCodeProperty); - currentDocsProperty.value.properties[currentSourcePropertyIndex] = currentDocsProperty; - currentParametersProperty.value.properties[currentDocsPropertyIndex] = currentDocsProperty; - compiledPropsObjectExpression.properties[currentParametersPropertyIndex] = - currentParametersProperty; - compiled.arguments[1] = compiledPropsObjectExpression; - - return updateCompiledNode(code, compiled); -} - -function updateCompiledNode(code: MagicString, node: Params['nodes']['compiled']) { - // @ts-expect-error FIXME: These keys exists at runtime, perhaps I missed some type extension from `svelte/compiler`? - const { start, end } = node; - - code.update( - start, - end, - toJs({ - type: 'Program', - sourceType: 'module', - body: [node as unknown as Statement], - }).value - ); -} - -function createProperty(name: string, value: Property['value']): Property { - return { - type: 'Property', - kind: 'init', - computed: false, - method: false, - shorthand: false, - key: { - type: 'Identifier', - name, - }, - value, - }; -} - -function createObjectExpression(properties: Property[]): ObjectExpression { - return { - type: 'ObjectExpression', - properties, - }; -} - -function findPropertyIndex(name: string, node: ObjectExpression) { - return node.properties.findIndex( - (p) => p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === name - ); -}