UNPKG

@storybook/codemod

Version:

A collection of codemod scripts written with JSCodeshift

198 lines (194 loc) • 9.51 kB
import CJS_COMPAT_NODE_URL_94b6aurt4b from 'node:url'; import CJS_COMPAT_NODE_PATH_94b6aurt4b from 'node:path'; import CJS_COMPAT_NODE_MODULE_94b6aurt4b from "node:module"; var __filename = CJS_COMPAT_NODE_URL_94b6aurt4b.fileURLToPath(import.meta.url); var __dirname = CJS_COMPAT_NODE_PATH_94b6aurt4b.dirname(__filename); var require = CJS_COMPAT_NODE_MODULE_94b6aurt4b.createRequire(import.meta.url); // ------------------------------------------------------------ // end of CJS compatibility banner, injected by Storybook's esbuild configuration // ------------------------------------------------------------ import { upgradeDeprecatedTypes } from "../_node-chunks/chunk-IMC5VE2V.js"; // src/transforms/csf-2-to-3.ts import { core as babel, types as t } from "storybook/internal/babel"; import { loadCsf, printCsf } from "storybook/internal/csf-tools"; import { logger } from "storybook/internal/node-logger"; import prettier from "prettier"; import invariant from "tiny-invariant"; var { isIdentifier, isTSTypeAnnotation, isTSTypeReference } = t, renameAnnotation = (annotation) => annotation === "storyName" ? "name" : annotation, getTemplateBindVariable = (init) => t.isCallExpression(init) && t.isMemberExpression(init.callee) && t.isIdentifier(init.callee.object) && t.isIdentifier(init.callee.property) && init.callee.property.name === "bind" && (init.arguments.length === 0 || init.arguments.length === 1 && t.isObjectExpression(init.arguments[0]) && init.arguments[0].properties.length === 0) ? init.callee.object.name : null, isStoryAnnotation = (stmt, objectExports) => t.isExpressionStatement(stmt) && t.isAssignmentExpression(stmt.expression) && t.isMemberExpression(stmt.expression.left) && t.isIdentifier(stmt.expression.left.object) && objectExports[stmt.expression.left.object.name], getNewExport = (stmt, objectExports) => { if (t.isExportNamedDeclaration(stmt) && t.isVariableDeclaration(stmt.declaration) && stmt.declaration.declarations.length === 1) { let decl = stmt.declaration.declarations[0]; if (t.isVariableDeclarator(decl) && t.isIdentifier(decl.id)) return objectExports[decl.id.name]; } return null; }, isReactGlobalRenderFn = (csf, storyFn) => { if (csf._meta?.component && t.isArrowFunctionExpression(storyFn) && storyFn.params.length === 1 && t.isJSXElement(storyFn.body)) { let { openingElement } = storyFn.body; if (openingElement.selfClosing && t.isJSXIdentifier(openingElement.name) && openingElement.attributes.length === 1) { let attr = openingElement.attributes[0], param = storyFn.params[0]; if (t.isJSXSpreadAttribute(attr) && t.isIdentifier(attr.argument) && t.isIdentifier(param) && param.name === attr.argument.name && csf._meta.component === openingElement.name.name) return !0; } } return !1; }, isSimpleCSFStory = (init, annotations) => annotations.length === 0 && t.isArrowFunctionExpression(init) && init.params.length === 0; function removeUnusedTemplates(csf) { Object.entries(csf._templates).forEach(([template, templateExpression]) => { let references = []; if (babel.traverse(csf._ast, { Identifier: (path) => { path.node.name === template && references.push(path); } }), references.length === 1) { let reference = references[0]; reference.parentPath?.isVariableDeclarator() && reference.parentPath.node.init === templateExpression && reference.parentPath.remove(); } }); } async function transform(info, api, options) { let makeTitle = (userTitle) => userTitle || "FIXME", csf = loadCsf(info.source, { makeTitle }); try { csf.parse(); } catch (err) { return logger.log(`Error ${err}, skipping`), info.source; } let file = new babel.File( { filename: info.path }, { code: info.source, ast: csf._ast } ), importHelper = new StorybookImportHelper(file, info), objectExports = {}; Object.entries(csf._storyExports).forEach(([key, decl]) => { let annotations = Object.entries(csf._storyAnnotations[key]).map(([annotation, val]) => t.objectProperty(t.identifier(renameAnnotation(annotation)), val)); if (t.isVariableDeclarator(decl)) { let { init, id } = decl; invariant(init, "Inital value should be declared"); let template = getTemplateBindVariable(init); if (!t.isArrowFunctionExpression(init) && !template) return; if (isSimpleCSFStory(init, annotations)) { objectExports[key] = t.exportNamedDeclaration( t.variableDeclaration("const", [ t.variableDeclarator(importHelper.updateTypeTo(id, "StoryFn"), init) ]) ); return; } let storyFn = template ? t.identifier(template) : init, renderAnnotation = isReactGlobalRenderFn( csf, template ? csf._templates[template] : storyFn ) ? [] : [t.objectProperty(t.identifier("render"), storyFn)]; objectExports[key] = t.exportNamedDeclaration( t.variableDeclaration("const", [ t.variableDeclarator( importHelper.updateTypeTo(id, "StoryObj"), t.objectExpression([...renderAnnotation, ...annotations]) ) ]) ); } }), csf._ast.program.body = csf._ast.program.body.reduce((acc, stmt) => { let statement = stmt; if (isStoryAnnotation(statement, objectExports)) return acc; let newExport = getNewExport(statement, objectExports); return newExport ? (acc.push(newExport), acc) : (acc.push(statement), acc); }, []), upgradeDeprecatedTypes(file), importHelper.removeDeprecatedStoryImport(), removeUnusedTemplates(csf); let output = printCsf(csf).code; try { output = await prettier.format(output, { ...await prettier.resolveConfig(info.path), filepath: info.path }); } catch { logger.log(`Failed applying prettier to ${info.path}.`); } return output; } var StorybookImportHelper = class { constructor(file, info) { this.getAllSbImportDeclarations = (file) => { let found = []; return file.path.traverse({ ImportDeclaration: (path) => { let source = path.node.source.value; if (source.startsWith("@storybook/csf") || !source.startsWith("@storybook")) return; path.get("specifiers").some((specifier) => { if (specifier.isImportNamespaceSpecifier()) throw new Error( `This codemod does not support namespace imports for a ${path.node.source.value} package. Replace the namespace import with named imports and try again.` ); if (!specifier.isImportSpecifier()) return !1; let imported = specifier.get("imported"); return Array.isArray(imported) ? imported.some((importedSpecifier) => importedSpecifier.isIdentifier() ? [ "Story", "StoryFn", "StoryObj", "Meta", "ComponentStory", "ComponentStoryFn", "ComponentStoryObj", "ComponentMeta" ].includes(importedSpecifier.node.name) : !1) : imported.isIdentifier() ? [ "Story", "StoryFn", "StoryObj", "Meta", "ComponentStory", "ComponentStoryFn", "ComponentStoryObj", "ComponentMeta" ].includes(imported.node.name) : !1; }) && found.push(path); } }), found; }; this.getOrAddImport = (type) => { let sbImport = this.sbImportDeclarations.find((path) => path.node.importKind === "type") ?? this.sbImportDeclarations[0]; if (sbImport == null) return; let specifiers = sbImport.get("specifiers"), importSpecifier = specifiers.find((specifier) => { if (!specifier.isImportSpecifier()) return !1; let imported = specifier.get("imported"); return imported.isIdentifier() ? imported.node.name === type : !1; }); return importSpecifier ? importSpecifier.node.local.name : (specifiers[0].insertBefore(t.importSpecifier(t.identifier(type), t.identifier(type))), type); }; this.removeDeprecatedStoryImport = () => { this.sbImportDeclarations.flatMap((it) => it.get("specifiers")).filter((specifier) => { if (!specifier.isImportSpecifier()) return !1; let imported = specifier.get("imported"); return imported.isIdentifier() ? imported.node.name === "Story" : !1; }).forEach((path) => path.remove()); }; this.getAllLocalImports = () => this.sbImportDeclarations.flatMap((it) => it.get("specifiers")).map((it) => it.node.local.name); this.updateTypeTo = (id, type) => { if (isIdentifier(id) && isTSTypeAnnotation(id.typeAnnotation) && isTSTypeReference(id.typeAnnotation.typeAnnotation) && isIdentifier(id.typeAnnotation.typeAnnotation.typeName)) { let { name } = id.typeAnnotation.typeAnnotation.typeName; if (this.getAllLocalImports().includes(name)) { let localTypeImport = this.getOrAddImport(type); return { ...id, typeAnnotation: t.tsTypeAnnotation( t.tsTypeReference( t.identifier(localTypeImport ?? ""), id.typeAnnotation.typeAnnotation.typeParameters ) ) }; } } return id; }; this.sbImportDeclarations = this.getAllSbImportDeclarations(file); } }, parser = "tsx"; export { transform as default, parser };