storybook
Version:
Storybook: Develop, document, and test UI components in isolation
1,026 lines (1,014 loc) • 74.1 kB
JavaScript
import CJS_COMPAT_NODE_URL_yr66iw5gef from 'node:url';
import CJS_COMPAT_NODE_PATH_yr66iw5gef from 'node:path';
import CJS_COMPAT_NODE_MODULE_yr66iw5gef from "node:module";
var __filename = CJS_COMPAT_NODE_URL_yr66iw5gef.fileURLToPath(import.meta.url);
var __dirname = CJS_COMPAT_NODE_PATH_yr66iw5gef.dirname(__filename);
var require = CJS_COMPAT_NODE_MODULE_yr66iw5gef.createRequire(import.meta.url);
// ------------------------------------------------------------
// end of CJS compatibility banner, injected by Storybook's esbuild configuration
// ------------------------------------------------------------
import {
require_dist
} from "./chunk-76KIYDRU.js";
import {
__toESM
} from "./chunk-J4VC4I2M.js";
// src/csf-tools/CsfFile.ts
var import_ts_dedent = __toESM(require_dist(), 1);
import { readFile, writeFile } from "node:fs/promises";
import {
BabelFileClass,
babelParse,
generate,
recast,
types as t2,
traverse
} from "storybook/internal/babel";
import { isExportStory, storyNameFromExport, toId, toTestId } from "storybook/internal/csf";
import { logger } from "storybook/internal/node-logger";
// src/csf-tools/findVarInitialization.ts
import { types as t } from "storybook/internal/babel";
var findVarInitialization = (identifier, program) => {
let init = null, declarations = null;
return program.body.find((node) => (t.isVariableDeclaration(node) ? declarations = node.declarations : t.isExportNamedDeclaration(node) && t.isVariableDeclaration(node.declaration) && (declarations = node.declaration.declarations), declarations && declarations.find((decl) => t.isVariableDeclarator(decl) && t.isIdentifier(decl.id) && decl.id.name === identifier ? (init = decl.init, !0) : !1))), init;
};
// src/csf-tools/CsfFile.ts
var PREVIEW_FILE_REGEX = /\/preview(.(js|jsx|mjs|ts|tsx))?$/, isValidPreviewPath = (filepath) => PREVIEW_FILE_REGEX.test(filepath);
function parseIncludeExclude(prop) {
if (t2.isArrayExpression(prop))
return prop.elements.map((e) => {
if (t2.isStringLiteral(e))
return e.value;
throw new Error(`Expected string literal: ${e}`);
});
if (t2.isStringLiteral(prop))
return new RegExp(prop.value);
if (t2.isRegExpLiteral(prop))
return new RegExp(prop.pattern, prop.flags);
throw new Error(`Unknown include/exclude: ${prop}`);
}
function parseTags(prop) {
if (!t2.isArrayExpression(prop))
throw new Error("CSF: Expected tags array");
return prop.elements.map((e) => {
if (t2.isStringLiteral(e))
return e.value;
throw new Error("CSF: Expected tag to be string literal");
});
}
function parseTestTags(optionsNode, program) {
if (!optionsNode)
return [];
let node = optionsNode;
if (t2.isIdentifier(node) && (node = findVarInitialization(node.name, program)), t2.isObjectExpression(node)) {
let tagsProp = node.properties.find(
(property) => t2.isObjectProperty(property) && t2.isIdentifier(property.key) && property.key.name === "tags"
);
if (tagsProp) {
let tagsNode = tagsProp.value;
return t2.isIdentifier(tagsNode) && (tagsNode = findVarInitialization(tagsNode.name, program)), parseTags(tagsNode);
}
}
return [];
}
var formatLocation = (node, fileName) => {
let loc = "";
if (node.loc) {
let { line, column } = node.loc?.start || {};
loc = `(line ${line}, col ${column})`;
}
return `${fileName || ""} ${loc}`.trim();
}, isModuleMock = (importPath) => MODULE_MOCK_REGEX.test(importPath), isArgsStory = (init, parent, csf) => {
let storyFn = init;
if (t2.isCallExpression(init)) {
let { callee, arguments: bindArguments } = init;
if (t2.isProgram(parent) && t2.isMemberExpression(callee) && t2.isIdentifier(callee.object) && t2.isIdentifier(callee.property) && callee.property.name === "bind" && (bindArguments.length === 0 || bindArguments.length === 1 && t2.isObjectExpression(bindArguments[0]) && bindArguments[0].properties.length === 0)) {
let boundIdentifier = callee.object.name, template = findVarInitialization(boundIdentifier, parent);
template && (csf._templates[boundIdentifier] = template, storyFn = template);
}
}
return t2.isArrowFunctionExpression(storyFn) || t2.isFunctionDeclaration(storyFn) ? storyFn.params.length > 0 : !1;
}, parseExportsOrder = (init) => {
if (t2.isArrayExpression(init))
return init.elements.map((item) => {
if (t2.isStringLiteral(item))
return item.value;
throw new Error(`Expected string literal named export: ${item}`);
});
throw new Error(`Expected array of string literals: ${init}`);
}, sortExports = (exportByName, order) => order.reduce(
(acc, name) => {
let namedExport = exportByName[name];
return namedExport && (acc[name] = namedExport), acc;
},
{}
), hasMount = (play) => {
if (t2.isArrowFunctionExpression(play) || t2.isFunctionDeclaration(play) || t2.isObjectMethod(play)) {
let params = play.params;
if (params.length >= 1) {
let [arg] = params;
if (t2.isObjectPattern(arg))
return !!arg.properties.find((prop) => {
if (t2.isObjectProperty(prop) && t2.isIdentifier(prop.key))
return prop.key.name === "mount";
});
}
}
return !1;
}, MODULE_MOCK_REGEX = /^[.\/#].*\.mock($|\.[^.]*$)/i, NoMetaError = class extends Error {
constructor(message, ast, fileName) {
let msg = message.trim();
super(import_ts_dedent.dedent`
CSF: ${msg} ${formatLocation(ast, fileName)}
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
`), this.name = this.constructor.name;
}
}, MultipleMetaError = class extends Error {
constructor(message, ast, fileName) {
let msg = `${message} ${formatLocation(ast, fileName)}`.trim();
super(import_ts_dedent.dedent`
CSF: ${message} ${formatLocation(ast, fileName)}
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
`), this.name = this.constructor.name;
}
}, MixedFactoryError = class extends Error {
constructor(message, ast, fileName) {
let msg = `${message} ${formatLocation(ast, fileName)}`.trim();
super(import_ts_dedent.dedent`
CSF: ${message} ${formatLocation(ast, fileName)}
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
`), this.name = this.constructor.name;
}
}, BadMetaError = class extends Error {
constructor(message, ast, fileName) {
let msg = "".trim();
super(import_ts_dedent.dedent`
CSF: ${message} ${formatLocation(ast, fileName)}
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
`), this.name = this.constructor.name;
}
}, CsfFile = class {
constructor(ast, options, file) {
this._stories = {};
this._metaAnnotations = {};
this._storyExports = {};
this._storyDeclarationPath = {};
this._storyPaths = {};
this._storyStatements = {};
this._storyAnnotations = {};
this._templates = {};
this._tests = [];
this._ast = ast, this._file = file, this._options = options, this.imports = [];
}
_parseTitle(value) {
let node = t2.isIdentifier(value) ? findVarInitialization(value.name, this._ast.program) : value;
if (t2.isStringLiteral(node))
return node.value;
if (t2.isTSSatisfiesExpression(node) && t2.isStringLiteral(node.expression))
return node.expression.value;
throw new Error(import_ts_dedent.dedent`
CSF: unexpected dynamic title ${formatLocation(node, this._options.fileName)}
More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#string-literal-titles
`);
}
_parseMeta(declaration, program) {
if (this._metaNode)
throw new MultipleMetaError("multiple meta objects", declaration, this._options.fileName);
this._metaNode = declaration;
let meta = {};
declaration.properties.forEach((p) => {
if (t2.isIdentifier(p.key)) {
let value = t2.isObjectMethod(p) ? p : p.value;
if (this._metaAnnotations[p.key.name] = value, p.key.name === "title")
meta.title = this._parseTitle(p.value);
else if (["includeStories", "excludeStories"].includes(p.key.name))
meta[p.key.name] = parseIncludeExclude(p.value);
else if (p.key.name === "component") {
let n = p.value;
if (t2.isIdentifier(n)) {
let id = n.name, importStmt = program.body.find(
(stmt) => t2.isImportDeclaration(stmt) && stmt.specifiers.find((spec) => spec.local.name === id)
);
if (importStmt) {
let { source } = importStmt, specifier = importStmt.specifiers.find((spec) => spec.local.name === id);
t2.isStringLiteral(source) && specifier && (this._rawComponentPath = source.value, (t2.isImportSpecifier(specifier) || t2.isImportDefaultSpecifier(specifier)) && (this._componentImportSpecifier = specifier));
}
}
let { code } = recast.print(p.value, {});
meta.component = code;
} else if (p.key.name === "tags") {
let node = p.value;
t2.isIdentifier(node) && (node = findVarInitialization(node.name, this._ast.program)), meta.tags = parseTags(node);
} else if (p.key.name === "id")
if (t2.isStringLiteral(p.value))
meta.id = p.value.value;
else
throw new Error(`Unexpected component id: ${p.value}`);
}
}), this._meta = meta;
}
getStoryExport(key) {
let node = this._storyExports[key];
if (node = t2.isVariableDeclarator(node) ? node.init : node, t2.isCallExpression(node)) {
let { callee, arguments: bindArguments } = node;
if (t2.isMemberExpression(callee) && t2.isIdentifier(callee.object) && t2.isIdentifier(callee.property) && callee.property.name === "bind" && (bindArguments.length === 0 || bindArguments.length === 1 && t2.isObjectExpression(bindArguments[0]) && bindArguments[0].properties.length === 0)) {
let { name } = callee.object;
node = this._templates[name];
}
}
return node;
}
parse() {
let self = this;
if (traverse(this._ast, {
ExportDefaultDeclaration: {
enter(path) {
let { node, parent } = path, isVariableReference = t2.isIdentifier(node.declaration) && t2.isProgram(parent);
if (self._options.transformInlineMeta && !isVariableReference && t2.isExpression(node.declaration)) {
let metaId = path.scope.generateUidIdentifier("meta");
self._metaVariableName = metaId.name;
let nodes = [
t2.variableDeclaration("const", [t2.variableDeclarator(metaId, node.declaration)]),
t2.exportDefaultDeclaration(metaId)
];
nodes.forEach((_node) => _node.loc = path.node.loc), path.replaceWithMultiple(nodes);
return;
}
let metaNode, decl;
if (isVariableReference) {
let variableName = node.declaration.name;
self._metaVariableName = variableName;
let isMetaVariable = (declaration) => t2.isIdentifier(declaration.id) && declaration.id.name === variableName;
self._metaStatementPath = self._file.path.get("body").find(
(path2) => path2.isVariableDeclaration() && path2.node.declarations.some(isMetaVariable)
), self._metaStatement = self._metaStatementPath?.node, decl = (self?._metaStatement?.declarations || []).find(
isMetaVariable
)?.init;
} else
self._metaStatement = node, self._metaStatementPath = path, decl = node.declaration;
if (t2.isObjectExpression(decl) ? metaNode = decl : /* export default { ... } as Meta<...> */ /* export default { ... } satisfies Meta<...> */ (t2.isTSAsExpression(decl) || t2.isTSSatisfiesExpression(decl)) && t2.isObjectExpression(decl.expression) ? metaNode = decl.expression : (
// export default { ... } satisfies Meta as Meta<...>
t2.isTSAsExpression(decl) && t2.isTSSatisfiesExpression(decl.expression) && t2.isObjectExpression(decl.expression.expression) && (metaNode = decl.expression.expression)
), metaNode && t2.isProgram(parent) && self._parseMeta(metaNode, parent), self._metaStatement && !self._metaNode)
throw new NoMetaError(
"default export must be an object",
self._metaStatement,
self._options.fileName
);
self._metaPath = path;
}
},
ExportNamedDeclaration: {
enter(path) {
let { node, parent } = path, declaration = path.get("declaration"), declarations;
declaration.isVariableDeclaration() ? declarations = declaration.get("declarations").filter((d) => d.isVariableDeclarator()) : declaration.isFunctionDeclaration() && (declarations = [declaration]), declarations ? declarations.forEach((declPath) => {
let decl = declPath.node, id = declPath.node.id;
if (t2.isIdentifier(id)) {
let storyIsFactory = !1, { name: exportName } = id;
if (exportName === "__namedExportsOrder" && declPath.isVariableDeclarator()) {
self._namedExportsOrder = parseExportsOrder(declPath.node.init);
return;
}
self._storyExports[exportName] = decl, self._storyDeclarationPath[exportName] = declPath, self._storyPaths[exportName] = path, self._storyStatements[exportName] = node;
let name = storyNameFromExport(exportName);
self._storyAnnotations[exportName] ? logger.warn(
`Unexpected annotations for "${exportName}" before story declaration`
) : self._storyAnnotations[exportName] = {};
let storyNode;
if (t2.isVariableDeclarator(decl) ? t2.isTSAsExpression(decl.init) && t2.isTSSatisfiesExpression(decl.init.expression) ? storyNode = decl.init.expression.expression : t2.isTSAsExpression(decl.init) || t2.isTSSatisfiesExpression(decl.init) ? storyNode = decl.init.expression : storyNode = decl.init : storyNode = decl, t2.isCallExpression(storyNode) && t2.isMemberExpression(storyNode.callee) && t2.isIdentifier(storyNode.callee.property) && (storyNode.callee.property.name === "story" || storyNode.callee.property.name === "extend") && (storyIsFactory = !0, storyNode = storyNode.arguments[0]), self._metaIsFactory && !storyIsFactory)
throw new MixedFactoryError(
"expected factory story",
storyNode,
self._options.fileName
);
if (!self._metaIsFactory && storyIsFactory)
throw self._metaNode ? new MixedFactoryError(
"expected non-factory story",
storyNode,
self._options.fileName
) : new BadMetaError(
"meta() factory must be imported from .storybook/preview configuration",
storyNode,
self._options.fileName
);
let parameters = {};
t2.isObjectExpression(storyNode) ? (parameters.__isArgsStory = !0, storyNode.properties.forEach((p) => {
if (t2.isIdentifier(p.key)) {
let key = p.key.name;
if (t2.isObjectMethod(p))
self._storyAnnotations[exportName][key] = p;
else {
if (p.key.name === "render")
parameters.__isArgsStory = isArgsStory(
p.value,
parent,
self
);
else if (p.key.name === "name" && t2.isStringLiteral(p.value))
name = p.value.value;
else if (p.key.name === "storyName" && t2.isStringLiteral(p.value))
logger.warn(
`Unexpected usage of "storyName" in "${exportName}". Please use "name" instead.`
);
else if (p.key.name === "parameters" && t2.isObjectExpression(p.value)) {
let idProperty = p.value.properties.find(
(property) => t2.isObjectProperty(property) && t2.isIdentifier(property.key) && property.key.name === "__id"
);
idProperty && (parameters.__id = idProperty.value.value);
}
self._storyAnnotations[exportName][p.key.name] = p.value;
}
}
})) : parameters.__isArgsStory = isArgsStory(storyNode, parent, self), self._stories[exportName] = {
id: "FIXME",
name,
parameters,
__stats: {
factory: storyIsFactory
}
};
}
}) : node.specifiers.length > 0 && node.specifiers.forEach((specifier) => {
if (t2.isExportSpecifier(specifier) && t2.isIdentifier(specifier.exported)) {
let { name: exportName } = specifier.exported, { name: localName } = specifier.local, decl = t2.isProgram(parent) ? findVarInitialization(localName, parent) : specifier.local;
if (exportName === "default") {
let metaNode;
t2.isObjectExpression(decl) ? metaNode = decl : /* export default { ... } as Meta<...> */ /* export default { ... } satisfies Meta<...> */ (t2.isTSAsExpression(decl) || t2.isTSSatisfiesExpression(decl)) && t2.isObjectExpression(decl.expression) ? metaNode = decl.expression : (
// export default { ... } satisfies Meta as Meta<...>
t2.isTSAsExpression(decl) && t2.isTSSatisfiesExpression(decl.expression) && t2.isObjectExpression(decl.expression.expression) && (metaNode = decl.expression.expression)
), metaNode && t2.isProgram(parent) && self._parseMeta(metaNode, parent);
} else {
let annotations = {}, storyNode = decl;
t2.isObjectExpression(storyNode) && storyNode.properties.forEach((p) => {
t2.isIdentifier(p.key) && (annotations[p.key.name] = p.value);
}), self._storyAnnotations[exportName] = annotations, self._storyStatements[exportName] = decl, self._storyPaths[exportName] = path, self._stories[exportName] = {
id: "FIXME",
name: exportName,
localName,
parameters: {},
__stats: {}
};
}
}
});
}
},
ExpressionStatement: {
enter({ node, parent }) {
let { expression } = node;
if (t2.isProgram(parent) && t2.isAssignmentExpression(expression) && t2.isMemberExpression(expression.left) && t2.isIdentifier(expression.left.object) && t2.isIdentifier(expression.left.property)) {
let exportName = expression.left.object.name, annotationKey = expression.left.property.name, annotationValue = expression.right;
if (self._storyAnnotations[exportName] && (annotationKey === "story" && t2.isObjectExpression(annotationValue) ? annotationValue.properties.forEach((prop) => {
t2.isIdentifier(prop.key) && (self._storyAnnotations[exportName][prop.key.name] = prop.value);
}) : self._storyAnnotations[exportName][annotationKey] = annotationValue), annotationKey === "storyName" && t2.isStringLiteral(annotationValue)) {
let storyName = annotationValue.value, story = self._stories[exportName];
if (!story)
return;
story.name = storyName;
}
}
if (t2.isCallExpression(expression) && t2.isMemberExpression(expression.callee) && t2.isIdentifier(expression.callee.object) && t2.isIdentifier(expression.callee.property) && expression.callee.property.name === "test" && expression.arguments.length >= 2 && t2.isStringLiteral(expression.arguments[0])) {
let exportName = expression.callee.object.name, testName = expression.arguments[0].value, testFunction = expression.arguments.length === 2 ? expression.arguments[1] : expression.arguments[2], testArguments = expression.arguments.length === 2 ? null : expression.arguments[1], tags = parseTestTags(testArguments, self._ast.program);
self._tests.push({
function: testFunction,
name: testName,
node: expression,
// can't set id because meta title isn't available yet
// so it's set later on
id: "FIXME",
tags,
parent: { node: self._storyStatements[exportName] }
}), self._stories[exportName].__stats.tests = !0;
}
}
},
CallExpression: {
enter(path) {
let { node } = path, { callee } = node;
if (t2.isIdentifier(callee) && callee.name === "storiesOf")
throw new Error(import_ts_dedent.dedent`
Unexpected \`storiesOf\` usage: ${formatLocation(node, self._options.fileName)}.
SB8 does not support \`storiesOf\`.
`);
if (t2.isMemberExpression(callee) && t2.isIdentifier(callee.property) && callee.property.name === "meta" && t2.isIdentifier(callee.object) && node.arguments.length > 0) {
let configParent = path.scope.getBinding(callee.object.name)?.path?.parentPath?.node;
if (t2.isImportDeclaration(configParent))
if (isValidPreviewPath(configParent.source.value)) {
self._metaIsFactory = !0;
let metaDeclarator = path.findParent(
(p) => p.isVariableDeclarator()
);
self._metaVariableName = t2.isIdentifier(metaDeclarator.node.id) ? metaDeclarator.node.id.name : callee.property.name;
let metaNode = node.arguments[0];
self._parseMeta(metaNode, self._ast.program);
} else
throw new BadMetaError(
"meta() factory must be imported from .storybook/preview configuration",
configParent,
self._options.fileName
);
}
}
},
ImportDeclaration: {
enter({ node }) {
let { source } = node;
if (t2.isStringLiteral(source))
self.imports.push(source.value);
else
throw new Error("CSF: unexpected import source");
}
}
}), !self._meta)
throw new NoMetaError("missing default export", self._ast, self._options.fileName);
let entries = Object.entries(self._stories);
if (self._meta.title = this._options.makeTitle(self._meta?.title), self._metaAnnotations.play && (self._meta.tags = [...self._meta.tags || [], "play-fn"]), self._stories = entries.reduce(
(acc, [key, story]) => {
if (!isExportStory(key, self._meta))
return acc;
let id = story.parameters?.__id ?? toId(self._meta?.id || self._meta?.title, storyNameFromExport(key)), parameters = { ...story.parameters, __id: id }, { includeStories } = self._meta || {};
key === "__page" && (entries.length === 1 || Array.isArray(includeStories) && includeStories.length === 1) && (parameters.docsOnly = !0), acc[key] = { ...story, id, parameters };
let storyAnnotations = self._storyAnnotations[key], { tags, play } = storyAnnotations;
if (tags) {
let node = t2.isIdentifier(tags) ? findVarInitialization(tags.name, this._ast.program) : tags;
acc[key].tags = parseTags(node);
}
play && (acc[key].tags = [...acc[key].tags || [], "play-fn"]);
let stats = acc[key].__stats;
["play", "render", "loaders", "beforeEach", "globals", "tags"].forEach((annotation) => {
stats[annotation] = !!storyAnnotations[annotation] || !!self._metaAnnotations[annotation];
});
let storyExport = self.getStoryExport(key);
stats.storyFn = !!(t2.isArrowFunctionExpression(storyExport) || t2.isFunctionDeclaration(storyExport)), stats.mount = hasMount(storyAnnotations.play ?? self._metaAnnotations.play), stats.moduleMock = !!self.imports.find((fname) => isModuleMock(fname));
let storyNode = self._storyStatements[key], storyTests = self._tests.filter((t7) => t7.parent.node === storyNode);
return storyTests.length > 0 && (stats.tests = !0, storyTests.forEach((test) => {
test.id = toTestId(id, test.name);
})), acc;
},
{}
), Object.keys(self._storyExports).forEach((key) => {
isExportStory(key, self._meta) || (delete self._storyExports[key], delete self._storyAnnotations[key], delete self._storyStatements[key]);
}), self._namedExportsOrder) {
let unsortedExports = Object.keys(self._storyExports);
self._storyExports = sortExports(self._storyExports, self._namedExportsOrder), self._stories = sortExports(self._stories, self._namedExportsOrder);
let sortedExports = Object.keys(self._storyExports);
if (unsortedExports.length !== sortedExports.length)
throw new Error(
`Missing exports after sort: ${unsortedExports.filter(
(key) => !sortedExports.includes(key)
)}`
);
}
return self;
}
get meta() {
return this._meta;
}
get stories() {
return Object.values(this._stories);
}
getStoryTests(story) {
let storyNode = typeof story == "string" ? this._storyStatements[story] : story;
return storyNode ? this._tests.filter((t7) => t7.parent.node === storyNode) : [];
}
get indexInputs() {
let { fileName } = this._options;
if (!fileName)
throw new Error(
import_ts_dedent.dedent`Cannot automatically create index inputs with CsfFile.indexInputs because the CsfFile instance was created without a the fileName option.
Either add the fileName option when creating the CsfFile instance, or create the index inputs manually.`
);
let index = [];
return Object.entries(this._stories).map(([exportName, story]) => {
let tags = [...this._meta?.tags ?? [], ...story.tags ?? []], storyInput = {
importPath: fileName,
rawComponentPath: this._rawComponentPath,
exportName,
title: this.meta?.title,
metaId: this.meta?.id,
tags,
__id: story.id,
__stats: story.__stats
}, tests = this.getStoryTests(exportName), hasTests = tests.length > 0;
index.push({
...storyInput,
type: "story",
subtype: "story",
name: story.name
}), hasTests && tests.forEach((test) => {
index.push({
...storyInput,
// TODO implementent proper title => path behavior in `transformStoryIndexToStoriesHash`
// title: `${storyInput.title}/${story.name}`,
type: "story",
subtype: "test",
name: test.name,
parent: story.id,
parentName: story.name,
tags: [
...storyInput.tags,
// this tag comes before test tags so users can invert if they like
"!autodocs",
...test.tags,
// this tag comes after test tags so users can't change it
"test-fn"
],
__id: test.id
});
});
}), index;
}
}, babelParseFile = ({
code,
filename = "",
ast
}) => new BabelFileClass(
{ filename, highlightCode: !1 },
{ code, ast: ast ?? babelParse(code) }
), loadCsf = (code, options) => {
let ast = babelParse(code), file = babelParseFile({ code, filename: options.fileName, ast });
return new CsfFile(ast, options, file);
}, formatCsf = (csf, options = { sourceMaps: !1 }, code) => {
let result = generate(csf._ast, options, code);
return options.sourceMaps ? result : result.code;
}, printCsf = (csf, options = {}) => recast.print(csf._ast, options), readCsf = async (fileName, options) => {
let code = (await readFile(fileName, "utf-8")).toString();
return loadCsf(code, { ...options, fileName });
}, writeCsf = async (csf, fileName) => {
if (!(fileName || csf._options.fileName))
throw new Error("Please specify a fileName for writeCsf");
await writeFile(fileName, printCsf(csf).code);
};
// src/csf-tools/ConfigFile.ts
var import_ts_dedent2 = __toESM(require_dist(), 1);
import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
import {
babelParse as babelParse2,
generate as generate2,
recast as recast2,
types as t3,
traverse as traverse2
} from "storybook/internal/babel";
import { logger as logger2 } from "storybook/internal/node-logger";
var getCsfParsingErrorMessage = ({
expectedType,
foundType,
node
}) => import_ts_dedent2.dedent`
CSF Parsing error: Expected '${expectedType}' but found '${foundType}' instead in '${node?.type}'.
`, propKey = (p) => t3.isIdentifier(p.key) ? p.key.name : t3.isStringLiteral(p.key) ? p.key.value : null, _getPath = (path, node) => {
if (path.length === 0)
return node;
if (t3.isObjectExpression(node)) {
let [first, ...rest] = path, field = node.properties.find((p) => propKey(p) === first);
if (field)
return _getPath(rest, field.value);
}
}, _getPathProperties = (path, node) => {
if (path.length === 0) {
if (t3.isObjectExpression(node))
return node.properties;
throw new Error("Expected object expression");
}
if (t3.isObjectExpression(node)) {
let [first, ...rest] = path, field = node.properties.find((p) => propKey(p) === first);
if (field)
return rest.length === 0 ? node.properties : _getPathProperties(rest, field.value);
}
}, _findVarDeclarator = (identifier, program) => {
let declarator = null, declarations = null;
return program.body.find((node) => (t3.isVariableDeclaration(node) ? declarations = node.declarations : t3.isExportNamedDeclaration(node) && t3.isVariableDeclaration(node.declaration) && (declarations = node.declaration.declarations), declarations && declarations.find((decl) => t3.isVariableDeclarator(decl) && t3.isIdentifier(decl.id) && decl.id.name === identifier ? (declarator = decl, !0) : !1))), declarator;
}, _findVarInitialization = (identifier, program) => _findVarDeclarator(identifier, program)?.init, _makeObjectExpression = (path, value) => {
if (path.length === 0)
return value;
let [first, ...rest] = path, innerExpression = _makeObjectExpression(rest, value);
return t3.objectExpression([t3.objectProperty(t3.identifier(first), innerExpression)]);
}, _updateExportNode = (path, expr, existing) => {
let [first, ...rest] = path, existingField = existing.properties.find(
(p) => propKey(p) === first
);
existingField ? t3.isObjectExpression(existingField.value) && rest.length > 0 ? _updateExportNode(rest, expr, existingField.value) : existingField.value = _makeObjectExpression(rest, expr) : existing.properties.push(
t3.objectProperty(t3.identifier(first), _makeObjectExpression(rest, expr))
);
}, ConfigFile = class {
constructor(ast, code, fileName) {
this._exports = {};
// FIXME: this is a hack. this is only used in the case where the user is
// modifying a named export that's a scalar. The _exports map is not suitable
// for that. But rather than refactor the whole thing, we just use this as a stopgap.
this._exportDecls = {};
this.hasDefaultExport = !1;
/** Unwraps TS assertions/satisfies from a node, to get the underlying node. */
this._unwrap = (node) => t3.isTSAsExpression(node) || t3.isTSSatisfiesExpression(node) ? this._unwrap(node.expression) : node;
/**
* Resolve a declaration node by unwrapping TS assertions/satisfies and following identifiers to
* resolve the correct node in case it's an identifier.
*/
this._resolveDeclaration = (node, parent = this._ast.program) => {
let decl = this._unwrap(node);
if (t3.isIdentifier(decl) && t3.isProgram(parent)) {
let initialization = _findVarInitialization(decl.name, parent);
return initialization ? this._unwrap(initialization) : decl;
}
return decl;
};
this._ast = ast, this._code = code, this.fileName = fileName;
}
_parseExportsObject(exportsObject) {
this._exportsObject = exportsObject, exportsObject.properties.forEach((p) => {
let exportName = propKey(p);
if (exportName) {
let exportVal = this._resolveDeclaration(p.value);
this._exports[exportName] = exportVal;
}
});
}
parse() {
let self = this;
return traverse2(this._ast, {
ExportDefaultDeclaration: {
enter({ node, parent }) {
self.hasDefaultExport = !0;
let decl = self._resolveDeclaration(node.declaration, parent);
t3.isCallExpression(decl) && t3.isObjectExpression(decl.arguments[0]) && (decl = decl.arguments[0]), t3.isObjectExpression(decl) ? self._parseExportsObject(decl) : logger2.warn(
getCsfParsingErrorMessage({
expectedType: "ObjectExpression",
foundType: decl?.type,
node: decl || node.declaration
})
);
}
},
ExportNamedDeclaration: {
enter({ node, parent }) {
if (t3.isVariableDeclaration(node.declaration))
node.declaration.declarations.forEach((decl) => {
if (t3.isVariableDeclarator(decl) && t3.isIdentifier(decl.id)) {
let { name: exportName } = decl.id, exportVal = self._resolveDeclaration(decl.init, parent);
self._exports[exportName] = exportVal, self._exportDecls[exportName] = decl;
}
});
else if (t3.isFunctionDeclaration(node.declaration)) {
let decl = node.declaration;
if (t3.isIdentifier(decl.id)) {
let { name: exportName } = decl.id;
self._exportDecls[exportName] = decl;
}
} else node.specifiers ? node.specifiers.forEach((spec) => {
if (t3.isExportSpecifier(spec) && t3.isIdentifier(spec.local) && t3.isIdentifier(spec.exported)) {
let { name: localName } = spec.local, { name: exportName } = spec.exported, decl = _findVarDeclarator(localName, parent);
decl && (self._exports[exportName] = self._resolveDeclaration(decl.init, parent), self._exportDecls[exportName] = decl);
}
}) : logger2.warn(
getCsfParsingErrorMessage({
expectedType: "VariableDeclaration",
foundType: node.declaration?.type,
node: node.declaration
})
);
}
},
ExpressionStatement: {
enter({ node, parent }) {
if (t3.isAssignmentExpression(node.expression) && node.expression.operator === "=") {
let { left, right } = node.expression;
if (t3.isMemberExpression(left) && t3.isIdentifier(left.object) && left.object.name === "module" && t3.isIdentifier(left.property) && left.property.name === "exports") {
let exportObject = right;
exportObject = self._resolveDeclaration(exportObject, parent), t3.isObjectExpression(exportObject) ? (self._exportsObject = exportObject, exportObject.properties.forEach((p) => {
let exportName = propKey(p);
if (exportName) {
let exportVal = self._resolveDeclaration(p.value, parent);
self._exports[exportName] = exportVal;
}
})) : logger2.warn(
getCsfParsingErrorMessage({
expectedType: "ObjectExpression",
foundType: exportObject?.type,
node: exportObject
})
);
}
}
}
},
CallExpression: {
enter: ({ node }) => {
t3.isIdentifier(node.callee) && node.callee.name === "definePreview" && node.arguments.length === 1 && t3.isObjectExpression(node.arguments[0]) && self._parseExportsObject(node.arguments[0]);
}
}
}), self;
}
getFieldNode(path) {
let [root, ...rest] = path, exported = this._exports[root];
if (exported)
return _getPath(rest, exported);
}
getFieldProperties(path) {
let [root, ...rest] = path, exported = this._exports[root];
if (exported)
return _getPathProperties(rest, exported);
}
getFieldValue(path) {
let node = this.getFieldNode(path);
if (node) {
let { code } = generate2(node, {});
return (0, eval)(`(() => (${code}))()`);
}
}
getSafeFieldValue(path) {
try {
return this.getFieldValue(path);
} catch {
}
}
setFieldNode(path, expr) {
let [first, ...rest] = path, exportNode = this._exports[first];
if (this._exportsObject) {
let existingProp = this._exportsObject.properties.find((p) => propKey(p) === first);
if (existingProp && t3.isIdentifier(existingProp.value)) {
let varDecl2 = _findVarDeclarator(existingProp.value.name, this._ast.program);
if (varDecl2 && t3.isObjectExpression(varDecl2.init)) {
_updateExportNode(rest, expr, varDecl2.init);
return;
}
}
_updateExportNode(path, expr, this._exportsObject), this._exports[path[0]] = expr;
return;
}
if (exportNode && t3.isObjectExpression(exportNode) && rest.length > 0) {
_updateExportNode(rest, expr, exportNode);
return;
}
let varDecl = _findVarDeclarator(first, this._ast.program);
if (varDecl && t3.isObjectExpression(varDecl.init)) {
_updateExportNode(rest, expr, varDecl.init);
return;
}
if (exportNode && rest.length === 0 && this._exportDecls[path[0]]) {
let decl = this._exportDecls[path[0]];
t3.isVariableDeclarator(decl) && (decl.init = _makeObjectExpression([], expr));
} else {
if (this.hasDefaultExport)
throw new Error(
`Could not set the "${path.join(
"."
)}" field as the default export is not an object in this file.`
);
{
let exportObj = _makeObjectExpression(rest, expr), newExport = t3.exportNamedDeclaration(
t3.variableDeclaration("const", [t3.variableDeclarator(t3.identifier(first), exportObj)])
);
this._exports[first] = exportObj, this._ast.program.body.push(newExport);
}
}
}
/**
* @example
*
* ```ts
* // 1. { framework: 'framework-name' }
* // 2. { framework: { name: 'framework-name', options: {} }
* getNameFromPath(['framework']); // => 'framework-name'
* ```
*
* @returns The name of a node in a given path, supporting the following formats:
*/
getNameFromPath(path) {
let node = this.getFieldNode(path);
if (node)
return this._getPresetValue(node, "name");
}
/**
* Returns an array of names of a node in a given path, supporting the following formats:
*
* @example
*
* ```ts
* const config = {
* addons: ['first-addon', { name: 'second-addon', options: {} }],
* };
* // => ['first-addon', 'second-addon']
* getNamesFromPath(['addons']);
* ```
*/
getNamesFromPath(path) {
let node = this.getFieldNode(path);
if (!node)
return;
let pathNames = [];
return t3.isArrayExpression(node) && node.elements.forEach((element) => {
pathNames.push(this._getPresetValue(element, "name"));
}), pathNames;
}
_getPnpWrappedValue(node) {
if (t3.isCallExpression(node)) {
let arg = node.arguments[0];
if (t3.isStringLiteral(arg))
return arg.value;
}
}
/**
* Given a node and a fallback property, returns a **non-evaluated** string value of the node.
*
* 1. `{ node: 'value' }`
* 2. `{ node: { fallbackProperty: 'value' } }`
*/
_getPresetValue(node, fallbackProperty) {
let value;
if (t3.isStringLiteral(node) ? value = node.value : t3.isObjectExpression(node) ? node.properties.forEach((prop) => {
t3.isObjectProperty(prop) && t3.isIdentifier(prop.key) && prop.key.name === fallbackProperty && (t3.isStringLiteral(prop.value) ? value = prop.value.value : value = this._getPnpWrappedValue(prop.value)), t3.isObjectProperty(prop) && t3.isStringLiteral(prop.key) && prop.key.value === "name" && t3.isStringLiteral(prop.value) && (value = prop.value.value);
}) : t3.isCallExpression(node) && (value = this._getPnpWrappedValue(node)), !value)
throw new Error(
`The given node must be a string literal or an object expression with a "${fallbackProperty}" property that is a string literal.`
);
return value;
}
removeField(path) {
let removeProperty = (properties2, prop) => {
let index = properties2.findIndex(
(p) => t3.isIdentifier(p.key) && p.key.name === prop || t3.isStringLiteral(p.key) && p.key.value === prop
);
index >= 0 && properties2.splice(index, 1);
};
if (path.length === 1) {
let removedRootProperty = !1;
if (this._ast.program.body.forEach((node) => {
if (t3.isExportNamedDeclaration(node) && t3.isVariableDeclaration(node.declaration)) {
let decl = node.declaration.declarations[0];
t3.isIdentifier(decl.id) && decl.id.name === path[0] && (this._ast.program.body.splice(this._ast.program.body.indexOf(node), 1), removedRootProperty = !0);
}
if (t3.isExportDefaultDeclaration(node)) {
let resolved = this._resolveDeclaration(node.declaration);
if (t3.isObjectExpression(resolved)) {
let properties2 = resolved.properties;
removeProperty(properties2, path[0]), removedRootProperty = !0;
}
}
if (t3.isExpressionStatement(node) && t3.isAssignmentExpression(node.expression) && t3.isMemberExpression(node.expression.left) && t3.isIdentifier(node.expression.left.object) && node.expression.left.object.name === "module" && t3.isIdentifier(node.expression.left.property) && node.expression.left.property.name === "exports" && t3.isObjectExpression(node.expression.right)) {
let properties2 = node.expression.right.properties;
removeProperty(properties2, path[0]), removedRootProperty = !0;
}
}), removedRootProperty)
return;
}
let properties = this.getFieldProperties(path);
if (properties) {
let lastPath = path.at(-1);
removeProperty(properties, lastPath);
}
}
appendValueToArray(path, value) {
let node = this.valueToNode(value);
node && this.appendNodeToArray(path, node);
}
appendNodeToArray(path, node) {
let current = this.getFieldNode(path);
if (!current)
this.setFieldNode(path, t3.arrayExpression([node]));
else if (t3.isArrayExpression(current))
current.elements.push(node);
else
throw new Error(`Expected array at '${path.join(".")}', got '${current.type}'`);
}
/**
* Specialized helper to remove addons or other array entries that can either be strings or
* objects with a name property.
*/
removeEntryFromArray(path, value) {
let current = this.getFieldNode(path);
if (current)
if (t3.isArrayExpression(current)) {
let index = current.elements.findIndex((element) => t3.isStringLiteral(element) ? element.value === value : t3.isObjectExpression(element) ? this._getPresetValue(element, "name") === value : this._getPnpWrappedValue(element) === value);
if (index >= 0)
current.elements.splice(index, 1);
else
throw new Error(`Could not find '${value}' in array at '${path.join(".")}'`);
} else
throw new Error(`Expected array at '${path.join(".")}', got '${current.type}'`);
}
_inferQuotes() {
if (!this._quotes) {
let occurrences = (this._ast.tokens || []).slice(0, 500).reduce(
(acc, token) => (token.type.label === "string" && (acc[this._code[token.start]] += 1), acc),
{ "'": 0, '"': 0 }
);
this._quotes = occurrences["'"] > occurrences['"'] ? "single" : "double";
}
return this._quotes;
}
valueToNode(value) {
let quotes = this._inferQuotes(), valueNode;
if (quotes === "single") {
let { code } = generate2(t3.valueToNode(value), { jsescOption: { quotes } }), program = babelParse2(`const __x = ${code}`);
traverse2(program, {
VariableDeclaration: {
enter({ node }) {
node.declarations.length === 1 && t3.isVariableDeclarator(node.declarations[0]) && t3.isIdentifier(node.declarations[0].id) && node.declarations[0].id.name === "__x" && (valueNode = node.declarations[0].init);
}
}
});
} else
valueNode = t3.valueToNode(value);
return valueNode;
}
setFieldValue(path, value) {
let valueNode = this.valueToNode(value);
if (!valueNode)
throw new Error(`Unexpected value ${JSON.stringify(value)}`);
this.setFieldNode(path, valueNode);
}
getBodyDeclarations() {
return this._ast.program.body;
}
setBodyDeclaration(declaration) {
this._ast.program.body.push(declaration);
}
/**
* Import specifiers for a specific require import
*
* @example
*
* ```ts
* // const { foo } = require('bar');
* setRequireImport(['foo'], 'bar');
*
* // const foo = require('bar');
* setRequireImport('foo', 'bar');
* ```
*
* @param importSpecifiers - The import specifiers to set. If a string is passed in, a default
* import will be set. Otherwise, an array of named imports will be set
* @param fromImport - The module to import from
*/
setRequireImport(importSpecifier, fromImport) {
let requireDeclaration = this._ast.program.body.find((node) => {
let hasDeclaration = t3.isVariableDeclaration(node) && node.declarations.length === 1 && t3.isVariableDeclarator(node.declarations[0]) && t3.isCallExpression(node.declarations[0].init) && t3.isIdentifier(node.declarations[0].init.callee) && node.declarations[0].init.callee.name === "require" && t3.isStringLiteral(node.declarations[0].init.arguments[0]) && (node.declarations[0].init.arguments[0].value === fromImport || node.declarations[0].init.arguments[0].value === fromImport.split("node:")[1]);
return hasDeclaration && (fromImport = node.declarations[0].init.arguments[0].value), hasDeclaration;
}), hasRequireSpecifier = (name) => t3.isObjectPattern(requireDeclaration?.declarations[0].id) && requireDeclaration?.declarations[0].id.properties.find(
(specifier) => t3.isObjectProperty(specifier) && t3.isIdentifier(specifier.key) && specifier.key.name === name
), hasDefaultRequireSpecifier = (declaration, name) => declaration.declarations.length === 1 && t3.isVariableDeclarator(declaration.declarations[0]) && t3.isIdentifier(declaration.declarations[0].id) && declaration.declarations[0].id.name === name;
if (typeof importSpecifier == "string") {
let addDefaultRequireSpecifier = () => {
this._ast.program.body.unshift(
t3.variableDeclaration("const", [
t3.variableDeclarator(
t3.identifier(importSpecifier),
t3.callExpression(t3.identifier("require"), [t3.stringLiteral(fromImport)])
)
])
);
};
requireDeclaration && hasDefaultRequireSpecifier(requireDeclaration, importSpecifier) || addDefaultRequireSpecifier();
} else requireDeclaration ? importSpecifier.forEach((specifier) => {
hasRequireSpecifier(specifier) || requireDeclaration.declarations[0].id.properties.push(
t3.objectProperty(t3.identifier(specifier), t3.identifier(specifier), void 0, !0)
);
}) : this._ast.program.body.unshift(
t3.variableDeclaration("const", [
t3.variableDeclarator(
t3.objectPattern(
importSpecifier.map(
(specifier) => t3.objectProperty(t3.identifier(specifier), t3.identifier(specifier), void 0, !0)
)
),
t3.callExpression(t3.identifier("require"), [t3.stringLiteral(fromImport)])
)
])
);
}
/**
* Set import specifiers for a given import statement.
*
* Does not support setting type imports (yet)
*
* @example
*
* ```ts
* // import { foo } from 'bar';
* setImport(['foo'], 'bar');
*
* // import foo from 'bar';
* setImport('foo', 'bar');
*
* // import * as foo from 'bar';
* setImport({ namespace: 'foo' }, 'bar');
*
* // import 'bar';
* setImport(null, 'bar');
* ```
*
* @param importSpecifiers - The import specifiers to set. If a string is passed in, a default
* import will be set. Otherwise, an array of named imports will be set
* @param fromImport - The module to import from
*/
setImport(importSpecifier, fromImport) {
let importDeclaration = this._ast.program.body.find((node) => {
let hasDeclaration = t3.isImportDeclaration(node) && (node.source.value === fromImport || node.source.value === fromImport.split("node:")[1]);
return hasDeclaration && (fromImport = node.source.value), hasDeclaration;
}), getNewImportSpecifier = (specifier) => t3.importSpecifier(t3.identifier(specifier), t3.identifier(specifier)), hasImportSpecifier = (declaration, name) => declaration.specifiers.find(
(specifier) => t3.isImportSpecifier(specifier) && t3.isIdentifier(specifier.imported) && specifier.imported.name === name
), hasNamespaceImportSpecifier = (declaration, name) => declaration.specifiers.find(
(specifier) => t3.isImportNamespaceSpecifier(specifier) && t3.isIdentifier(specifier.local) && specifier.local.name === name
);
importSpecifier === null ? importDeclaration || this._ast.program.body.unshift(t3.importDeclaration([], t3.stringLiteral(fromImport))) : typeof importSpecifier == "string" ? importDeclaration ? ((declaration, name) => declaration.specifiers.find(
(specifier) => t3.isImportDefault