Storybook ESLint 规则 no-stories-of 深度解读:告别 storiesOf,全面拥抱 CSF
no-stories-of 是 eslint-plugin-storybook(本仓库 code/lib/eslint-plugin)中的一条核心规则,它的职责非常单一而坚决:只要代码里从 Storybook 渲染器导入 storiesOf,就直接报错。这条规则存在的根本原因,是 Storybook 自 5.2 版本起以 Component Story Format(CSF)作为编写 story 的推荐方式,而 storiesOf 这套旧式 API 已被官方移除。读完本文,你将完整掌握该规则的判定逻辑、启用方式(csf-strict / flat/csf-strict)、正确的 CSF 写法,以及使用官方 codemod 一键把存量 storiesOf 代码迁移到 CSF 的实战流程。
一、背景:为什么 storiesOf 必须被淘汰
在 Storybook 的早期版本中,storiesOf 是组织与声明 story 的标准写法。它的特征是通过链式调用 .add() 动态地往某个层级中追加 story,其 API 形态大致如下:
import { storiesOf } from '@storybook/react';
import Button from '../components/Button';
storiesOf('Button', module).add('primary', () => <Button primary />);
从 Storybook 5.2 开始,官方引入了 Component Story Format(CSF),把"一个 story"重新定义为模块顶层的一个具名导出。相比 storiesOf 那种"在模块执行时动态注册"的命令式 API,CSF 是完全声明式、静态可分析的:story 文件本身就是一个普通的 ES Module,story 就是导出值,元信息通过 default 导出声明。
正如该规则的官方说明 no-stories-of.md 所述:从引入 CSF 开始,storiesOf 就处于被弃用状态,如今该 API 已从 Storybook 中移除。因此任何新代码都不应再使用它,存量代码也应尽快迁移。
二、规则速览:属于哪套配置、报什么错
这条规则由 Yann Braga 维护,实现在 src/rules/no-stories-of.ts。规则元信息(meta)中明确标注了以下关键属性:
| 属性 | 值 | 说明 |
|---|---|---|
| rule name | no-stories-of |
规则全名,在配置中写作 storybook/no-stories-of |
| type | problem |
归类为"代码确实有问题",而非风格(suggestion)类 |
| severity | error |
规则自身的默认严重级别为 error |
| categories | csf-strict |
通过 CategoryId.CSF_STRICT 常量挂载 |
| messages.doNotUseStoriesOf | storiesOf is deprecated and should not be used |
触犯规则时输出的报错文案 |
| options schema | [] |
该规则不接受任何可配置参数 |
也就是说,这条规则是"一刀切"且无选项的——它不接受任何开关或白名单,一旦命中就报错。规则文档首部的自动生成区块还标注了它的配置归属:
Included in these configurations:
csf-strict、flat/csf-strict
这意味着只要你启用了 eslint-plugin-storybook 的 csf-strict 配置(legacy 模式)或 flat/csf-strict(ESLint flat config 模式),该规则就会自动以 error 级别生效,无需手动逐条声明。
三、违规与合规示例:规则到底盯住什么
3.1 违规代码(incorrect)
规则文档给出的典型违规示例就是从渲染器导入 storiesOf 并使用链式 .add():
import { storiesOf } from '@storybook/react';
import Button from '../components/Button';
storiesOf('Button', module).add('primary', () => <Button primary />);
3.2 合规代码(correct)之一:CSF 函数式 story
把上面的例子改写为 CSF:用 default 导出声明元信息(component),再把每个 story 变成具名导出(如 Primary):
import Button from "../components/Button";
export default {
component: Button
};
export const Primary = () => <Button primary />;
3.3 合规代码(correct)之二:CSF 对象式 story(带 args)
另一种等价但更推荐的 CSF 写法是把 story 声明为对象,通过 args 描述传入组件的属性。这样 story 变得完全可序列化,便于 controls、测试和复用:
import Button from "../components/Button";
export default {
component: Button
};
export const Primary = {
args: {
primary: true
}
};
说明:规则文档中的正确示例保留了
export default = {...}的字样,那是文档笔误——ES Module 的默认导出语法应为export default {...}(本仓库规则测试用例 no-stories-of.test.ts 中的合法示例均使用export default {)。上面两段代码已修正为可真实运行的形式。
3.4 从测试用例看边界的精确判定
在 src/rules/no-stories-of.test.ts 中,该规则被标注为 valid 的用例包括:
import Button from '../components/Button';
export default {
title: 'Button',
component: Button
} as ComponentMeta<typeof Button>;
export const Primary: Story = () => <Button primary />;
也就是说,只要不存在 storiesOf 的具名导入,即使是带 ComponentMeta / Story 类型标注的 TS 版 CSF 也完全合法。而被标记为 invalid 的用例则准确命中错误节点类型为 ImportSpecifier:
import { storiesOf } from '@storybook/react';
import Button from '../components/Button';
storiesOf('Button', module)
.add('primary', () => <Button primary />)
四、规则的实现机制:它在 AST 上做了什么
阅读 src/rules/no-stories-of.ts 的源码可以看到,规则逻辑非常精简:它基于 @typescript-eslint/utils 的 RuleCreator(封装在 create-storybook-rule.ts 中),只注册了一个 AST 节点监听器 ImportSpecifier:
return {
ImportSpecifier(node) {
if ('name' in node.imported && node.imported.name === 'storiesOf') {
context.report({
node,
messageId: 'doNotUseStoriesOf',
});
}
},
};
从这段实现可以提炼出三个关键事实:
- 报错点精确到 import 语句本身。规则监听的是 ES Module 的具名导入节点(
ImportSpecifier),当imported的名称恰为storiesOf时,直接把错误报告标记在该导入节点上。 - 仅针对"具名导入"路径拦截。判断条件是
imported.name === 'storiesOf',因此任何import { storiesOf } from '@storybook/xxx'形式(React、Vue3、Angular、Web Components 等渲染器)都会被捕获。 - 命中即报错、不做二次分析。规则没有去分析后续是否真的调用了
storiesOf(...).add(...),因为只要引入了该 API 基本可以断定是在编写旧式 stories;报错文案直接复用消息模板doNotUseStoriesOf。
这种"极简拦截 + 明确报错"的设计正是该规则能够稳定运行的原因:它只依赖 import 语句就能完成判定,不涉及复杂的数据流分析。
五、如何启用该规则
5.1 方式一:整包启用 csf-strict 配置(推荐)
先按 eslint-plugin-storybook/README.md 的指引安装插件。插件本身已经打包了所需的 CSF 辅助工具,无需为了加载该 ESLint 插件而额外安装 storybook(例如在 monorepo 的共享 ESLint preset 中这一点很关键):
npm install eslint-plugin-storybook --save-dev
# 或
yarn add eslint-plugin-storybook --dev
随后在 legacy 版 .eslintrc 中直接继承 csf-strict:
{
"extends": ["plugin:storybook/csf-strict"]
}
在 flat config(eslint.config.js)中则引入对应的 flat 配置:
import storybook from 'eslint-plugin-storybook';
export default [
...storybook.configs['flat/csf-strict'],
// ... 其余配置
];
在 code/lib/eslint-plugin/src/configs/csf-strict.ts 与 code/lib/eslint-plugin/src/configs/flat/csf-strict.ts 中可以看到该配置的具体内容:它基于 csf 配置扩展,并对 story 文件开启了额外的严格规则,其中就包括 'storybook/no-stories-of': 'error':
rules: {
'react-hooks/rules-of-hooks': 'off',
'import-x/no-anonymous-default-export': 'off',
'storybook/no-stories-of': 'error',
'storybook/no-title-property-in-meta': 'error',
}
同时,两个配置文件都限定了规则的适用文件范围,只针对 story 源码:
files: ['**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)', '**/*.story.@(ts|tsx|js|jsx|mjs|cjs)']
5.2 方式二:在任意配置中单独开启
如果你不想整体切换 csf-strict,也可以只对 story 文件单独开启这一条:
{
"overrides": [
{
"files": ["**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)", "**/*.story.@(ts|tsx|js|jsx|mjs|cjs)"],
"rules": {
"storybook/no-stories-of": "error"
}
}
]
}
5.3 别忘了处理 .storybook 目录
根据插件 README 的提醒,建议在 .eslintignore(或 flat config 的 ignores)中放行 .storybook 目录,让插件能同时检查其中的配置文件(例如 addon 名称拼写):
!.storybook
六、存量代码自动迁移:官方的 storiesof-to-csf codemod
手动逐文件改写存量 storiesOf 代码既繁琐又容易出错。规则文档提供了官方的一键迁移命令——在项目根目录执行即可:
npx storybook@latest migrate storiesof-to-csf --glob="*/**/*.stories.@(tsx|jsx|ts|js)"
对该命令的几个要点说明:
npx storybook@latest会临时拉取最新版 Storybook CLI 并执行其内置的migrate子命令;storiesof-to-csf是迁移名称,代表"把storiesOf写法改写成 CSF 写法"这一转换;--glob用于限定需要扫描/改写哪些文件,默认覆盖*.stories.tsx/jsx/ts/js四类典型 story 文件。若你的项目同时存在*.story.*后缀,可以按需调整 glob 模式(例如追加*.story.@(tsx|jsx|ts|js))。
从本仓库源码结构看,Storybook 的 codemod 转换器集中在 code/lib/codemod/src/transforms 目录下,例如 csf-2-to-3、upgrade-hierarchy-separators 等。其中 upgrade-hierarchy-separators 的测试夹具(code/lib/codemod/src/transforms/testfixtures/upgrade-hierarchy-separators)就包含 dynamic-storiesof.input.js、storiesof.input.js 等以 storiesOf 语法作为输入的样例,可见 Storybook 的迁移工具链长期以来都围绕"消化 storiesOf 旧语法"做了大量适配。如果你在迁移后还想进一步把老式 CSF 规范成当前推荐写法,可以继续关注同一目录下的 csf-2-to-3 迁移。
七、迁移后的自查要点
完成迁移与开启规则后,建议对照以下清单自查,确保存量与新增代码都不再触碰 storiesOf:
- 全局搜索:在项目中搜索
storiesOf(与from '@storybook/*'中是否仍残留storiesOf具名导入,确保import { storiesOf }不再出现。 - lint 验证:运行
eslint "*/**/*.stories.@(tsx|jsx|ts|js)",确认storybook/no-stories-of无任何 error。 - 结构校验:每个 story 文件都应具备
export default(声明component或title)与至少一个 story 的具名导出;story 既可以是函数(export const Primary = () => <Button primary />),也可以是带args的对象(export const Primary = { args: { primary: true } })。
遵守 no-stories-of,本质上是让 story 从"运行时动态注册"走向"静态可分析的模块导出"。这不仅能获得 ESLint、TypeScript 与各类工具链的静态分析支持,也让 story 作为组件的可复用输入(args)得以在测试、文档与可视化调试之间顺畅流动——这正是 Storybook 在 CSF 时代推荐的最佳实践。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00