Repository navigation
fix: support the Storybook 11 csf-tools API and keep Storybook 10 - #34
Draft
kasperpeulen wants to merge 14 commits into
Draft
kasperpeulen wants to merge 14 commits into
kasperpeulen wants to merge 14 commits into
Conversation
Storybook 11 (storybookjs/storybook#36613) parses with OXC instead of Babel and recast. `storybook/internal/babel` is removed, and so are `CsfFile._ast` and the Babel `NodePath` fields. Devtools imported the former in the React transform and used the latter in the story writer, so both broke on load. The React transform now parses with `parseModule` and finds JSX with `walk`. It appends the runtime import and the `__chRegisterMeta` calls after the module body instead of reprinting the module with Babel's generator, so the module's text and line numbers are kept. The story writer reads metadata with `loadCsf().parse()`. It edits the source as text in a `SourceEditor`, using the csf-tools import helpers, and renames the snippet's references through the editor's scope analysis. The import rules are the same as before. An added import no longer gets the blank line recast put before it. `parseVersionFloor` keeps a prerelease, so the peer floor can name an exact Storybook 11 prerelease once one ships with #36613. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
commit: |
2 of 9 tasks
Drop `@ts-nocheck` now that the transform reads csf-tools' typed ESTree nodes, and handle function and class declarations in one place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The writer's `SourceEditor` already knows the file's top-level bindings and preferred quote, so the hand-written pattern walk and the quote counter go. The story append no longer needs a branch for an empty trailing range, nor a final-newline check it always satisfied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plugin is about to read syntax with Babel on Storybook 10 and with the ESTree csf-tools on Storybook 11, in `*-babel.ts` and `*-estree.ts` files. Only one of the two has its types installed, so `pnpm typecheck` now runs the compiler through `scripts/typecheck.mjs`, which leaves out the other path's files, picked like at runtime: by whether csf-tools have `parseModule`. `src/test-utils/csf-tools.ts` gives tests the same choice: the installed generation, a parse check with its parser, and a `describe` that skips itself with the reason on the other Storybook. The peer test loads `loadCsf`, which both have, and the dist import scan parses with `parseModule` or Babel, with room for Babel's slower parse of all of `dist`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The transform reads a few facts from each module: the "use client" directive, whether it has JSX, and its exported component bindings. `transform-babel.ts` reads them with Storybook 10's Babel, as `main` did, and `transform-estree.ts` with `parseModule` and `walk` from Storybook 11's csf-tools. `transform.ts` picks one on first use by whether csf-tools have `parseModule`; both are imported statically, since the transform hooks are synchronous, and neither touches `storybook` before it is called. Everything after the read is shared, including the output: the runtime import and the tags are appended after the module body on both, so a module keeps its text and line numbers on Storybook 10 too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The writer's rules stay in `csf-writer.ts`, written once: validate with `loadCsf().parse()`, dedupe the export name, follow the file's format, and merge imports by exported symbol, reusing aliases and promoting type-only bindings. The syntax reads and edits go through a `StoryFileEditor`. `csf-writer-babel.ts` is the Babel AST and recast code from `main`, for Storybook 10; `csf-writer-estree.ts` is the `SourceEditor` code for Storybook 11. The writer imports the one whose csf-tools it finds, lazily, so neither loads on the other Storybook. On Storybook 10 the output is the same as on `main`. The one difference between the two is a blank line recast puts before an added import, so the five snapshots with one keep a version per path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rejected editor import no longer sticks for the life of the process, so a later write tries again instead of always taking the regex splice. The Babel reader loses `@ts-nocheck`: it type-checks against Storybook 10's Babel types with two small changes. The typecheck script also reports tsconfig errors, and only skips the other path's files under `src/`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Storybook 11 prereleases without storybookjs/storybook#36613 still ship the Babel csf-tools and fall in the peer range, so "Storybook 11" is not the same as the ESTree path. Comments, skip reasons and docs now say which csf-tools each path needs. `AGENTS.md` also warns that `vitest -u` on one path deletes the other path's five snapshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI runs vitest in CI mode, which fails on obsolete snapshots, and each path left the other path's five `babel`/`estree` snapshots obsolete. The five tests now strip the one difference, the blank line recast puts before an added import, check it against the installed path, and match the snapshots of this PR's first commits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ESTree story writer now reads and edits the file through the CsfFile's own editor instead of a second SourceEditor, adds value imports with CsfFile.setImport, and takes top-level names from scopes.program.bindings and scopes.tsDeclarationNames. Story typing comes from csf.stories and the scope bindings, and the factory meta variable from the meta object or the first meta.story() story, since csf-tools have no public name for it. The writer checks that the appended export is indexed as a story. The shared writer no longer reads _storyExports, _metaIsFactory or _metaVariableName; picking the export style moved into each editor. The React transform uses locationOf instead of a local copy. The Babel editor imports storybook/internal/babel through a variable, so Vite bundling a playground config from source does not try to resolve a subpath Storybook 11 lacks. Promoting a type import the file also re-exports still falls back to the text splice on the ESTree path: the removal commit indexes the file and the CSF index rejects the re-export without its import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The storybook dev dependency is now the pkg.pr.new canary of storybookjs/storybook next at b26f35e, which has the public csf-tools API, so CI runs the ESTree path. The peer range is unchanged. The playgrounds keep 11.0.0-alpha.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the ESTree path, promoting `import type { fn }` in a file that also
has `export type { fn }` fell back to the text splice, which added a
second import of `fn`. The removal of the type import now waits for the
value import that replaces it, and both go in one commit of the
CsfFile's editor, so the file is indexed only once both edits are made.
The text splice also refuses, writing nothing, when the story needs a
value import of a name the file imports as a type only, since that
would declare the name twice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The peer range allows Storybook 10, so CI runs the Babel path, as on `main`. The ESTree path is checked by hand against the canary of Storybook `next`; AGENTS.md has the command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Summary
Devtools keeps working on Storybook 10 and also runs on Storybook 11, where storybookjs/storybook#36613 removes
storybook/internal/babeland turns the csf-tools AST into ESTree (MIGRATION section). Only the code that reads or edits syntax nodes comes in two versions, one per Storybook. The rest is written once. The peer range stays>=10.6.0 || ^11.0.0-0, so the README's 10.6 floor holds.This can be merged now. On Storybook 10, saved stories come out as they do on
main. Thestorybookdev dependency stays10.6.3, so CI runs the Babel path. The ESTree path uses the csf-tools API that #36947 made public, and its tests run by hand on a canary of Storybooknext(see Validation). The Babel files can be deleted when devtools drops Storybook 10.Why
Devtools used Babel in two places. The React transform parsed, traversed and reprinted every component module with Storybook's Babel. The story writer edited
csf._astand printed it with recast. On a Storybook with #36613,main's transform fails on every component module, and its writer always falls back to the regex splice.Reading the diff
Each of the two picks its implementation the first time it runs, by whether
storybook/internal/csf-toolshasparseModule. There is no version check. Below, "Babel path" means Storybook 10 and the 11 prereleases without #36613, which still ship Babel. "ESTree path" means a Storybook with #36613. The ESTree path also needs #36947; no prerelease has #36613 without it.src/frameworks/react/transform.tsholds everything after the parse, now shared: the RSC gate, the diagnostics, and the output. It asks aModuleReaderfor a few facts about the module: the"use client"directive, whether it has JSX, the exported component bindings, and the patterns it can't tag.transform-babel.tsreads them with Storybook's Babel; that is the code frommain, moved, now type-checked instead of@ts-nocheck.transform-estree.tsreads them withparseModuleandwalk. Both readers are imported statically, because the bundler transform hooks are synchronous. That is safe: neither touchesstorybookuntil it is called, and both go through the guarded peer loader.The output is the same on both: the runtime import and the
__chRegisterMetacalls are appended after the module body, so the module's own text and line numbers are kept. Onmainthe module was reprinted by Babel's generator, and the transform returns no source map.src/utils/csf-writer.tskeeps the writer's rules, written once.loadCsf().parse()validates the file. The writer dedupes the export name and renders the story. It then merges imports by exported symbol: it reuses an alias, promotes a type-only binding and picks a free name on a collision. The syntax steps go through aStoryFileEditor:csf-writer-babel.tsis the Babel AST and recast code frommain, split into those steps.csf-writer-estree.tsedits the file through theCsfFile's owneditorand adds value imports withCsfFile.setImport. Top-level names come from the editor's scope analysis, plustsDeclarationNames. Every commit indexes the file again, and the writer checks that the appended export became a story. Promoting a type-only import to a value import is one commit: the type import's removal waits for the value import, because a file that re-exports the type (export type { fn }) is not valid CSF in between.Each editor also tells a factory file from a CSF3 one. csf-tools have no public name for the meta variable, so the ESTree editor reads it off the
.meta()call around the meta's object, or from the firstmeta.story()story.The writer imports the editor for the installed Storybook with a dynamic
import(), so neither loads on the other Storybook. The build keeps them as two lazy chunks. A failed load is not cached, so the next write tries again.scripts/typecheck.mjsis whatpnpm typecheckruns now. Only one path has its types installed, so it type-checks the project and leaves out diagnostics from the other path's files (src/**/*-babel.tsorsrc/**/*-estree.ts), picked the same way.src/test-utils/csf-tools.tsgives tests the installed path, a parse check with its parser, and adescribethat skips itself on the other Storybook, with the reason in its name.src/storybook-peer.tskeeps the prerelease part of the peer floor, so a floor can name an exact 11 prerelease.What changes for users
On the Babel path, so on Storybook 10, one thing: the React transform no longer reprints a component module. It appends the runtime import and the tags, so stack traces and breakpoints line up with the source. The runtime import now loads after the module's own imports. The runtime imports nothing from the app, so the order does not matter. Saved stories are byte for byte what
mainwrites.On the ESTree path only, which is Storybook 11 with #36613, the writer's output differs from the Babel path's:
setImportdecides.On both paths, the regex splice now refuses, and writes nothing, when the story needs a value import of a name the file imports as a type only. It used to add a second import of that name.
Scope
docs/SUPPORTED_FRAMEWORKS.md)The transform change reaches the React, React 18, Rsbuild and Next playgrounds. The writer is shared by React and Vue. No E2E code and no dock lifecycle code changed.
Validation
With the
storybookdev dependency as committed (10.6.3), so the Babel path:Port 6006 was taken on my machine, so the Storybook suite ran with
STORYBOOK_E2E_URL=http://localhost:6116. Its Vue run fails the same way on this PR's earlier head with11.0.0-alpha.1: the preview throws "Cannot read properties of undefined (reading 'ReactCurrentDispatcher')", so it is not from this change.ESTree path, on the canary
https://pkg.pr.new/storybook@b26f35e(Storybooknextat b26f35e1aff, 11.0.0-alpha.7 plus #36613 and #36947), installed locally and not committed:pnpm buildandpnpm typecheckpass, andpnpm test --runpasses 498 tests and skips the one Babel-only line-ending test frommain. On that canary, one commit before the promotion fix, the Playwright suites gave the same results as above.To run the ESTree tests:
pnpm add -D \ storybook@https://pkg.pr.new/storybook@b26f35e pnpm build pnpm test --run pnpm typecheck git checkout package.json pnpm-lock.yaml pnpm install --frozen-lockfileFive React and Vue story-generator tests append to a file that needs a new import, so their output differs between the paths by that one blank line. They remove it with
withoutAddedImportGap, check that it was there exactly on the Babel path, and match the snapshots of this PR's first commits. Both paths share one snapshot per test, which matters because vitest in CI mode fails on obsolete snapshots.Build sizes, uncompressed:
main, 27.7 kB at this PR's first commits, now 23.8 kB plus a 7.1 kB Babel chunk or a 10.0 kB ESTree chunkmain, 12.9 kB at the first commits, now 18.5 kBScreenshots / Evidence
No visible change.
Documentation
AGENTS.mdif workflow/expectations changeddocs/ARCHITECTURE.mdanddocs/REACT_PATTERNS.mddescribe both paths.AGENTS.mdsays how to check the other path.PR Type
Notes for Reviewers
The shared writer and the ESTree editor use only public csf-tools API. The Babel editor reads
_ast,_storyExports,_metaIsFactoryand_metaVariableNamewithprintCsf, asmaindoes. Tests frommainstill read_metaIsFactoryand_storyExports, since they run on both paths.The ESTree path is picked on
parseModulealone; no released Storybook has #36613 without #36947. CI runs the Babel path only, since that is the installed Storybook; run the ESTree tests by hand as above.The Babel editor imports
storybook/internal/babelthrough a variable. The playgrounds' Vite configs load the plugin from source, and Vite's config bundler resolved the literalimport()up front, which failed on the canary and stopped every Playwright web server.🤖 Generated with Claude Code