UNPKG

@storybook/addon-svelte-csf

Version:
268 lines (267 loc) 12.1 kB
import fs from 'node:fs/promises'; import pkg from '@storybook/addon-svelte-csf/package.json' with { type: 'json' }; import { preprocess } from 'svelte/compiler'; import { getSvelteAST } from '../parser/ast.js'; import { extractStoryAttributesNodes } from '../parser/extract/svelte/story/attributes.js'; import { getStoryIdentifiers } from '../parser/analyse/story/attributes/identifiers.js'; import { getArrayOfStringsValueFromAttribute, getStringValueFromAttribute, } from '../parser/analyse/story/attributes.js'; import { getPropertyArrayOfStringsValue, getPropertyStringValue, } from '../parser/analyse/define-meta/properties.js'; import { DefaultOrNamespaceImportUsedError, GetDefineMetaFirstArgumentError, MissingModuleTagError, NoStoryComponentDestructuredError, } from '../utils/error/parser/extract/svelte.js'; import { NoDestructuredDefineMetaCallError } from '../utils/error/parser/analyse/define-meta.js'; import { StoryTemplateAndChildrenError, StoryTemplateAndAsChildError, StoryAsChildWithoutChildrenError, } from '../utils/error/parser/analyse/story.js'; import { extractStoryTemplateSnippetBlock } from '../parser/extract/svelte/story/template.js'; export async function parseForIndexer(filename, options) { let [code, { walk }, { loadSvelteConfig }] = await Promise.all([ fs.readFile(filename, { encoding: 'utf8' }), import('zimmerframe'), import('@sveltejs/vite-plugin-svelte'), ]); const { legacyTemplate } = options; const svelteConfig = await loadSvelteConfig(); if (svelteConfig?.preprocess) { code = (await preprocess(code, svelteConfig.preprocess, { filename: filename, })).code; } const svelteAST = getSvelteAST({ code, filename }); let results = { meta: {}, stories: [], isLegacy: false, }; let foundMeta = false; walk(svelteAST, results, { _(_node, context) { const { next, state } = context; next(state); }, Root(node, context) { const { fragment, // TODO: Remove it in the next major version instance, module, } = node; const { state, visit } = context; // TODO: Remove it in the next major version if (legacyTemplate && instance) { visit(instance, state); } if (module) { visit(module, state); } else if (!legacyTemplate) { throw new MissingModuleTagError(filename); } visit(fragment, state); }, // NOTE: We walk on instance (if flag was enabled - `Root` handles it) or module Script(node, context) { const { content } = node; const { state, visit } = context; visit(content, state); }, Program(node, context) { const { body } = node; const { state, visit } = context; for (const statement of body) { if (statement.type === 'ImportDeclaration' && statement.source.value === pkg.name) { visit(statement, state); } if (statement.type === 'VariableDeclaration') { visit(statement, state); } // TODO: Remove it in the next major version if (legacyTemplate && statement.type === 'ExportNamedDeclaration') { const { declaration } = statement; if (declaration?.type === 'VariableDeclaration') { visit(declaration, state); } } } }, ImportDeclaration(node, context) { const { specifiers } = node; const { state } = context; for (const specifier of specifiers) { if (specifier.type !== 'ImportSpecifier') { throw new DefaultOrNamespaceImportUsedError(filename); } if (!('name' in specifier.imported)) { return; } if (specifier.imported.name === 'defineMeta') { state.defineMetaImport = specifier; } // TODO: Remove it in the next major version if (legacyTemplate && specifier.imported.name === 'Meta') { state.legacyMetaImport = specifier; state.isLegacy = true; } // TODO: Remove it in the next major version if (legacyTemplate && specifier.imported.name === 'Story') { state.legacyStoryImport = specifier; state.isLegacy = true; } } }, VariableDeclaration(node, context) { const { declarations } = node; const { state, visit } = context; const { id, init } = declarations[0]; if (init?.type === 'CallExpression') { const { arguments: arguments_, callee } = init; if (callee.type === 'Identifier' && callee.name === state.defineMetaImport?.local.name) { foundMeta = true; if (id?.type !== 'ObjectPattern') { throw new NoDestructuredDefineMetaCallError({ defineMetaVariableDeclarator: declarations[0], filename, }); } const { properties } = id; const destructuredStoryIdentifier = properties.find((property) => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === 'Story'); if (!destructuredStoryIdentifier) { throw new NoStoryComponentDestructuredError({ filename, defineMetaImport: state.defineMetaImport, }); } state.defineMetaStory = destructuredStoryIdentifier.value; if (arguments_[0].type !== 'ObjectExpression') { throw new GetDefineMetaFirstArgumentError({ filename, defineMetaVariableDeclaration: node, }); } visit(arguments_[0], state); } } // TODO: Remove in the next major version if (legacyTemplate && !foundMeta && id.type === 'Identifier') { const { name } = id; if (name === 'meta' && init?.type === 'ObjectExpression') { foundMeta = true; visit(init, state); } } }, // NOTE: We assume this one is value of first argument passed to `defineMeta({ ... })` call, // or assigned value to legacy `export const meta = {}` ObjectExpression(node, context) { const { properties } = node; const { state, visit } = context; for (const property of properties) { if (property.type === 'Property' && property.key.type === 'Identifier') { visit(property, state); } } }, // NOTE: We assume these properties are from 'meta' _(from `defineMeta` or `export const meta`)_ object expression Property(node, context) { const { key } = node; const { state } = context; const { name } = key; if (name === 'title') { state.meta.title = getPropertyStringValue({ node, filename }); } if (name === 'tags') { state.meta.tags = getPropertyArrayOfStringsValue({ node, filename, }); } if (name === 'play') { state.meta.tags ??= []; state.meta.tags.push('play-fn'); } }, Fragment(node, context) { const { nodes } = node; const { state, visit } = context; for (const node of nodes) { if (node.type === 'Component') { visit(node, state); } } }, Component(node, context) { const { name } = node; const { state } = context; // TODO: Remove in the next major version if (!foundMeta && legacyTemplate && name === state.legacyMetaImport?.local.name) { const { attributes } = node; for (const attribute of attributes) { if (attribute.type === 'Attribute') { const { name } = attribute; if (name === 'title') { state.meta.title = getStringValueFromAttribute({ component: node, node: attribute, filename, }); } if (name === 'tags') { state.meta.tags = getArrayOfStringsValueFromAttribute({ component: node, node: attribute, filename, }); } if (name === 'play') { state.meta.tags ??= []; state.meta.tags.push('play-fn'); } } } } if (state.defineMetaStory?.name === name || // TODO: Remove in the next major version (legacyTemplate && name === state.legacyStoryImport?.local.name)) { const storyAttributes = extractStoryAttributesNodes({ component: node, attributes: ['exportName', 'name', 'tags', 'template', 'asChild', 'children', 'play'], }); const templateSnippet = extractStoryTemplateSnippetBlock(node); const hasChildren = storyAttributes.children || node.fragment.nodes.length > 0; const hasTemplate = storyAttributes.template || templateSnippet; const hasAsChild = storyAttributes.asChild !== undefined; // TODO: This could actually work in the future, by supporting referencing a template // and forwarding any children to that. if (storyAttributes.template && hasChildren) { throw new StoryTemplateAndChildrenError({ component: node, filename }); } if (hasTemplate && hasAsChild) { throw new StoryTemplateAndAsChildError({ component: node, filename }); } if (hasAsChild && !hasChildren) { throw new StoryAsChildWithoutChildrenError({ component: node, filename }); } const { exportName, name: storyName } = getStoryIdentifiers({ component: node, nameNode: storyAttributes.name, exportNameNode: storyAttributes.exportName, filename, }); const tags = getArrayOfStringsValueFromAttribute({ component: node, node: storyAttributes.tags, filename, }); if (storyAttributes.play !== undefined) { tags.push('play-fn'); } state.stories.push({ exportName, name: storyName, tags, }); } }, }); const { meta, stories, isLegacy } = results; return { meta, stories, isLegacy, }; }