Repository navigation
fix(codemod): run migrate on Storybook 11's csf-tools - #202
Draft
kasperpeulen wants to merge 20 commits into
Draft
kasperpeulen wants to merge 20 commits into
kasperpeulen wants to merge 20 commits into
Conversation
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>
2 of 9 tasks
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>
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>
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Storybook 11 replaces Babel with oxc-parser in its CSF tools. The
storybook/internal/babelentry point is gone, andstorybook/internal/csf-toolsnow works on ESTree nodes and edits the source text instead of reprinting it. ThemigrateCLI imports Babel from Storybook and editsCsfFile._astandConfigFile._ast, so on Storybook 11npx msw-storybook-addon migratecrashes as soon as it loads.migrateupgrades 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 frommainstays 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 frommain. 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 importsstorybook/internal/csf-toolsand checks forparseModule. If it is there, it loadstransforms/estree, otherwisetransforms/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.loadTransformsis typed by the Babel module, so a result the ESTree path widens fails to compile.codemod/src/transforms/babelis the code frommain, moved unchanged.The two paths share what is the same on every Storybook:
bin.tsfinds 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 hasStringLiteral,ObjectPropertyand AST builders; ESTree hasLiteral,Propertyand source text. Only parsing, printing andisCsfFactoryPreviewmean the same on both.setImportandremoveImportexist 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, readingparameters.msw, buildingbeforeEachand editing theinitialize(...)statement have no API on both. Even inmain.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 frommain. So each transform is complete on its own.codemod/src/transforms/estree/stories.tsedits story files.CsfFile.objects()returns one editor per meta and story. A CSF2 story'sStory.parameters = ...andStory.beforeEach = ...assignments are fields of that editor, so CSF2, CSF3 and CSF factories share one code path.inspectreadsparameters.mswandbeforeEach. Onetransactionper story removes the parameter and sets the hook. When csf-tools refuses either edit, both roll back and the story is reported asunrecognized-shape.codemod/src/transforms/estree/preview.tsruns the whole migration in onetransaction, split into a CSF 3 half and a CSF Next half. Fields, their array elements and the arguments of calls in them go throughConfigFilepaths such as['loaders', 0], withset,remove,appendandprepend. Imports go throughsetImportandremoveImport. Only theinitialize(...)statement is not part of the config, so it is removed through the file'sSourceEditor. References toinitializeare looked up through the file's scopes.codemod/src/transforms/estree/main.tsreadsaddonswithgetFieldNodeand adds the addon withappend, wrapped in the same resolver call as the other entries if they have one.getFieldNodealso 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__/babelandcodemod/__tests__/estree. Like the CLI,codemod/__tests__/csf-tools.tschecks forparseModule. 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 frommain, 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.tsalso fixes a crash on Storybook 9 and 10.0: their logger has noCLI_COLORS.muted, somigratethrew right after writing the files, before printing its summary and warnings. The report heading for skipped stories now reads "Could not migrateparameters.mswsafely", 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:beforeEachis written asbeforeEach: ({ msw }) => { ... }after the object's other fields, where the Babel path writes a method beforeparameters.mswLoaderimport included, follow the file's quotes and semicolons and go below the existing imports.Story.beforeEach = ..., follow the file's semicolon style.loadersandaddonsfields 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:
Story.story = { parameters }form is left alone. Storybook 11 no longer reads.storyannotations, andstorybook migrate csf-2-to-3lifts them out first.beforeEachor shadowparameters.msw, such as{ ...Base, parameters: { msw } }, is skipped with a warning.{ ...base, decorators: [mswDecorator] }, a config binding that is also referenced elsewhere, and a config wrapped in a call other thandefinePreview. A main config like that gets an error in the report, and so doesexport default makeConfig(). The Babel path migrates these, and countsmakeConfig()as up to date.loaders()method in the preview gets a warning, where the Babel path adds a secondloadersnext to it. A story whoseparameters.mswis a method is reported as skipped; the Babel path ignores it.export { meta as default }metas and stories exported asexport { A as B }orexport { A }.export const A = { args }followed byA.parameters = { msw }, the ESTree path putsbeforeEachinto the object and removes the statement. The Babel path adds anA.beforeEach = ...statement.initialize, such as a function parameter, does not make the preview skip.loaders: sharedLoadersnext toinitialize(options)is skipped with a warning. The Babel path removes theinitializecall and loses its options.addons: [addonMsw]becomesaddonMsw(setup)when there areinitializeoptions to fold. The Babel path loses the options the same way.addonscan only come from a spread, like{ ...shared, framework }, getsaddons: [...(shared.addons ?? []), 'msw-storybook-addon']. The Babel path writes a newaddonsthat replaces the shared ones.How it was checked
pnpm test:codemodruns the suite that matches the installed Storybook:11.0.0-alpha.0dev 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 onmain; see "Before merging".nextatb26f35e, installed by hand: 94 tests pass and 65 are skipped. The canary reports version11.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:Then put the files back:
pnpm build,pnpm test:ts,pnpm lint,pnpm test:playandpnpm test:storiespass on the dev dependencies, on Storybook 10.6.3 and on the canary.tsc -p codemod/tsconfig.jsonpasses except in the path whose Storybook is not installed. The build writesbuild/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).mainships 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)andparameters.msw, and a CSF3 and a CSF2 story. The other is CSF Next:definePreviewwith the namespace entryautomigrate csf-factoriesinjects,preview.metaandmeta.story. On all three Storybooks every file migrated, and a second run reported all of them up to date. I ran both again on theb26f35ecanary, with the same result.Before merging
This can be merged and released before Storybook 11 ships. On Storybook 9 and 10
migrateruns the same transforms as onmain, 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 onparseModulealone, 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
mainstay, since the Babel path is moved unchanged. On Storybook 9,getFieldNodedoes not returnaddons: sharedAddonsin a main config, somigratereplaces it with['msw-storybook-addon']and the shared addons are lost. And Storybook 9.0.0 to 9.0.11 lack the logger APIbin.tsimports, somigrateneeds 9.0.12 or later. Both are better fixed in a follow-up.CI now runs
pnpm test:codemodafterpnpm test:ts. With the current dev dependencies it runs the Babel suite, and the ESTree suite skips and prints why.🤖 Generated with Claude Code