Skip to content

fix: port parcel-transformer-storybook to Storybook 11's csf-tools - #10811

Draft
kasperpeulen wants to merge 6 commits into
adobe:mainfrom
kasperpeulen:storybook-11-csf-tools
Draft

kasperpeulen wants to merge 6 commits into
adobe:mainfrom
kasperpeulen:storybook-11-csf-tools

Conversation

@kasperpeulen

@kasperpeulen kasperpeulen commented Oct 20, 2026 •

Copy link
Copy Markdown

The transformer moves to Storybook 11's csf-tools, so the release that ships it needs Storybook 11.

Storybook 11 parses story files with oxc-parser instead of Babel: storybook/internal/csf-tools now hands out ESTree nodes, and CsfFile._ast is gone (storybookjs/storybook#36613, see its MIGRATION section).

Today the transformer builds Babel nodes and pushes them into csf._ast.program. On the canary, every story file throws under HMR:

Cannot read properties of undefined (reading 'program')

Without HMR the builds pass, but every asset loses its source map, and TypeScript stories lose __docgenInfo.

The transformer now edits the source text through csf._editor and prints the file once with formatCsf. Code it does not touch keeps its formatting and its source map, story bodies included. @babel/parser and @babel/types are no longer dependencies.

For each story file it:

  1. Parses it with loadCsf(…).parse().
  2. Under HMR, rewrites the exports for the Fast Refresh wrapper and moves imports that sit below other code to the top. This goes first, because statements appended later go after the last statement left in place.
  3. Awaits enrichCsf, which adds each story's originalSource and JSDoc description.
  4. Under HMR, turns each story function into a component in place, so Fast Refresh can update it. Stories and the meta get a _hash; those with a story function also get _internalComponent.
  5. Appends Button.__docgenInfo = {…} for the meta's component.
  6. Under HMR, closes the try. Then it prints the file with one source map.

A story function becomes a component without moving its code:

const Primary = (args) => <Button {...args} />;

becomes, wrapped here to fit:

const Primary = (args) => <Story0 {...args} />,
  Story0 = (args) => <Button {...args} />;

A render function is named inside its expression. render: (args) => <Button {...args} /> becomes:

render: (Story0 = (args) => <Button {...args} />,
  (args) => <Story0 {...args} />)

CSF 2 stories get Primary._internalComponent = Story0; and Primary._hash = "…"; as statements. Object stories and the meta get them as members.

Under HMR, the Babel version turned export { type T as X } into $Export0 = T, which throws at runtime, and dropped the async of an async render() method. It also skipped __docgenInfo for a 'component' string key. The new version handles all three.

Land it with the move to Storybook 11, once a Storybook 11 prerelease contains storybookjs/storybook#36613. The repo is on 10.3.5, where yarn check-types fails in this file because the csf-tools exports it uses do not exist yet. The transformer cannot support both, so move the storybook ranges from ^10.0.0 to that prerelease in the same change.

Two behaviour changes are intended:

  • enrichCsf is async, and the transformer now awaits it. The Babel version did not, so its story edits landed after the file was printed. Stories now get parameters.docs.source.originalSource and their JSDoc description.
  • A legacy Story.story = {…} is hashed as a whole. Storybook 10 spread its members into the story's annotations, so its args and parameters were hashed. Storybook 11 keeps it as one story annotation. Hashing all of it keeps Fast Refresh bailing out when it changes. A change to its name or decorators now bails out too.

The same change for the copy in parcel-bundler/storybook: parcel-bundler/storybook#6

No issue exists for this.

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices
  • I understand every change in this PR and can explain why it's there.
  • If AI-assisted, I followed our AI contribution guidance and pointed my assistant at AGENTS.md.

The transformer has no unit tests; the checks below stand in for them. Documentation and accessibility do not apply to build tooling.

📝 Test Instructions:

Everything ran on the canary storybook@https://pkg.pr.new/storybook@93fc3ee and the matching @storybook/* packages, installed locally through resolutions. Nothing here pins it: the storybook ranges still read ^10.0.0.

  • yarn build:storybook and yarn build:storybook-s2 pass.
  • yarn check-types reports no errors in this package.
  • yarn oxfmt --check passes on the file.

I ran the transformer over the 455 story files of .storybook, .storybook-s2, .chromatic and .chromatic-fc, with HMR on and off: 910 transforms. A stub docgen client gave every file docs. Each output ran with stubbed imports, and I compared it with the Babel transformer's output on Storybook 10. The comparison covers the exports, whether each export has a _hash and an _internalComponent, what each story renders through its component, __docgenInfo, and the refresh prelude and postlude. All 910 match.

To try it, add the canary to resolutions for storybook and the @storybook/* packages and run yarn install. Then run yarn start (port 9003) or yarn start:s2 (port 6006), open a story and edit its render function. Fast Refresh should keep the component's state, as before. I did not run the dev server.

On the canary, yarn check-types reports 729 errors elsewhere, from Storybook 11's own types. 711 come from the legacy Story.story property (693 TS2339, 18 TS2559), 14 from stricter meta and args types, and 4 from @mdx-js/react declaring JSX twice. They belong to the Storybook 11 upgrade itself.

🧢 Your Project:

Storybook (Storybook 11 csf-tools migration)

🤖 Generated with Claude Code

kasperpeulen and others added 2 commits October 20, 2026 09:00
Storybook 11 parses story files with OXC instead of Babel: CsfFile._ast
is gone and CsfFile._metaNode is an ESTree node, so the transformer
crashed on `csf._ast.program` and lost its source map.

Storybook now only loads, enriches and prints the story file. The Fast
Refresh and docgen edits run on the transformer's own @babel/parser
parse of that output, printed with @babel/generator and chained onto
Storybook's source map. Stories and meta are found at the offsets of
Storybook's nodes, which mean the same for Babel and ESTree nodes.

enrichCsf is async and is awaited now. Before, its story edits ran after
the file was printed, so stories never got docs.source.originalSource.
A legacy `Story.story = {parameters}` is hashed for Fast Refresh too,
as Storybook 11 no longer spreads it into `parameters`.

Same change as parcel-bundler/storybook's copy of this transformer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…parser 7.24

This repo resolves @babel/parser to 7.24, which needs the
importAttributes plugin for `import … with {type: 'macro'}` in the S2
stories. Also resolve the new @babel/generator range to the existing
7.29.1 instead of adding a second copy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
kasperpeulen and others added 2 commits October 20, 2026 10:09
…sf-tools editor

Storybook 11 parses story files with OXC and edits them as text through
`csf._editor`. Instead of parsing Storybook's output again with Babel,
make the Fast Refresh and docgen edits on that editor, so there is one
parse, one print and one source map, and code the edits do not touch
keeps its mapping.

- Story functions become components in place: a variable is renamed to
  `StoryN` and followed by a wrapper under its own name; a function
  declaration gets a wrapper before it; a `render` value is assigned to
  `StoryN` inside the expression (`wrapExpression`).
- `_internalComponent` and `_hash` members go through `appendToList`;
  the CSF 2 assignments and `__docgenInfo` through `_appendStatement`.
- The module is wrapped for Fast Refresh by removing `export` keywords
  and inserting the prelude, the `try` and the export list around the
  rest, as before.

Drops @babel/parser, @babel/types and @babel/generator. Needs
Storybook 11. Same change as parcel-bundler/storybook#6.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…a computed render key

Under HMR, a 'use client' directive stayed below the inserted prelude,
inside the `try`, where it is no longer a directive. A computed
`['render'](args) {}` method kept its brackets. Also print the docgen
target without parentheses when it is a member expression.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@kasperpeulen kasperpeulen changed the title fix: stop editing Storybook's Babel AST in parcel-transformer-storybook refactor: edit through Storybook 11's csf-tools in parcel-transformer-storybook Oct 20, 2026
@kasperpeulen kasperpeulen changed the title refactor: edit through Storybook 11's csf-tools in parcel-transformer-storybook fix: port parcel-transformer-storybook to Storybook 11's csf-tools Oct 20, 2026
kasperpeulen and others added 2 commits October 20, 2026 12:42
…ools fixes made unneeded

Storybook 11's editor now keeps a wrapper's suffix inside text written at
the expression's end, lets several appendToList calls on one list add up,
and has `update`, an overwrite that keeps what was written next to the
span. So enrichCsf can run before this transformer's edits again; only
the Fast Refresh wrapping still goes first, because statements appended
later skip the ones it removes. A variable story is now renamed by
rewriting its name alone. Same change as parcel-bundler/storybook#6.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Find the declarator of a story function with `parentOf` instead of
indexing every top-level declaration, rewrite a `render` method into a
function value that `addComponent` wraps like any other, and pass the
editor to `wrapRefresh`.

Every story export has annotations, so every story gets a `_hash`, and
the guards for a missing one go. A `get render()` getter is left alone
instead of being rewritten into invalid code, and a `render` method
keeps `async` and `*`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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