@storybook/codemod
Version:
A collection of codemod scripts written with JSCodeshift
198 lines (194 loc) • 9.51 kB
JavaScript
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
};