Skip to content

fix(codemod): run migrate on Storybook 11's csf-tools - #202

Draft
kasperpeulen wants to merge 20 commits into
mswjs:mainfrom
kasperpeulen:storybook-11-csf-tools
Draft

kasperpeulen wants to merge 20 commits into
mswjs:mainfrom
kasperpeulen:storybook-11-csf-tools

Conversation

@kasperpeulen

@kasperpeulen kasperpeulen commented Oct 20, 2026 •

Copy link
Copy Markdown

Storybook 11 replaces Babel with oxc-parser in its CSF tools. The storybook/internal/babel entry point is gone, and storybook/internal/csf-tools now works on ESTree nodes and edits the source text instead of reprinting it. The migrate CLI imports Babel from Storybook and edits CsfFile._ast and ConfigFile._ast, so on Storybook 11 npx msw-storybook-addon migrate crashes as soon as it loads.

migrate upgrades a 2.x setup, and most projects that run it are on Storybook 9 or 10. So the codemod now has two implementations. The Babel one from main stays as it is for Storybook 9 and 10. A new one runs on Storybook 11, on the public csf-tools API of storybookjs/storybook#36613 and #36947, which the Storybook 11 migration notes describe. On Storybook 9 and 10 the transforms are the ones from main. The only difference there is that the summary now prints on every version.

Reading the diff

Start with codemod/src/bin.ts. At startup it imports storybook/internal/csf-tools and checks for parseModule. If it is there, it loads transforms/estree, otherwise transforms/babel. Both are imported only after that check, because neither loads on the other Storybook. Both export the same three functions with the same result types, so the file handling and the report stay in one place. loadTransforms is typed by the Babel module, so a result the ESTree path widens fails to compile.

codemod/src/transforms/babel is the code from main, moved unchanged.

The two paths share what is the same on every Storybook: bin.ts finds the files, runs the transforms, checks whether a file is up to date and prints the report. Inside the transforms almost every step reads or builds nodes, and the nodes differ. Babel has StringLiteral, ObjectProperty and AST builders; ESTree has Literal, Property and source text. Only parsing, printing and isCsfFactoryPreview mean the same on both. setImport and removeImport exist on both but differ: on Storybook 10 they edit only the first import of a module and count a type-only import as present. Finding a CSF2 story's assignments, reading parameters.msw, building beforeEach and editing the initialize(...) statement have no API on both. Even in main.ts, the smallest transform, a shared version would keep a dozen lines of control flow and pass three node callbacks for the rest, and the Babel path would no longer be the code from main. So each transform is complete on its own.

codemod/src/transforms/estree/stories.ts edits story files. CsfFile.objects() returns one editor per meta and story. A CSF2 story's Story.parameters = ... and Story.beforeEach = ... assignments are fields of that editor, so CSF2, CSF3 and CSF factories share one code path. inspect reads parameters.msw and beforeEach. One transaction per story removes the parameter and sets the hook. When csf-tools refuses either edit, both roll back and the story is reported as unrecognized-shape.

codemod/src/transforms/estree/preview.ts runs the whole migration in one transaction, split into a CSF 3 half and a CSF Next half. Fields, their array elements and the arguments of calls in them go through ConfigFile paths such as ['loaders', 0], with set, remove, append and prepend. Imports go through setImport and removeImport. Only the initialize(...) statement is not part of the config, so it is removed through the file's SourceEditor. References to initialize are looked up through the file's scopes.

codemod/src/transforms/estree/main.ts reads addons with getFieldNode and adds the addon with append, wrapped in the same resolver call as the other entries if they have one. getFieldNode also reads a config csf-tools will not edit, so an addon already registered there is found. When csf-tools refuses the edit, the CLI reports an error and leaves the file alone.

The tests are split the same way, into codemod/__tests__/babel and codemod/__tests__/estree. Like the CLI, codemod/__tests__/csf-tools.ts checks for parseModule. Each suite runs on the Storybook it is written for and is skipped on the other, with the reason in its name. A vitest global setup does the check once and prints which suite runs. The Babel tests are the ones from main, 65 of them. The ESTree suite starts from the same fixtures, changes them only for the output differences listed below, and has 94 tests.

