UNPKG

@storybook/addon-svelte-csf

Version:
249 lines (248 loc) 11.6 kB
import pkg from '@storybook/addon-svelte-csf/package.json' with { type: 'json' }; import { transformComponentMetaToDefineMeta } from './codemods/component-meta-to-define-meta.js'; import { transformExportMetaToDefineMeta } from './codemods/export-const-to-define-meta.js'; import { transformImportDeclaration } from './codemods/import-declaration.js'; import { transformLegacyStory } from './codemods/legacy-story.js'; import { transformTemplateToSnippet } from './codemods/template-to-snippet.js'; import { createASTScript } from '../../parser/ast.js'; import { DuplicatedUnidentifiedTemplateError } from '../../utils/error/legacy-api/index.js'; export async function codemodLegacyNodes(params) { const { walk } = await import('zimmerframe'); const { ast, filename } = params; const state = { componentIdentifierName: {}, }; let transformedAst = walk(ast, state, { _(_node, context) { const { next, state } = context; next(state); }, Root(node, context) { const { fragment, instance, module, ...rest } = node; const { state, visit } = context; // NOTE: We walk on instance first to see if the package import declaration is there or `export const meta` const transformedInstance = (instance ? visit(instance, state) : null); // NOTE: We will create in 'clean-up' walk if it wasn't there const transformedModule = module ? visit(module, state) : null; const transformedFragment = visit(fragment, state); return { ...rest, fragment: transformedFragment, instance: transformedInstance, module: transformedModule, }; }, // NOTE: It walks either on module or instance tag Script(node, context) { const { next, state } = context; state.currentScript = node.context === 'module' ? 'module' : 'instance'; next(state); }, Program(_node, context) { const { next, state } = context; next(state); }, ImportDeclaration(node, context) { const { state } = context; if (node.source.value === pkg.name) { state.componentIdentifierName = getComponentsIdentifiersNames(node.specifiers); const transformed = transformImportDeclaration({ node, filename }); if (state.currentScript !== 'module') { // NOTE: We store current node in AST walker state. // And will remove it from instance & append it to module in the "clean-up" walk after this one. state.pkgImportDeclaration = transformed; } return transformed; } return node; }, ExportNamedDeclaration(node, context) { const { declaration } = node; const { state } = context; if (!(declaration && declaration.type === 'VariableDeclaration' && declaration.declarations[0].type === 'VariableDeclarator' && declaration.declarations[0].id.type === 'Identifier' && declaration.declarations[0].id.name === 'meta')) { return; } const transformed = transformExportMetaToDefineMeta(node); const { currentScript } = state; if (currentScript === 'instance') { state.defineMetaFromExportConstMeta = transformed; } const { init } = declaration.declarations[0]; // NOTE: // Type assertion because we already ran transform codemod function by this point (`transformed`). // So the possible issue is on our side, not user. const { properties } = init; for (const property of properties) { if (property.type === 'Property' && property.key.name === 'component' && property.value.type === 'Identifier') { state.storiesComponentIdentifier = property.value; } } return transformed; }, Fragment(_node, context) { const { next, state } = context; next(state); }, Comment(node, context) { const { state } = context; state.componentMetaHtmlComment = node; }, Component(node, context) { const { name } = node; const { state } = context; const { componentIdentifierName, componentMetaHtmlComment } = state; if (name === componentIdentifierName?.Meta) { state.defineMetaFromComponentMeta = transformComponentMetaToDefineMeta({ component: node, comment: componentMetaHtmlComment, }); } if (name === componentIdentifierName?.Story) { state.componentMetaHtmlComment = undefined; return transformLegacyStory({ component: node, filename, state }); } if (name === componentIdentifierName?.Template) { state.componentMetaHtmlComment = undefined; const isIdentifiedTemplate = node.attributes.some((attr) => attr.type === 'Attribute' && attr.name === 'id'); if (!isIdentifiedTemplate) { // NOTE: Stories file already have one idless 'Template' component. if (state.unidentifiedTemplateComponent) { throw new DuplicatedUnidentifiedTemplateError(filename); } state.unidentifiedTemplateComponent = node; } return transformTemplateToSnippet({ component: node }); } }, }); // Clean-up // TODO: To optimize it (stop walking on AST again)... // it might be possible to use `visit(path(-1))` for some cases in the previous AST walk, // but I haven't managed to get it to work properly transformedAst = walk(transformedAst, state, { Root(node, context) { let { fragment, instance, module, ...rest } = node; const { state, visit } = context; // NOTE: At this point, we decide for walker where it should walk first instead of using `next(state)` instance = instance ? visit(instance, state) : null; if (!module) { module = createASTScript({ module: true, content: { type: 'Program', body: [], sourceType: 'module', }, }); } visit(module, state); fragment = visit(fragment, state); // NOTE: Remove if body is empty if (instance && instance.content.body.length === 0) { instance = null; } return { ...rest, module, instance, fragment, }; }, Script(node, context) { let { content, context: scriptContext, ...rest } = node; const { state, visit } = context; state.currentScript = scriptContext === 'module' ? 'module' : 'instance'; content = visit(content, state); return { ...rest, content, context: scriptContext }; }, Program(node, context) { if (context.state.pkgImportDeclaration && context.state.currentScript === 'instance') { let instanceBody = []; for (const declaration of node.body) { if (declaration.type === 'ImportDeclaration') { if (declaration.source.value === pkg.name) { continue; } const { storiesComponentIdentifier } = context.state; const storiesComponentSpecifier = declaration.specifiers.find((s) => s.local.name === storiesComponentIdentifier?.name); if (storiesComponentSpecifier) { // NOTE: We are storing it in the state, so later we will move from instance tag to module tag state.storiesComponentImportDeclaration = declaration; continue; } } if (declaration.type === 'VariableDeclaration') { const { init } = declaration.declarations[0]; if (init?.type === 'CallExpression' && 'name' in init.callee && init.callee.name === 'defineMeta') { continue; } } instanceBody.push(declaration); } node.body = instanceBody; } if (state.currentScript === 'module') { if (state.storiesComponentImportDeclaration) { node.body.unshift(state.storiesComponentImportDeclaration); } if (state.pkgImportDeclaration) { node.body.unshift(state.pkgImportDeclaration); } if (state.defineMetaFromExportConstMeta) { node.body.push(state.defineMetaFromExportConstMeta); } if (state.defineMetaFromComponentMeta) { node.body.push(state.defineMetaFromComponentMeta); } } return { ...node, body: node.body }; }, Fragment(node, context) { const { state, stop } = context; const { componentIdentifierName, defineMetaFromComponentMeta } = state; if (defineMetaFromComponentMeta) { let { nodes, ...rest } = node; const componentMetaIndex = nodes.findIndex((node) => node.type === 'Component' && node.name === componentIdentifierName.Meta); if (componentMetaIndex !== -1) { // NOTE: Removes Meta component AST nodes.splice(componentMetaIndex, 1); if (nodes[componentMetaIndex - 1]?.type === 'Comment') { // NOTE: Removes leading Comment nodes.splice(componentMetaIndex - 1, 1); } if (nodes[componentMetaIndex - 1]?.type === 'Text' && nodes[componentMetaIndex - 2]?.type === 'Comment') { // NOTE: Remove leading Comment - sometimes there might a new line and indent - aka `\n ` nodes.splice(componentMetaIndex - 2, 2); } } return { ...rest, nodes }; } stop(); }, }); return transformedAst; } function getComponentsIdentifiersNames(specifiers) { let results = {}; for (const specifier of specifiers) { if (specifier.imported.name === 'Meta') { results.Meta = specifier.local.name; } if (specifier.imported.name === 'Story') { results.Story = specifier.local.name; } if (specifier.imported.name === 'Template') { results.Template = specifier.local.name; } } return results; }