UNPKG

eslint-plugin-storybook

Version:

Storybook ESLint Plugin: Best practice rules for writing stories

1,264 lines (1,234 loc) • 57.2 kB
import CJS_COMPAT_NODE_URL_sdweyd8qp6n from 'node:url'; import CJS_COMPAT_NODE_PATH_sdweyd8qp6n from 'node:path'; import CJS_COMPAT_NODE_MODULE_sdweyd8qp6n from "node:module"; var __filename = CJS_COMPAT_NODE_URL_sdweyd8qp6n.fileURLToPath(import.meta.url); var __dirname = CJS_COMPAT_NODE_PATH_sdweyd8qp6n.dirname(__filename); var require = CJS_COMPAT_NODE_MODULE_sdweyd8qp6n.createRequire(import.meta.url); // ------------------------------------------------------------ // end of CJS compatibility banner, injected by Storybook's esbuild configuration // ------------------------------------------------------------ var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf, __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from == "object" || typeof from == "function") for (let key of __getOwnPropNames(from)) !__hasOwnProp.call(to, key) && key !== except && __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: !0 }) : target, mod )); // ../../node_modules/ts-dedent/dist/index.js var require_dist = __commonJS({ "../../node_modules/ts-dedent/dist/index.js"(exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: !0 }); exports.dedent = void 0; function dedent2(templ) { for (var values = [], _i = 1; _i < arguments.length; _i++) values[_i - 1] = arguments[_i]; var strings = Array.from(typeof templ == "string" ? [templ] : templ); strings[strings.length - 1] = strings[strings.length - 1].replace(/\r?\n([\t ]*)$/, ""); var indentLengths = strings.reduce(function(arr, str) { var matches = str.match(/\n([\t ]+|(?!\s).)/g); return matches ? arr.concat(matches.map(function(match) { var _a, _b; return (_b = (_a = match.match(/[\t ]/g)) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0; })) : arr; }, []); if (indentLengths.length) { var pattern_1 = new RegExp(` [ ]{` + Math.min.apply(Math, indentLengths) + "}", "g"); strings = strings.map(function(str) { return str.replace(pattern_1, ` `); }); } strings[0] = strings[0].replace(/^\r?\n/, ""); var string = strings[0]; return values.forEach(function(value, i) { var endentations = string.match(/(?:^|\n)( *)$/), endentation = endentations ? endentations[1] : "", indentedValue = value; typeof value == "string" && value.includes(` `) && (indentedValue = String(value).split(` `).map(function(str, i2) { return i2 === 0 ? str : "" + endentation + str; }).join(` `)), string += indentedValue + strings[i + 1]; }), string; } exports.dedent = dedent2; exports.default = dedent2; } }); // src/configs/addon-interactions.ts var addon_interactions_default = { plugins: ["storybook"], overrides: [ { files: ["**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)", "**/*.story.@(ts|tsx|js|jsx|mjs|cjs)"], rules: { "react-hooks/rules-of-hooks": "off", "import/no-anonymous-default-export": "off", "storybook/await-interactions": "error", "storybook/context-in-play-function": "error", "storybook/use-storybook-expect": "error", "storybook/use-storybook-testing-library": "error" } }, { files: [".storybook/main.@(js|cjs|mjs|ts)"], rules: { "storybook/no-uninstalled-addons": "error" } } ] }; // src/configs/csf.ts var csf_default = { plugins: ["storybook"], overrides: [ { files: ["**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)", "**/*.story.@(ts|tsx|js|jsx|mjs|cjs)"], rules: { "react-hooks/rules-of-hooks": "off", "import/no-anonymous-default-export": "off", "storybook/csf-component": "warn", "storybook/default-exports": "error", "storybook/hierarchy-separator": "warn", "storybook/no-redundant-story-name": "warn", "storybook/story-exports": "error" } }, { files: [".storybook/main.@(js|cjs|mjs|ts)"], rules: { "storybook/no-uninstalled-addons": "error" } } ] }; // src/configs/csf-strict.ts var csf_strict_default = { // This file is bundled in an index.js file at the root // so the reference is relative to the src directory extends: "./configs/csf", overrides: [ { files: ["**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)", "**/*.story.@(ts|tsx|js|jsx|mjs|cjs)"], rules: { "react-hooks/rules-of-hooks": "off", "import/no-anonymous-default-export": "off", "storybook/no-stories-of": "error", "storybook/no-title-property-in-meta": "error" } } ] }; // src/configs/flat/addon-interactions.ts var addon_interactions_default2 = [ { name: "storybook:addon-interactions:setup", plugins: { get storybook() { return index_default; } } }, { name: "storybook:addon-interactions:stories-rules", files: ["**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)", "**/*.story.@(ts|tsx|js|jsx|mjs|cjs)"], rules: { "react-hooks/rules-of-hooks": "off", "import/no-anonymous-default-export": "off", "storybook/await-interactions": "error", "storybook/context-in-play-function": "error", "storybook/use-storybook-expect": "error", "storybook/use-storybook-testing-library": "error" } }, { name: "storybook:addon-interactions:main-rules", files: [".storybook/main.@(js|cjs|mjs|ts)"], rules: { "storybook/no-uninstalled-addons": "error" } } ]; // src/configs/flat/csf.ts var csf_default2 = [ { name: "storybook:csf:setup", plugins: { get storybook() { return index_default; } } }, { name: "storybook:csf:stories-rules", files: ["**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)", "**/*.story.@(ts|tsx|js|jsx|mjs|cjs)"], rules: { "react-hooks/rules-of-hooks": "off", "import/no-anonymous-default-export": "off", "storybook/csf-component": "warn", "storybook/default-exports": "error", "storybook/hierarchy-separator": "warn", "storybook/no-redundant-story-name": "warn", "storybook/story-exports": "error" } }, { name: "storybook:csf:main-rules", files: [".storybook/main.@(js|cjs|mjs|ts)"], rules: { "storybook/no-uninstalled-addons": "error" } } ]; // src/configs/flat/csf-strict.ts var csf_strict_default2 = [ ...csf_default2, { name: "storybook:csf-strict:rules", files: ["**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)", "**/*.story.@(ts|tsx|js|jsx|mjs|cjs)"], rules: { "react-hooks/rules-of-hooks": "off", "import/no-anonymous-default-export": "off", "storybook/no-stories-of": "error", "storybook/no-title-property-in-meta": "error" } } ]; // src/configs/flat/recommended.ts var recommended_default = [ { name: "storybook:recommended:setup", plugins: { get storybook() { return index_default; } } }, { name: "storybook:recommended:stories-rules", files: ["**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)", "**/*.story.@(ts|tsx|js|jsx|mjs|cjs)"], rules: { "react-hooks/rules-of-hooks": "off", "import/no-anonymous-default-export": "off", "storybook/await-interactions": "error", "storybook/context-in-play-function": "error", "storybook/default-exports": "error", "storybook/hierarchy-separator": "warn", "storybook/no-redundant-story-name": "warn", "storybook/no-renderer-packages": "error", "storybook/prefer-pascal-case": "warn", "storybook/story-exports": "error", "storybook/use-storybook-expect": "error", "storybook/use-storybook-testing-library": "error" } }, { name: "storybook:recommended:main-rules", files: [".storybook/main.@(js|cjs|mjs|ts)"], rules: { "storybook/no-uninstalled-addons": "error" } } ]; // src/configs/recommended.ts var recommended_default2 = { plugins: ["storybook"], overrides: [ { files: ["**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)", "**/*.story.@(ts|tsx|js|jsx|mjs|cjs)"], rules: { "react-hooks/rules-of-hooks": "off", "import/no-anonymous-default-export": "off", "storybook/await-interactions": "error", "storybook/context-in-play-function": "error", "storybook/default-exports": "error", "storybook/hierarchy-separator": "warn", "storybook/no-redundant-story-name": "warn", "storybook/no-renderer-packages": "error", "storybook/prefer-pascal-case": "warn", "storybook/story-exports": "error", "storybook/use-storybook-expect": "error", "storybook/use-storybook-testing-library": "error" } }, { files: [".storybook/main.@(js|cjs|mjs|ts)"], rules: { "storybook/no-uninstalled-addons": "error" } } ] }; // src/utils/ast.ts import { AST_NODE_TYPES } from "@typescript-eslint/utils"; import { ASTUtils } from "@typescript-eslint/utils"; var isNodeOfType = (nodeType) => (node) => node?.type === nodeType, isAwaitExpression = isNodeOfType(AST_NODE_TYPES.AwaitExpression), isIdentifier = isNodeOfType(AST_NODE_TYPES.Identifier), isVariableDeclarator = isNodeOfType(AST_NODE_TYPES.VariableDeclarator), isArrayExpression = isNodeOfType(AST_NODE_TYPES.ArrayExpression), isArrowFunctionExpression = isNodeOfType(AST_NODE_TYPES.ArrowFunctionExpression), isBlockStatement = isNodeOfType(AST_NODE_TYPES.BlockStatement), isCallExpression = isNodeOfType(AST_NODE_TYPES.CallExpression), isExpressionStatement = isNodeOfType(AST_NODE_TYPES.ExpressionStatement), isVariableDeclaration = isNodeOfType(AST_NODE_TYPES.VariableDeclaration), isAssignmentExpression = isNodeOfType(AST_NODE_TYPES.AssignmentExpression), isSequenceExpression = isNodeOfType(AST_NODE_TYPES.SequenceExpression), isImportDeclaration = isNodeOfType(AST_NODE_TYPES.ImportDeclaration), isImportDefaultSpecifier = isNodeOfType(AST_NODE_TYPES.ImportDefaultSpecifier), isImportNamespaceSpecifier = isNodeOfType(AST_NODE_TYPES.ImportNamespaceSpecifier), isImportSpecifier = isNodeOfType(AST_NODE_TYPES.ImportSpecifier), isJSXAttribute = isNodeOfType(AST_NODE_TYPES.JSXAttribute), isLiteral = isNodeOfType(AST_NODE_TYPES.Literal), isMemberExpression = isNodeOfType(AST_NODE_TYPES.MemberExpression), isNewExpression = isNodeOfType(AST_NODE_TYPES.NewExpression), isObjectExpression = isNodeOfType(AST_NODE_TYPES.ObjectExpression), isObjectPattern = isNodeOfType(AST_NODE_TYPES.ObjectPattern), isProperty = isNodeOfType(AST_NODE_TYPES.Property), isSpreadElement = isNodeOfType(AST_NODE_TYPES.SpreadElement), isRestElement = isNodeOfType(AST_NODE_TYPES.RestElement), isReturnStatement = isNodeOfType(AST_NODE_TYPES.ReturnStatement), isFunctionDeclaration = isNodeOfType(AST_NODE_TYPES.FunctionDeclaration), isFunctionExpression = isNodeOfType(AST_NODE_TYPES.FunctionExpression), isProgram = isNodeOfType(AST_NODE_TYPES.Program), isTSTypeAliasDeclaration = isNodeOfType(AST_NODE_TYPES.TSTypeAliasDeclaration), isTSInterfaceDeclaration = isNodeOfType(AST_NODE_TYPES.TSInterfaceDeclaration), isTSAsExpression = isNodeOfType(AST_NODE_TYPES.TSAsExpression), isTSSatisfiesExpression = isNodeOfType(AST_NODE_TYPES.TSSatisfiesExpression), isTSNonNullExpression = isNodeOfType(AST_NODE_TYPES.TSNonNullExpression), isMetaProperty = isNodeOfType(AST_NODE_TYPES.MetaProperty); // src/utils/create-storybook-rule.ts import { ESLintUtils } from "@typescript-eslint/utils"; // src/utils/index.ts import { isExportStory } from "storybook/internal/csf"; import { ASTUtils as ASTUtils2 } from "@typescript-eslint/utils"; var docsUrl = (ruleName) => `https://github.com/storybookjs/storybook/blob/next/code/lib/eslint-plugin/docs/rules/${ruleName}.md`, getMetaObjectExpression = (node, context) => { let meta = node.declaration, { sourceCode } = context; if (isIdentifier(meta)) { let scope = sourceCode.getScope ? sourceCode.getScope(node) : context.getScope(), variable = ASTUtils2.findVariable(scope, meta.name), decl = variable && variable.defs.find((def) => isVariableDeclarator(def.node)); decl && isVariableDeclarator(decl.node) && (meta = decl.node.init); } return (isTSAsExpression(meta) || isTSSatisfiesExpression(meta)) && (meta = meta.expression), isObjectExpression(meta) ? meta : null; }, getDescriptor = (metaDeclaration, propertyName) => { let property = metaDeclaration && metaDeclaration.properties.find( (p) => "key" in p && "name" in p.key && p.key.name === propertyName ); if (!property || isSpreadElement(property)) return; let { type } = property.value; switch (type) { case "ArrayExpression": return property.value.elements.map((t) => { if (t === null) throw new Error("Unexpected descriptor element: null"); if (!["StringLiteral", "Literal"].includes(t.type)) throw new Error(`Unexpected descriptor element: ${t.type}`); return t.value; }); case "Literal": // @ts-expect-error TODO: Investigation needed. Type systems says, that "RegExpLiteral" does not exist case "RegExpLiteral": return property.value.value; default: throw new Error(`Unexpected descriptor: ${type}`); } }, isValidStoryExport = (node, nonStoryExportsConfig) => isExportStory(node.name, nonStoryExportsConfig) && node.name !== "__namedExportsOrder", getAllNamedExports = (node) => { if (!node.declaration && node.specifiers) return node.specifiers.reduce((acc, specifier) => (isIdentifier(specifier.exported) && acc.push(specifier.exported), acc), []); let decl = node.declaration; if (isVariableDeclaration(decl)) { let declaration = decl.declarations[0]; if (declaration) { let { id } = declaration; if (isIdentifier(id)) return [id]; } } return isFunctionDeclaration(decl) && isIdentifier(decl.id) ? [decl.id] : []; }; // src/utils/create-storybook-rule.ts function createStorybookRule({ create, meta, ...remainingConfig }) { return ESLintUtils.RuleCreator(docsUrl)({ ...remainingConfig, create, meta: { ...meta, docs: { ...meta.docs }, defaultOptions: remainingConfig.defaultOptions } }); } // src/rules/await-interactions.ts var await_interactions_default = createStorybookRule({ name: "await-interactions", defaultOptions: [], meta: { severity: "error", docs: { description: "Interactions should be awaited", categories: ["addon-interactions" /* ADDON_INTERACTIONS */, "recommended" /* RECOMMENDED */] }, messages: { interactionShouldBeAwaited: "Interaction should be awaited: {{method}}", fixSuggestion: "Add `await` to method" }, type: "problem", fixable: "code", hasSuggestions: !0, schema: [] }, create(context) { let FUNCTIONS_TO_BE_AWAITED = [ "waitFor", "waitForElementToBeRemoved", "wait", "waitForElement", "waitForDomChange", "userEvent", "play" ], getMethodThatShouldBeAwaited = (expr) => { let shouldAwait = (name) => FUNCTIONS_TO_BE_AWAITED.includes(name) || name.startsWith("findBy"); return isArrowFunctionExpression(expr.parent) || isReturnStatement(expr.parent) ? null : isMemberExpression(expr.callee) && isIdentifier(expr.callee.object) && shouldAwait(expr.callee.object.name) ? expr.callee.object : isTSNonNullExpression(expr.callee) && isMemberExpression(expr.callee.expression) && isIdentifier(expr.callee.expression.property) && shouldAwait(expr.callee.expression.property.name) ? expr.callee.expression.property : isMemberExpression(expr.callee) && isIdentifier(expr.callee.property) && shouldAwait(expr.callee.property.name) || isMemberExpression(expr.callee) && isCallExpression(expr.callee.object) && isIdentifier(expr.callee.object.callee) && isIdentifier(expr.callee.property) && expr.callee.object.callee.name === "expect" ? expr.callee.property : isIdentifier(expr.callee) && shouldAwait(expr.callee.name) ? expr.callee : null; }, getClosestFunctionAncestor = (node) => { let parent = node.parent; if (!(!parent || isProgram(parent))) return isArrowFunctionExpression(parent) || isFunctionExpression(parent) || isFunctionDeclaration(parent) ? node.parent : getClosestFunctionAncestor(parent); }, isUserEventFromStorybookImported = (node) => (node.source.value === "@storybook/testing-library" || node.source.value === "@storybook/test") && node.specifiers.find( (spec) => isImportSpecifier(spec) && "name" in spec.imported && spec.imported.name === "userEvent" && spec.local.name === "userEvent" ) !== void 0, isExpectFromStorybookImported = (node) => (node.source.value === "@storybook/jest" || node.source.value === "@storybook/test") && node.specifiers.find( (spec) => isImportSpecifier(spec) && "name" in spec.imported && spec.imported.name === "expect" ) !== void 0, isImportedFromStorybook = !0, invocationsThatShouldBeAwaited = []; return { ImportDeclaration(node) { isImportedFromStorybook = isUserEventFromStorybookImported(node) || isExpectFromStorybookImported(node); }, VariableDeclarator(node) { isImportedFromStorybook = isImportedFromStorybook && isIdentifier(node.id) && node.id.name !== "userEvent"; }, CallExpression(node) { let method = getMethodThatShouldBeAwaited(node); method && !isAwaitExpression(node.parent) && !isAwaitExpression(node.parent?.parent) && invocationsThatShouldBeAwaited.push({ node, method }); }, "Program:exit": function() { isImportedFromStorybook && invocationsThatShouldBeAwaited.length && invocationsThatShouldBeAwaited.forEach(({ node, method }) => { let parentFnNode = getClosestFunctionAncestor(node), parentFnNeedsAsync = parentFnNode && !("async" in parentFnNode && parentFnNode.async), fixFn = (fixer) => { let fixerResult = [fixer.insertTextBefore(node, "await ")]; return parentFnNeedsAsync && fixerResult.push(fixer.insertTextBefore(parentFnNode, "async ")), fixerResult; }; context.report({ node, messageId: "interactionShouldBeAwaited", data: { method: method.name }, fix: fixFn, suggest: [ { messageId: "fixSuggestion", fix: fixFn } ] }); }); } }; } }); // src/rules/context-in-play-function.ts var context_in_play_function_default = createStorybookRule({ name: "context-in-play-function", defaultOptions: [], meta: { type: "problem", severity: "error", docs: { description: "Pass a context when invoking play function of another story", categories: ["recommended" /* RECOMMENDED */, "addon-interactions" /* ADDON_INTERACTIONS */] }, messages: { passContextToPlayFunction: "Pass a context when invoking play function of another story" }, fixable: void 0, schema: [] }, create(context) { let isPlayFunctionFromAnotherStory = (expr) => !!(isTSNonNullExpression(expr.callee) && isMemberExpression(expr.callee.expression) && isIdentifier(expr.callee.expression.property) && expr.callee.expression.property.name === "play" || isMemberExpression(expr.callee) && isIdentifier(expr.callee.property) && expr.callee.property.name === "play"), getParentParameterName = (node) => { if (!isArrowFunctionExpression(node)) return node.parent ? getParentParameterName(node.parent) : void 0; if (node.params.length !== 0 && node.params.length >= 1) { let param = node.params[0]; if (isIdentifier(param)) return param.name; if (isObjectPattern(param)) { if (param.properties.find((prop) => prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "context")) return "context"; let restElement = param.properties.find(isRestElement); return !restElement || !isIdentifier(restElement.argument) ? void 0 : restElement.argument.name; } } }, isNotPassingContextCorrectly = (expr) => { let firstExpressionArgument = expr.arguments[0]; if (!firstExpressionArgument) return !0; let contextVariableName = getParentParameterName(expr); return contextVariableName ? !(expr.arguments.length === 1 && isIdentifier(firstExpressionArgument) && firstExpressionArgument.name === contextVariableName || isObjectExpression(firstExpressionArgument) && firstExpressionArgument.properties.some((prop) => isSpreadElement(prop) && isIdentifier(prop.argument) && prop.argument.name === contextVariableName)) : !0; }, invocationsWithoutProperContext = []; return { CallExpression(node) { isPlayFunctionFromAnotherStory(node) && isNotPassingContextCorrectly(node) && invocationsWithoutProperContext.push(node); }, "Program:exit": function() { invocationsWithoutProperContext.forEach((node) => { context.report({ node, messageId: "passContextToPlayFunction" }); }); } }; } }); // src/rules/csf-component.ts var csf_component_default = createStorybookRule({ name: "csf-component", defaultOptions: [], meta: { type: "suggestion", severity: "warn", docs: { description: "The component property should be set", categories: ["csf" /* CSF */] }, messages: { missingComponentProperty: "Missing component property." }, schema: [] }, create(context) { return { ExportDefaultDeclaration(node) { let meta = getMetaObjectExpression(node, context); if (!meta) return null; meta.properties.find( (property) => !isSpreadElement(property) && "name" in property.key && property.key.name === "component" ) || context.report({ node, messageId: "missingComponentProperty" }); } }; } }); // src/rules/default-exports.ts import path from "path"; var default_exports_default = createStorybookRule({ name: "default-exports", defaultOptions: [], meta: { type: "problem", severity: "error", docs: { description: "Story files should have a default export", categories: ["csf" /* CSF */, "recommended" /* RECOMMENDED */] }, messages: { shouldHaveDefaultExport: "The file should have a default export.", fixSuggestion: "Add default export" }, fixable: "code", hasSuggestions: !0, schema: [] }, create(context) { let getComponentName = (node, filePath) => { let name = path.basename(filePath).split(".")[0]; return node.body.find((stmt) => { if (isImportDeclaration(stmt) && isLiteral(stmt.source) && stmt.source.value.startsWith(`./${name}`)) return !!stmt.specifiers.find( (spec) => isIdentifier(spec.local) && spec.local.name === name ); }) ? name : null; }, hasDefaultExport = !1, isCsf4Style = !1, hasStoriesOfImport = !1; return { ImportSpecifier(node) { "name" in node.imported && node.imported.name === "storiesOf" && (hasStoriesOfImport = !0); }, VariableDeclaration(node) { node.parent.type === "Program" && node.declarations.forEach((declaration) => { let init = declaration.init; if (init && init.type === "CallExpression") { let callee = init.callee; callee.type === "MemberExpression" && callee.property.type === "Identifier" && callee.property.name === "meta" && (isCsf4Style = !0); } }); }, ExportDefaultSpecifier: function() { hasDefaultExport = !0; }, ExportDefaultDeclaration: function() { hasDefaultExport = !0; }, "Program:exit": function(program) { if (!isCsf4Style && !hasDefaultExport && !hasStoriesOfImport) { let componentName = getComponentName(program, context.getFilename()), node = program.body.find((n) => !isImportDeclaration(n)) || program.body[0] || program, report = { node, messageId: "shouldHaveDefaultExport" }, fix = (fixer) => { let metaDeclaration = componentName ? `export default { component: ${componentName} } ` : `export default {} `; return fixer.insertTextBefore(node, metaDeclaration); }; context.report({ ...report, fix, suggest: [ { messageId: "fixSuggestion", fix } ] }); } } }; } }); // src/rules/hierarchy-separator.ts var hierarchy_separator_default = createStorybookRule({ name: "hierarchy-separator", defaultOptions: [], meta: { type: "problem", fixable: "code", hasSuggestions: !0, severity: "warn", docs: { description: "Deprecated hierarchy separator in title property", categories: ["csf" /* CSF */, "recommended" /* RECOMMENDED */] }, messages: { useCorrectSeparators: "Use correct separators", deprecatedHierarchySeparator: "Deprecated hierarchy separator in title property: {{metaTitle}}." }, schema: [] }, create: function(context) { return { ExportDefaultDeclaration: function(node) { let meta = getMetaObjectExpression(node, context); if (!meta) return null; let titleNode = meta.properties.find( (prop) => !isSpreadElement(prop) && "name" in prop.key && prop.key?.name === "title" ); if (!titleNode || !isLiteral(titleNode.value)) return; let metaTitle = titleNode.value.raw || ""; metaTitle.includes("|") && context.report({ node: titleNode, messageId: "deprecatedHierarchySeparator", data: { metaTitle }, // In case we want this to be auto fixed by --fix fix: function(fixer) { return fixer.replaceTextRange(titleNode.value.range, metaTitle.replace(/\|/g, "/")); }, suggest: [ { messageId: "useCorrectSeparators", fix: function(fixer) { return fixer.replaceTextRange( titleNode.value.range, metaTitle.replace(/\|/g, "/") ); } } ] }); } }; } }); // src/rules/meta-inline-properties.ts var meta_inline_properties_default = createStorybookRule({ name: "meta-inline-properties", defaultOptions: [{ csfVersion: 3 }], meta: { type: "problem", severity: "error", docs: { description: "Meta should only have inline properties", categories: ["csf" /* CSF */, "recommended" /* RECOMMENDED */], excludeFromConfig: !0 }, messages: { metaShouldHaveInlineProperties: "Meta should only have inline properties: {{property}}" }, schema: [ { type: "object", properties: { csfVersion: { type: "number" } }, additionalProperties: !1 } ] }, create(context) { let isInline = (node) => node && typeof node == "object" && "value" in node ? node.value.type === "ObjectExpression" || node.value.type === "Literal" || node.value.type === "ArrayExpression" : !1; return { ExportDefaultDeclaration(node) { let meta = getMetaObjectExpression(node, context); if (!meta) return null; let ruleProperties = ["title", "args"], dynamicProperties = []; meta.properties.filter( (prop) => "key" in prop && "name" in prop.key && ruleProperties.includes(prop.key.name) ).forEach((metaNode) => { isInline(metaNode) || dynamicProperties.push(metaNode); }), dynamicProperties.length > 0 && dynamicProperties.forEach((propertyNode) => { context.report({ node: propertyNode, messageId: "metaShouldHaveInlineProperties", data: { property: propertyNode.key?.name } }); }); } }; } }); // src/rules/meta-satisfies-type.ts import { ASTUtils as ASTUtils3, AST_NODE_TYPES as AST_NODE_TYPES2 } from "@typescript-eslint/utils"; var meta_satisfies_type_default = createStorybookRule({ name: "meta-satisfies-type", defaultOptions: [], meta: { type: "problem", fixable: "code", severity: "error", docs: { description: "Meta should use `satisfies Meta`", categories: [], excludeFromConfig: !0 }, messages: { metaShouldSatisfyType: "CSF Meta should use `satisfies` for type safety" }, schema: [] }, create(context) { let sourceCode = context.getSourceCode(), getTextWithParentheses = (node) => { let beforeCount = 0, afterCount = 0; if (ASTUtils3.isParenthesized(node, sourceCode)) { let bodyOpeningParen = sourceCode.getTokenBefore(node, ASTUtils3.isOpeningParenToken), bodyClosingParen = sourceCode.getTokenAfter(node, ASTUtils3.isClosingParenToken); bodyOpeningParen && bodyClosingParen && (beforeCount = node.range[0] - bodyOpeningParen.range[0], afterCount = bodyClosingParen.range[1] - node.range[1]); } return sourceCode.getText(node, beforeCount, afterCount); }, getFixer = (meta) => { let { parent } = meta; if (parent) switch (parent.type) { // {} as Meta case AST_NODE_TYPES2.TSAsExpression: return (fixer) => [ fixer.replaceText(parent, getTextWithParentheses(meta)), fixer.insertTextAfter( parent, ` satisfies ${getTextWithParentheses(parent.typeAnnotation)}` ) ]; // const meta: Meta = {} case AST_NODE_TYPES2.VariableDeclarator: { let { typeAnnotation } = parent.id; return typeAnnotation ? (fixer) => [ fixer.remove(typeAnnotation), fixer.insertTextAfter( meta, ` satisfies ${getTextWithParentheses(typeAnnotation.typeAnnotation)}` ) ] : void 0; } default: return; } }; return { ExportDefaultDeclaration(node) { let meta = getMetaObjectExpression(node, context); if (!meta) return null; (!meta.parent || !isTSSatisfiesExpression(meta.parent)) && context.report({ node: meta, messageId: "metaShouldSatisfyType", fix: getFixer(meta) }); } }; } }); // src/rules/no-redundant-story-name.ts import { storyNameFromExport } from "storybook/internal/csf"; var no_redundant_story_name_default = createStorybookRule({ name: "no-redundant-story-name", defaultOptions: [], meta: { type: "suggestion", fixable: "code", hasSuggestions: !0, severity: "warn", docs: { description: "A story should not have a redundant name property", categories: ["csf" /* CSF */, "recommended" /* RECOMMENDED */] }, messages: { removeRedundantName: "Remove redundant name", storyNameIsRedundant: "Named exports should not use the name annotation if it is redundant to the name that would be generated by the export name" }, schema: [] }, create(context) { return { // CSF3 ExportNamedDeclaration: function(node) { if (!node.declaration) return; let decl = node.declaration; if (isVariableDeclaration(decl)) { let declaration = decl.declarations[0]; if (declaration == null) return; let { id, init } = declaration; if (isIdentifier(id) && isObjectExpression(init)) { let storyNameNode = init.properties.find( (prop) => isProperty(prop) && isIdentifier(prop.key) && (prop.key?.name === "name" || prop.key?.name === "storyName") ); if (!storyNameNode) return; let { name } = id, resolvedStoryName = storyNameFromExport(name); !isSpreadElement(storyNameNode) && isLiteral(storyNameNode.value) && storyNameNode.value.value === resolvedStoryName && context.report({ node: storyNameNode, messageId: "storyNameIsRedundant", suggest: [ { messageId: "removeRedundantName", fix: function(fixer) { return fixer.remove(storyNameNode); } } ] }); } } }, // CSF2 AssignmentExpression: function(node) { if (!isExpressionStatement(node.parent)) return; let { left, right } = node; if ("property" in left && isIdentifier(left.property) && !isMetaProperty(left) && left.property.name === "storyName") { if (!("name" in left.object && "value" in right)) return; let propertyName = left.object.name, propertyValue = right.value, resolvedStoryName = storyNameFromExport(propertyName); propertyValue === resolvedStoryName && context.report({ node, messageId: "storyNameIsRedundant", suggest: [ { messageId: "removeRedundantName", fix: function(fixer) { return fixer.remove(node); } } ] }); } } }; } }); // src/rules/no-renderer-packages.ts var rendererToFrameworks = { "@storybook/html": ["@storybook/html-vite", "@storybook/html-webpack5"], "@storybook/preact": ["@storybook/preact-vite", "@storybook/preact-webpack5"], "@storybook/react": [ "@storybook/nextjs", "@storybook/react-vite", "@storybook/nextjs-vite", "@storybook/react-webpack5", "@storybook/react-native-web-vite" ], "@storybook/server": ["@storybook/server-webpack5"], "@storybook/svelte": [ "@storybook/svelte-vite", "@storybook/svelte-webpack5", "@storybook/sveltekit" ], "@storybook/vue3": ["@storybook/vue3-vite", "@storybook/vue3-webpack5"], "@storybook/web-components": [ "@storybook/web-components-vite", "@storybook/web-components-webpack5" ] }, no_renderer_packages_default = createStorybookRule({ name: "no-renderer-packages", defaultOptions: [], meta: { type: "problem", severity: "error", docs: { description: "Do not import renderer packages directly in stories", categories: ["recommended" /* RECOMMENDED */] }, schema: [], messages: { noRendererPackages: 'Do not import renderer package "{{rendererPackage}}" directly. Use a framework package instead (e.g. {{suggestions}}).' } }, create(context) { return { ImportDeclaration(node) { let packageName = node.source.value; if (typeof packageName == "string" && packageName in rendererToFrameworks) { let suggestions = rendererToFrameworks[packageName]; context.report({ node, messageId: "noRendererPackages", data: { rendererPackage: packageName, suggestions: suggestions.join(", ") } }); } } }; } }); // src/rules/no-stories-of.ts var no_stories_of_default = createStorybookRule({ name: "no-stories-of", defaultOptions: [], meta: { type: "problem", severity: "error", docs: { description: "storiesOf is deprecated and should not be used", categories: ["csf-strict" /* CSF_STRICT */] }, messages: { doNotUseStoriesOf: "storiesOf is deprecated and should not be used" }, schema: [] }, create(context) { return { ImportSpecifier(node) { "name" in node.imported && node.imported.name === "storiesOf" && context.report({ node, messageId: "doNotUseStoriesOf" }); } }; } }); // src/rules/no-title-property-in-meta.ts var no_title_property_in_meta_default = createStorybookRule({ name: "no-title-property-in-meta", defaultOptions: [], meta: { type: "problem", fixable: "code", hasSuggestions: !0, severity: "error", docs: { description: "Do not define a title in meta", categories: ["csf-strict" /* CSF_STRICT */] }, messages: { removeTitleInMeta: "Remove title property from meta", noTitleInMeta: "CSF3 does not need a title in meta" }, schema: [] }, create: function(context) { return { ExportDefaultDeclaration: function(node) { let meta = getMetaObjectExpression(node, context); if (!meta) return null; let titleNode = meta.properties.find( (prop) => !isSpreadElement(prop) && "name" in prop.key && prop.key?.name === "title" ); titleNode && context.report({ node: titleNode, messageId: "noTitleInMeta", suggest: [ { messageId: "removeTitleInMeta", fix(fixer) { let hasComma = context.getSourceCode().text.slice( titleNode.range[0], titleNode.range[1] + 1 ).slice(-1) === ",", propertyRange = [ titleNode.range[0], hasComma ? titleNode.range[1] + 1 : titleNode.range[1] ]; return fixer.removeRange(propertyRange); } } ] }); } }; } }); // src/rules/no-uninstalled-addons.ts var import_ts_dedent = __toESM(require_dist(), 1); import { readFileSync } from "fs"; import { relative, resolve, sep } from "path"; var no_uninstalled_addons_default = createStorybookRule({ name: "no-uninstalled-addons", defaultOptions: [ { packageJsonLocation: "", ignore: [] } ], meta: { type: "problem", severity: "error", docs: { description: "This rule identifies storybook addons that are invalid because they are either not installed or contain a typo in their name.", categories: ["recommended" /* RECOMMENDED */] }, messages: { addonIsNotInstalled: "The {{ addonName }} is not installed in {{packageJsonPath}}. Did you forget to install it or is your package.json in a different location?" }, schema: [ { type: "object", properties: { packageJsonLocation: { type: "string" }, ignore: { type: "array", items: { type: "string" } } } } ] }, create(context) { let { packageJsonLocation, ignore } = context.options.reduce( (acc, val) => ({ packageJsonLocation: val.packageJsonLocation || acc.packageJsonLocation, ignore: val.ignore || acc.ignore }), { packageJsonLocation: "", ignore: [] } ); function excludeNullable(item) { return !!item; } let mergeDepsWithDevDeps = (packageJson) => { let deps = Object.keys(packageJson.dependencies || {}), devDeps = Object.keys(packageJson.devDependencies || {}); return [...deps, ...devDeps]; }, isAddonInstalled = (addon, installedAddons) => { let addonName = addon.replace(/\.[mc]?js$/, "").replace(/\/register$/, "").replace(/\/preset$/, ""); return installedAddons.includes(addonName); }, filterLocalAddons = (addon) => !((addonName) => addonName.startsWith(".") || addonName.startsWith("/") || // for local Windows files e.g. (C: F: D:) /\w:.*/.test(addonName) || addonName.startsWith("\\"))(addon), areThereAddonsNotInstalled = (addons, installedSbAddons) => { let result = addons.filter(filterLocalAddons).filter((addon) => !isAddonInstalled(addon, installedSbAddons) && !ignore.includes(addon)).map((addon) => ({ name: addon })); return result.length ? result : !1; }, getPackageJson = (path2) => { let packageJson = { devDependencies: {}, dependencies: {} }; try { let file = readFileSync(path2, "utf8"), parsedFile = JSON.parse(file); packageJson.dependencies = parsedFile.dependencies || {}, packageJson.devDependencies = parsedFile.devDependencies || {}; } catch { throw new Error( import_ts_dedent.dedent`The provided path in your eslintrc.json - ${path2} is not a valid path to a package.json file or your package.json file is not in the same folder as ESLint is running from. Read more at: https://github.com/storybookjs/storybook/blob/next/code/lib/eslint-plugin/docs/rules/no-uninstalled-addons.md ` ); } return packageJson; }, extractAllAddonsFromTheStorybookConfig = (addonsExpression) => { if (addonsExpression?.elements) { let nodesWithAddons = addonsExpression.elements.map((elem) => isLiteral(elem) ? { value: elem.value, node: elem } : void 0).filter(excludeNullable), listOfAddonsInString = nodesWithAddons.map((elem) => elem.value), nodesWithAddonsInObj = addonsExpression.elements.map((elem) => isObjectExpression(elem) ? elem : { properties: [] }).map((elem) => { let property = elem.properties.find( (prop) => isProperty(prop) && isIdentifier(prop.key) && prop.key.name === "name" ); return isLiteral(property?.value) ? { value: property.value.value, node: property.value } : void 0; }).filter(excludeNullable), listOfAddonsInObj = nodesWithAddonsInObj.map((elem) => elem.value), listOfAddons = [...listOfAddonsInString, ...listOfAddonsInObj], listOfAddonElements = [...nodesWithAddons, ...nodesWithAddonsInObj]; return { listOfAddons, listOfAddonElements }; } return { listOfAddons: [], listOfAddonElements: [] }; }; function reportUninstalledAddons(addonsProp) { let packageJsonPath = resolve(packageJsonLocation || "./package.json"), packageJsonObject; try { packageJsonObject = getPackageJson(packageJsonPath); } catch (e) { throw new Error(e); } let depsAndDevDeps = mergeDepsWithDevDeps(packageJsonObject), { listOfAddons, listOfAddonElements } = extractAllAddonsFromTheStorybookConfig(addonsProp), result = areThereAddonsNotInstalled(listOfAddons, depsAndDevDeps); if (result) { let elemsWithErrors = listOfAddonElements.filter( (elem) => !!result.find((addon) => addon.name === elem.value) ), currentPackageJsonPath = `${process.cwd().split(sep).pop()}${sep}${relative(process.cwd(), packageJsonLocation)}`; elemsWithErrors.forEach((elem) => { context.report({ node: elem.node, messageId: "addonIsNotInstalled", data: { addonName: elem.value, packageJsonPath: currentPackageJsonPath } }); }); } } function findAddonsPropAndReport(node) { let addonsProp = node.properties.find( (prop) => isProperty(prop) && isIdentifier(prop.key) && prop.key.name === "addons" ); addonsProp?.value && isArrayExpression(addonsProp.value) && reportUninstalledAddons(addonsProp.value); } return { AssignmentExpression: function(node) { isObjectExpression(node.right) && findAddonsPropAndReport(node.right); }, ExportDefaultDeclaration: function(node) { let meta = getMetaObjectExpression(node, context); if (!meta) return null; findAddonsPropAndReport(meta); }, ExportNamedDeclaration: function(node) { let addonsProp = isVariableDeclaration(node.declaration) && node.declaration.declarations.find( (decl) => isVariableDeclarator(decl) && isIdentifier(decl.id) && decl.id.name === "addons" ); addonsProp && isArrayExpression(addonsProp.init) && reportUninstalledAddons(addonsProp.init); } }; } }); // src/rules/prefer-pascal-case.ts import { isExportStory as isExportStory2 } from "storybook/internal/csf"; import { ASTUtils as ASTUtils4 } from "@typescript-eslint/utils"; var prefer_pascal_case_default = createStorybookRule({ name: "prefer-pascal-case", defaultOptions: [], meta: { type: "suggestion", fixable: "code", hasSuggestions: !0, severity: "warn", docs: { description: "Stories should use PascalCase", categories: ["recommended" /* RECOMMENDED */] }, messages: { convertToPascalCase: "Use pascal case", usePascalCase: "The story should use PascalCase notation: {{name}}" }, schema: [] }, create(context) { let isPascalCase = (str) => /^[A-Z]+([a-z0-9]?)+/.test(str), toPascalCase = (str) => str.replace(new RegExp(/[-_]+/, "g"), " ").replace(new RegExp(/[^\w\s]/, "g"), "").replace( new RegExp(/\s+(.)(\w+)/, "g"), (_, $2, $3) => `${$2.toUpperCase() + $3.toLowerCase()}` ).replace(new RegExp(/\s/, "g"), "").replace(new RegExp(/\w/), (s) => s.toUpperCase()), getModuleScope = () => { let { sourceCode } = context; return sourceCode.getScope ? sourceCode.scopeManager?.scopes?.find( (scope) => scope.type === "module" ) : context.getScope().childScopes.find((scope) => scope.type === "module"); }, checkAndReportError = (id, nonStoryExportsConfig2 = {}) => { let { name } = id; if (!isExportStory2(name, nonStoryExportsConfig2) || name === "__namedExportsOrder") return null; !name.startsWith("_") && !isPascalCase(name) && context.report({ node: id, messageId: "usePascalCase", data: { name }, suggest: [ { messageId: "convertToPascalCase", *fix(fixer) { let suffix = context.getSourceCode().text.slice(id.range[0], id.range[1]).substring(name.length), pascal = toPascalCase(name); yield fixer.replaceTextRange(id.range, pascal + suffix); let scope = getModuleScope(); if (scope) { let variable = ASTUtils4.findVariable(scope, name), referenceCount = variable?.references?.length || 0; for (let i = 0; i < referenceCount; i++) { let ref = variable?.references[i]; ref && !ref.init && (yield fixer.replaceTextRange(ref.identifier.range, pascal)); } } } } ] }); }, meta, nonStoryExportsConfig, namedExports = [], hasStoriesOfImport = !1; return { ImportSpecifier(node) { "name" in node.imported && node.imported.name === "storiesOf" && (hasStoriesOfImport = !0); }, ExportDefaultDeclaration: function(node) { if (meta = getMetaObjectExpression(node, context), meta) try { nonStoryExportsConfig = { excludeStories: getDescriptor(meta, "excludeStories"), includeStories: getDescriptor(meta, "includeStories") }; } catch { } }, ExportNamedDeclaration: function(node) { if (!node.declaration) return; let decl = node.declaration; if (isVariableDeclaration(decl)) { let declaration = decl.declarations[0]; if (declaration == null) return; let { id } = declaration; isIdentifier(id) && namedExports.push(id); } }, "Program:exit": function() { namedExports.length && !hasStoriesOfImport && namedExports.forEach((n) => checkAndReportError(n, nonStoryExportsConfig)); } }; } }); // src/rules/story-exports.ts var story_exports_default = createStorybookRule({ name: "story-exports", defaultOptions: [], meta: { type: "problem", severity: "error", docs: { description: "A story file must contain at least one story export", categories: ["recommended" /* RECOMMENDED */, "csf" /* CSF */] }, messages: { shouldHaveStoryExport: "The file should have at least one story export", shouldHaveStoryExportWithFilters: "The file should have at least one story export. Make sure the includeStories/excludeStories you defined are correct, otherwise Storybook will not use any stories for this file.", addStoryExport: "Add a story export" }, fixable: void 0, // change to 'code' once we have autofixes schema: [] }, create(context) { let hasStoriesOfImport = !1, nonStoryExportsConfig = {}, meta, namedExports = []; return { ImportSpecifier(node) { "name" in node.imported && node.imported.name === "storiesOf" && (hasStoriesOfImport = !0); }, ExportDefaultDeclaration: function(node) { if (meta = getMetaObjectExpression(node, context), meta) try { nonStoryExportsConfig = { excludeStories: getDescriptor(meta, "exclude