bin.ts also fixes a crash on Storybook 9 and 10.0: their logger has no CLI_COLORS.muted, so migrate threw right after writing the files, before printing its summary and warnings. The report heading for skipped stories now reads "Could not migrate parameters.msw safely", which fits the stories the ESTree path skips for a spread too.

What changes for users

On Storybook 9 and 10, only the summary fix above. The rest of this section is about the ESTree path on Storybook 11, compared with the Babel path.

The CLI still runs Prettier over every file it writes when the project has a Prettier config or an .editorconfig, so most output differences only show in projects without either:

  • beforeEach is written as beforeEach: ({ msw }) => { ... } after the object's other fields, where the Babel path writes a method before parameters.
  • Untouched lines keep their trailing commas and semicolons, and no blank lines are added.
  • New imports, the retargeted mswLoader import included, follow the file's quotes and semicolons and go below the existing imports.
  • New statements, such as those in the setup function and a CSF2 Story.beforeEach = ..., follow the file's semicolon style.
  • New loaders and addons fields go at the end of the config object. A one-line object that gets a multi-line field is laid out one member per line.

Some files are migrated differently:

  • The v1 Story.story = { parameters } form is left alone. Storybook 11 no longer reads .story annotations, and storybook migrate csf-2-to-3 lifts them out first.
  • A story, meta or preview with a spread that could hold beforeEach or shadow parameters.msw, such as { ...Base, parameters: { msw } }, is skipped with a warning.
  • A preview that csf-tools cannot edit safely is skipped with a warning. Examples are a spread next to a field the codemod changes, like { ...base, decorators: [mswDecorator] }, a config binding that is also referenced elsewhere, and a config wrapped in a call other than definePreview. A main config like that gets an error in the report, and so does export default makeConfig(). The Babel path migrates these, and counts makeConfig() as up to date.
  • A loaders() method in the preview gets a warning, where the Babel path adds a second loaders next to it. A story whose parameters.msw is a method is reported as skipped; the Babel path ignores it.
  • Only the ESTree path migrates export { meta as default } metas and stories exported as export { A as B } or export { A }.
  • For export const A = { args } followed by A.parameters = { msw }, the ESTree path puts beforeEach into the object and removes the statement. The Babel path adds an A.beforeEach = ... statement.
  • A nested binding named initialize, such as a function parameter, does not make the preview skip.
  • loaders: sharedLoaders next to initialize(options) is skipped with a warning. The Babel path removes the initialize call and loses its options.
  • A bare addons: [addonMsw] becomes addonMsw(setup) when there are initialize options to fold. The Babel path loses the options the same way.
  • A main config whose addons can only come from a spread, like { ...shared, framework }, gets addons: [...(shared.addons ?? []), 'msw-storybook-addon']. The Babel path writes a new addons that replaces the shared ones.
  • Story files with Flow syntax are left alone and counted as up to date, because Storybook 11 cannot parse Flow.

How it was checked

pnpm test:codemod runs the suite that matches the installed Storybook:

  • Babel path, against the 11.0.0-alpha.0 dev dependencies, which still ship the Babel csf-tools, and against Storybook 10.6.3: 65 tests pass and 94 are skipped. Against 9.1.21 one Babel test fails, as it does on main; see "Before merging".
  • ESTree path, against the pkg.pr.new canary of storybookjs/storybook next at b26f35e, installed by hand: 94 tests pass and 65 are skipped. The canary reports version 11.0.0-alpha.7, but unlike that npm release it has #36613 and #36947.

To run the ESTree tests, install the canary as the Storybook dev dependencies. Its packages depend on each other by URL, which pnpm only allows with blockExoticSubdeps: false:

echo 'blockExoticSubdeps: false' >> pnpm-workspace.yaml
P=https://pkg.pr.new/storybookjs/storybook
V=b26f35e
pnpm add -D "storybook@$P/storybook@$V" \
  "@storybook/addon-vitest@$P/@storybook/addon-vitest@$V" \
  "@storybook/react-vite@$P/@storybook/react-vite@$V"
