
Storybook Codemods基于 JSCodeshift 的 Story 文件批量迁移工具【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook本文以 Storybook 仓库中的storybook/codemod包code/lib/codemod/README.md为核心讲解这套基于 JSCodeshift 编写的 codemod 脚本集合是如何工作的既讲清楚了 README 中记录的 CLI 集成方式与手工运行方式也深入到code/lib/codemod/src源码解析sb migrate命令的完整执行链路、四个 transform 的实现细节与参数含义帮助你在执行 Storybook 大版本迁移层级分隔符、CSF 2 → 3、废弃类型清理时能够准确选择并安全运行对应的 codemod。Codemods 是什么Storybook Codemods 是一组用 JSCodeshift一个基于 AST 的 JavaScript 代码变换引擎编写的自动化迁移脚本用于帮助开发者批量处理 Storybook 大版本升级中的 breaking changes 与 deprecations。仓库中它对应storybook/codemod包位于 code/lib/codemod核心结构为src/index.ts导出listCodemods/runCodemod两个入口函数供 CLI 调用src/transforms/每个 transform 是一个独立的 JSCodeshift 变换脚本src/lib/utils.ts辅助工具命名清理、jscodeshift parser 到 prettier parser 的映射package.json通过exports字段暴露各 transform如./transforms/csf-2-to-3、./transforms/upgrade-hierarchy-separators。其中jscodeshift是执行迁移的引擎storybook/codemod则提供具体的变换逻辑两者分工明确。CLI 集成通过sb migrate运行推荐方式README 推荐的运行方式是通过 Storybook CLI 的migrate命令其实现位于 code/lib/cli-storybook/src/migrate.ts。查看可用的 codemod 列表npx sb migrate --list从源码看--list会调用 src/index.ts 中的listCodemods()它通过readdirSync读取已安装的storybook/codemod/dist/transforms目录过滤出所有.js文件并去掉后缀作为 codemod 名称输出。这意味着运行时实际可用的 codemod 列表取决于你安装的包版本中dist/transforms目录的内容这也是为什么升级 Storybook 版本后建议重新执行一次--list确认。运行一个 codemodnpx sb migrate name-of-codemod --glob**/*.stories.jsmigrate命令支持的选项见 migrate.ts 中的CLIOptions定义选项含义migration位置参数codemod 名称必须与--list输出一致否则报错Unknown codemod ... Run --list for options--glob匹配要迁移的文件的 glob 模式如**/*.stories.js--list仅列出所有可用 codemod不执行迁移--dry-run试运行只统计匹配文件、不实际写入源码中对应dryRun见下文--rename迁移完成后批量重命名文件格式为from:to例如.js:.ts--parser指定 jscodeshift 解析器取值为babel \| babylon \| flow \| ts \| tsx如果既没有指定 migration 名称也没有--list命令会抛出Migrate: please specify a migration name or --list。手工运行 codemod不依赖 CLIREADME 同时给出了不通过 CLI、直接驱动 jscodeshift 的手工方式适合需要在 CI 或脚本中精细控制的场景yarn add jscodeshift storybook/codemod --devstorybook/codemod是 codemod 脚本集合jscodeshift是执行 codemod 的引擎。随后在安装了两者的目录下运行以upgrade-hierarchy-separators为例./node_modules/.bin/jscodeshift -t ./node_modules/storybook/codemod/dist/transforms/upgrade-hierarchy-separators.js . --ignore-pattern node_modules|dist命令结构解析jscodeShiftCommand -t transformFileLocation pathToSource --ignore-pattern globPatternToIgnore-t指定 transform 脚本位置注意必须是dist/transforms/下编译后的.js第四个参数是源码路径示例中为当前目录.--ignore-pattern用 glob 模式排除不需要扫描的目录。迁移完成后README 建议可以把这两个包从package.json中移除因为它们只是迁移期工具。sb migrate的底层执行链路runCodemod的实现src/index.ts解释了 CLI 方式相比手工方式多做了什么校验名称先调用listCodemods()若传入的 codemod 不在列表中直接抛错解析 rename 参数若提供了--rename必须满足from:to两段式格式否则报Codemod rename: expected format from:to解析文件列表用tinyglobby对--glob求值且自动追加!**/node_modules与!**/dist两条排除规则若匹配到 0 个文件打印No matching files for glob后直接返回推断 parser若未显式传--parser会从 glob 的文件扩展名推断——src/lib/utils.ts 中jscodeshiftToPrettierParser维护了映射表babylon→babel、flow→flow、ts/tsx→typescript默认babel仅当推断结果不是babel时才会向 jscodeshift 传--parserspawn 执行 jscodeshift通过cross-spawn同步启动node jscodeshift/bin/jscodeshift --no-babel --extensions文件实际扩展名列表 --fail-on-error -t TRANSFORM_DIR/codemod.js [--parser x] 匹配到的文件列表几个值得注意的细节--no-babel是为了避免 codeshift 用 babel 解析自身依赖从而变慢并刷出[BABEL]警告--extensions传的是实际匹配文件的扩展名并集逗号分隔保证混合 js/ts 文件也能正确解析--fail-on-error使单个文件报错时整体退出码非零错误与重命名处理若 jscodeshift 退出码为 1则打印Skipped renaming because of errors并中止不会在代码出错的情况下重命名文件成功后若配置了--rename会并发地对每个文件执行from→to的重命名并逐条打印Rename: ...日志。源码中还有一个针对mdx-to-csf的特殊分支该 codemod 失败时会将无法转换的文件改名为.mdx.broken并提示用户手工处理后改回。当前仓库的 transforms 目录中未包含该脚本这是为兼容更高版本包而保留的处理逻辑。Transforms 详解README 专门记录了两个 transform 的用途与示例结合当前源码树code/lib/codemod/src/transforms现共有四个 transform。upgrade-hierarchy-separators5.3 起统一层级分隔符为/从 5.3 开始Storybook 改用单一路径分隔符/表示 story 层级旧版中|用于 story “roots”可选/或.用于表示路径。该 codemod 把旧写法统一更新为新写法storiesOf(Foo|Bar/baz); storiesOf(Foo.Bar.baz); export default { title: Foo|Bar/baz.whatever, };迁移后变为storiesOf(Foo/Bar/baz); storiesOf(Foo/Bar/baz); export default { title: Foo/Bar/baz/whatever, };./node_modules/.bin/jscodeshift -t ./node_modules/storybook/codemod/dist/transforms/upgrade-hierarchy-separators.js . --ignore-pattern node_modules|dist从实现看upgrade-hierarchy-separators.js它只做一件事upgradeSeparator用正则/[|.]/g把|和.全部替换为/。AST 层面它处理两类节点storiesOf(...)调用仅当第一个实参是字符串字面量Literal/StringLiteral时才改写动态拼接的 title 会被跳过测试夹具__testfixtures__/upgrade-hierarchy-separators/dynamic-storiesof.input.js正是验证这一保守行为export default { title: ... }仅改写 CSF 对象中名为title的字符串属性。输出通过root.toSource({ quote: single })保持单引号风格。测试与快照见 code/lib/codemod/src/transforms/testfixtures/upgrade-hierarchy-separators。csf-hoist-story-annotations6.0 起.story注解提升README 记录的第二个 transform 对应 6.0 的变更CSF 中story.story对象注解被废弃改为“提升注解”hoisted annotations直接挂在导出函数上export const Basic () Button / Basic.story { name: foo, parameters: { ... }, decorators: [ ... ], };迁移后export const Basic () Button / Basic.storyName foo; Basic.parameters { ... }; Basic.decorators [ ... ];README 指出新语法更紧凑、更顺手且与 React 的displayName/propTypes/defaultProps注解风格一致。运行命令额外加了--extensionsjs限定扩展名./node_modules/.bin/jscodeshift -t ./node_modules/storybook/codemod/dist/transforms/csf-hoist-story-annotations.js . --ignore-pattern node_modules|dist --extensionsjscsf-2-to-3从 CSF 2 函数式 story 迁移到 CSF 3 对象式 story当前源码中功能最重的 transform 是 csf-2-to-3.ts它把 CSF 2 的“函数导出 逐条注解”形式转换为 CSF 3 的“导出对象字面量”形式。其工作流可以从源码中归纳为解析 CSF调用storybook/internal/csf-tools的loadCsf解析整个文件得到_meta、_storyExports、_storyAnnotations等结构化信息若解析失败则原样返回保证不会破坏坏文件识别 story 形态对每个 story 导出如export const A Template.bind({})或箭头函数区分三种情况——简单 story无参箭头函数且无任何注解直接改写类型标注为StoryFn模板绑定识别Template.bind({})形式将render指向模板变量与全局 render 重复的 story若 meta 声明了component: Cat而 story 恰好是(args) Cat {...args} /这种纯透传函数则删除冗余的render见isReactGlobalRenderFn其余情况生成export const A: StoryObj { render, ...annotations }对象导出清理删除已被提升进对象的注解语句如A.parameters {...}、删除不再被引用的模板变量removeUnusedTemplates、移除废弃的Story类型导入类型升级内嵌调用upgradeDeprecatedTypes完成下一节的类型迁移格式化printCsf输出后再用prettier.format并尝试读取项目自身 prettier 配置重排失败时仅告警、不中断。值得注意的是该 transform 明确不支持命名空间导入遇到import * as React from ...式的 renderer 导入会抛出This codemod does not support namespace imports并要求先改写为具名导入。upgrade-deprecated-types废弃类型重命名upgrade-deprecated-types.ts 针对storybook/*包中已废弃的类型标识符映射关系为废弃类型迁移为StoryStoryFnComponentStoryStoryFnComponentStoryFnStoryFn去Component前缀ComponentStoryObjStoryObjComponentMetaMeta它的处理逻辑upgradeDeprecatedTypes函数分两步遍历第一步扫所有storybook开头的 import替换废弃的导入名若新类型名已存在则去重移除若存在同名的别名导入则抛出带 code frame 的明确错误提示重命名本地导入后重试第二步扫TSTypeReference替换所有类型引用位置包括SB.StoryObj这种命名空间限定写法import * as SB。该函数同时被csf-2-to-3复用保证两条迁移路径的类型处理一致。find-implicit-spiesplay 函数中的隐式 spy 审计find-implicit-spies.ts 是一个只审计、不改写的 codemod它遍历每个 story 的play函数兼容 CSF 2 的Story.play ...与 CSF 3 的const Story { play: ... }两种形态找出形如on[A-Z]...的标识符——这类命名通常是args中声明的事件回调。如果某个onXxx既不在 meta 的args/argTypes里、也不在该 story 的args/argTypes里就会打印带 code frame 的警告Possible implicit spy found提示该回调可能是一个没有被显式声明的隐式 spy建议显式声明。这为接入storybook/test的expect(...).toHaveBeenCalled()断言前的代码清理提供了入口。文档与当前源码的差异说明需要提醒的是README 的 Transforms 章节只完整记录了upgrade-hierarchy-separators与csf-hoist-story-annotations两个 transform而当前仓库src/transforms目录实际包含csf-2-to-3、find-implicit-spies、upgrade-deprecated-types、upgrade-hierarchy-separators四个实现。可以推断 README 相对当前版本有所滞后这也是src/index.ts中listCodemods()采用“运行时扫描 dist 目录”而非硬编码列表的原因——以npx sb migrate --list的输出作为可用 codemod 的权威清单是处理版本差异的最稳妥做法。实操建议先 dry-run 再执行CLI 的--dry-run只统计匹配文件数、不写盘适合先验证--glob范围是否正确在干净的工作区运行所有 transform 都是原地改写文件运行前确保 git 工作区可提交、可回滚利用--fail-on-error语义CLI 链路上 jscodeshift 带--fail-on-error执行任一文件失败整体即报错并跳过 rename因此出现失败日志时不要手动重命名先修复源文件再重跑缩小 glob 范围相比手工方式对整个.目录扫描sb migrate --glob**/*.stories.{ts,tsx,js,jsx}只处理 story 文件更快也更安全node_modules与dist已被内置排除规则覆盖。小结storybook/codemod是 Storybook 版本迁移的工程化抓手sb migrate命令code/lib/cli-storybook/src/migrate.ts封装了 glob 解析、parser 推断、失败中止与安全重命名等细节code/lib/codemod/src/index.ts而四个 transform 分别覆盖了层级分隔符统一、CSF 2→3 对象化、废弃类型重命名与隐式 spy 审计四类典型迁移需求。掌握--list/--dry-run/--rename/--parser各选项与 transform 的 AST 级行为边界如动态 title 不处理、命名空间导入会报错就能在大版本升级中把大量机械性改动交给自动化完成并将人工精力集中在 codemod 明确跳过的文件上。【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考