Skip to content

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

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

kasperpeulen wants to merge 8 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. For now, resolutions in the root package.json point storybook and the @storybook/* packages at a canary of storybookjs/storybook next (b26f35e), which has the public csf-tools API the transformer uses. On 10.3.5, yarn check-types fails in this file because those exports do not exist there. The transformer cannot support both, so before this lands, replace the resolutions with storybook ranges for the first Storybook 11 prerelease.

Three 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.
  • When csf-tools cannot read a story's args, parameters or story, the story's hash covers the whole file, so any change to the file bails out. That happens for a story other code uses, such as ...Example in another story, and for args set both in the story's object and as Example.args = {…}. Here it affects three stories: Example in the S2 TableView and TextField stories, and in useViewportSize.

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@b26f35e and the matching @storybook/* packages, which the root resolutions pin.

  • 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.

That comparison ran on an earlier canary. On b26f35e I compared the output with that of the previous commit, which read csf-tools' internal fields, over the same files plus four edge-case files, with HMR on and off. Only _hash values differ, since csf-tools now hands out the annotation text with its indentation normalized. The source maps are identical.

To try it, run yarn install, then 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 725 errors elsewhere, from Storybook 11's own types. 711 come from the legacy Story.story property (693 TS2339, 18 TS2559) and 14 from stricter meta and args types. 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>
kasperpeulen and others added 2 commits October 21, 2026 16:33
Resolve storybook and the @storybook packages the repo uses to the
pkg.pr.new canary of storybookjs/storybook next at b26f35e, which has the
public csf-tools API the transformer now uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read the editor, meta, stories and story annotations through
csf.editor, csf.objects(), csf.stories and csf.storyObject() instead of
the _-prefixed CsfFile fields, and use the exported objectProperty
instead of a local copy.

When csf-tools cannot read a story's annotations, such as for a story
another story spreads, its hash covers the whole file, so Fast Refresh
bails out on any change instead of missing an args change.

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