pnpm test:codemod

Then put the files back:

git checkout package.json pnpm-lock.yaml pnpm-workspace.yaml
pnpm install

pnpm build, pnpm test:ts, pnpm lint, pnpm test:play and pnpm test:stories pass on the dev dependencies, on Storybook 10.6.3 and on the canary. tsc -p codemod/tsconfig.json passes except in the path whose Storybook is not installed. The build writes build/migrate.mjs (11 kB) and one chunk per path. Storybook 9 and 10 load the Babel chunk (35 kB), and Storybook 11 the ESTree chunk (26 kB). main ships 46 kB.

I ran the built CLI on two sample projects with Storybook 9.1.21, 10.6.3 and the canary. One is CSF 3: a main config, a preview with initialize(options) and parameters.msw, and a CSF3 and a CSF2 story. The other is CSF Next: definePreview with the namespace entry automigrate csf-factories injects, preview.meta and meta.story. On all three Storybooks every file migrated, and a second run reported all of them up to date. I ran both again on the b26f35e canary, with the same result.

Before merging

This can be merged and released before Storybook 11 ships. On Storybook 9 and 10 migrate runs the same transforms as on main, and the addon itself does not use csf-tools. The peer range stays ^9.0.0 || ^10.0.0 || ^11.0.0-0.

The Storybook dev dependencies stay on 11.0.0-alpha.0, so CI runs the Babel transforms that Storybook 9 and 10 users run today, and the PR does not depend on a canary. They move to Storybook 11 with its first prerelease that has storybookjs/storybook#36947, and the ESTree tests then run in CI. The ESTree path is picked on parseModule alone, so run its tests again on that prerelease before releasing. The story tests find a sidebar story by its link or by the tree grid row that later Storybook 11 builds render, so they pass on both. The Babel path can go once the addon drops Storybook 9 and 10.

Two problems from main stay, since the Babel path is moved unchanged. On Storybook 9, getFieldNode does not return addons: sharedAddons in a main config, so migrate replaces it with ['msw-storybook-addon'] and the shared addons are lost. And Storybook 9.0.0 to 9.0.11 lack the logger API bin.ts imports, so migrate needs 9.0.12 or later. Both are better fixed in a follow-up.

CI now runs pnpm test:codemod after pnpm test:ts. With the current dev dependencies it runs the Babel suite, and the ESTree suite skips and prints why.

🤖 Generated with Claude Code

kasperpeulen and others added 2 commits October 20, 2026 09:06
Storybook 11 replaces Babel and recast with oxc-parser in its CSF tools
(storybookjs/storybook#36613): `storybook/internal/babel` is removed and
`storybook/internal/csf-tools` exposes ESTree nodes, without
`CsfFile._ast` or `ConfigFile._ast`. The migrate CLI imported the Babel
entry point and edited those trees, so it failed to load on Storybook 11.

The transforms now parse and print with their own @babel/parser and
recast, using the options Storybook 10 used, so their output is
unchanged. Story files still go through `loadCsf` to tell stories and
the factory meta apart, but only export names are read from it. Main and
preview files are no longer read through `ConfigFile`; a small helper
finds the exported config object the way Storybook 10 did.

Babel and recast are bundled into build/migrate.mjs, which is now
minified. A guard test keeps Storybook 10-only API out of codemod/src,
as the Storybook in the dev dependencies still ships Babel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n own Babel

Drops @babel/parser, @babel/types and recast again. Story files are now
edited through `CsfFile.objects()`: `get`/`remove` on `parameters.msw`
and `set` on `beforeEach`, per meta, story and CSF2 `Story.parameters`
assignment. Preview and main fields go through `ConfigFile` (`get`,
`set`, `remove`, `appendNodeToArray`, `appendValueToArray`, `setImport`,
`removeImport`). Array elements, call arguments and the `initialize(...)`
statement, which have no field path, are edited through the file's tree
and csf-tools' editor helpers.

Code the codemod does not touch now stays as written, so the fixtures
change where recast used to reprint it. `beforeEach` is written as an
arrow property after the object's other fields. The v1 `Story.story`
form, which Storybook 11 no longer reads, is left alone.

This only works with Storybook 11's csf-tools and must not be released
before Storybook 11.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every `CsfObject`/`ConfigFile` mutation result is now checked. A
`beforeEach` or `parameters.msw` read that csf-tools cannot prove (a
spread, a duplicate key) skips the story or preview with a warning, and
a refused edit leaves the whole file as it was instead of writing it
with the handlers or the loader half moved. `main` reports a refused
`addons` edit instead of skipping it silently.

Imports are edited with the editor's import helpers, across every value
declaration of a source and never in an `import type`, which
`ConfigFile#setImport`/`removeImport` do not handle. Stories are found
through their binding, so `export { A as B }` works, and an existing
CSF2 `A.beforeEach` is looked up at the top level directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kasperpeulen kasperpeulen changed the title fix(codemod): parse with its own Babel so migrate runs on Storybook 11 refactor(codemod): edit through Storybook 11's csf-tools Oct 20, 2026
@kasperpeulen kasperpeulen changed the title refactor(codemod): edit through Storybook 11's csf-tools fix(codemod): run migrate on Storybook 11's csf-tools Oct 20, 2026
kasperpeulen and others added 14 commits October 20, 2026 12:24
Storybook's csf-tools now covers what the codemod worked around, so the
workarounds go:

- `inspect` tells a missing field from an unreadable one, and
  `transaction` rolls back every edit when one is refused, per story and
  per preview file. The own diagnostic checks and `RefusedEdit` are gone.
- A CSF2 story's `Story.parameters`/`Story.beforeEach` assignments are
  fields of its editor, so the statement lookup and the `_editor` writes
  for CSF2 are gone, and so is the re-derived story discovery.
- Handlers are read from `inspect`, in a `SourceEditor` of their own.
- `set` reindents an existing field, so `loaders: mswLoader` is replaced
  in place instead of removed and added again.
- `setImport`/`removeImport` see every declaration, leave `import type`
  alone and take `'x as y'`, so they replace the hand-written import
  edits.
- `appendValueToArray`/`appendNodeToArray` return a result, which `main`
  checks instead of reading `mutationDiagnostics`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Split the preview migration into its CSF 3 and CSF Next halves and let
the transaction decide whether anything is written, instead of
threading a `changed` flag through every step. Shared node checks
(`isIdentifier`, `isCallOf`, `isRef`, `fieldValue`) replace the
repeated inline type tests and their casts.

Story files use `CsfFile.changed`, and new statements use the editor's
`semicolons` style instead of counting them by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once the dev dependency is a Storybook 11 release with the oxc-based
csf-tools, the type checker reports every pattern the scan looked for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A nested binding named `initialize`, such as a parameter, made the
preview transform skip the file, because every identifier with that
name counted as a use of the import. Read the references of the import
binding instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… configs

A new `Story.beforeEach = ...` statement now follows the file's
semicolon style, and a main config that only spreads another gets
`addons: [...(shared.addons ?? []), 'msw-storybook-addon']`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A CSF2 story whose `parameters` spread another story's was reported as
an unrecognized shape, and a `Story.parameters = { msw }` assignment
to a story object was skipped without a warning. Decide from the
`parameters` field instead, falling back to the object literal when
that field cannot be read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cannot take

`loaders: sharedLoaders` and a bare `addons: [addonMsw]` both lost the
`initialize(...)` options without a word: the call was removed and no
setup function was passed on. A bare `addonMsw` now becomes
`addonMsw(setup)`, and a `loaders` that is not an array literal skips
the file with a warning.

A spread that could hold `beforeEach` in the preview gets its own
warning instead of claiming the preview defines one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both already ignore absent and type-only imports, so the preview no
longer looks the specifiers up first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The transforms import csf-tools APIs that only Storybook 11 has, so on
Storybook 9 or 10 the CLI failed to load with a missing-export error.
Check the installed csf-tools first and load the transforms only when
it has `parseModule`; otherwise say what to run instead and exit 1.

The fallback pins `storybook@10` next to `msw-storybook-addon@3.0.3`,
because npx installs the peer itself and would pick Storybook 11 once
that is `latest`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`migrate` upgrades a 2.x setup, and the projects that run it are on
Storybook 9 and 10. Their csf-tools are Babel-based; Storybook 11's
work on ESTree nodes. Keep both implementations, the Babel one as it is
on main, in `transforms/babel` and `transforms/estree`, and let the CLI
pick by whether the installed csf-tools has `parseModule`. Each one is
imported only after that check, as neither loads on the other
Storybook.

The tests are split the same way. Each suite runs on the Storybook it
is written for and is skipped, with the reason, on the other.

This replaces the guard that stopped `migrate` on Storybook 9 and 10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Storybook 9's `CLI_COLORS` has no `muted`, so `migrate` threw right
after writing the files, before its summary and warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A vitest global setup checks the installed csf-tools for `parseModule`,
as the CLI does, and passes the result to the suites. It prints which
transforms are tested and why the others are skipped, which the default
reporter did not show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Typing `loadTransforms` by the Babel module makes a result the ESTree
path widens, such as a new skip reason, a compile error instead of a
report entry the CLI drops. The unrecognized-shape section now says
"could not migrate safely", which also fits the stories the ESTree path
skips for a spread or a refused edit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Babel suite runs on the current `storybook` dev dependency; the
ESTree suite skips and prints why until that is a Storybook 11
prerelease with the ESTree csf-tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kasperpeulen and others added 3 commits October 21, 2026 16:38
Point the storybook, @storybook/addon-vitest and @storybook/react-vite
dev dependencies at the pkg.pr.new canary of storybookjs/storybook next
(b26f35e1aff), which has storybookjs/storybook#36613 and #36947. Its
packages depend on each other by URL, so pnpm-workspace.yaml allows
exotic subdependencies until they are on an npm release again.

With the csf-tools of Storybook 11 installed, test:codemod now runs the
ESTree suite and skips the Babel one.

The Storybook 11 sidebar lists stories as tree grid rows rather than
links, so the story tests click the row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
csf-tools now has public API for what the ESTree path worked around:

- `editor`, `program` and `node` replace `_editor`, `_program` and
  `_exportsObject`.
- `objectProperty`, `isIdentifier` and `detachedTextOf` replace the
  local `findObjectProperty`, `isIdentifier` and `nodeText`.
- Paths with indexes replace the raw edits of `loaders`, `decorators`
  and `addons` entries: `set(['loaders', i])` for `replaceNode`,
  `append(['addons', i])` for `addFirstArgument`, `remove([name, i])`
  for `removeFromList`, and `prepend(['addons'])` for `prependToList`.
- `append` replaces the deprecated `appendNodeToArray` and
  `appendValueToArray`.

Going through paths makes csf-tools refuse a preview it will not edit
(a config binding used elsewhere, a config wrapped in a call other than
`definePreview`), which the raw edits wrote to anyway. Such a preview
is now skipped with the reason csf-tools gives, and only when its
imports still show v2 wiring. `node` is undefined for those configs, so
main.ts keeps reading `addons` with `getFieldNode`, which finds an
addon registered there, and leaves the refusal to `append`.
`export default makeConfig()` is now reported, where it was counted as
up to date.

Refused list entries are named as `decorators[0]` with `printCsfPath`.
`objectProperty` also finds methods, so a `loaders()` method in the
preview and a `parameters.msw()` method in a story are now reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the storybook, @storybook/addon-vitest and @storybook/react-vite
dev dependencies back from the pkg.pr.new canary to 11.0.0-alpha.0,
which still ships the Babel csf-tools, and drop `blockExoticSubdeps`.
CI then covers the transforms Storybook 9 and 10 users run today, and
the PR does not depend on a canary. The dev dependencies move to
Storybook 11 with its first prerelease that has the public csf-tools
API.

The story tests find a sidebar story by its link or, in later Storybook
11 builds, its tree grid row, so they pass on both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant