You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Dependency modernization: Node 24, Vite, and Storybook 10
Our dependency backlog isn't a queue of independent updates — it's one blocked chain plus a set of genuinely independent upgrades. This issue records what's actually blocking what, and the PR sequence we're using to unwind it.
The core finding: webpack is a dead end for our Storybook
@storybook/html-webpack5 stops at 8.6.14 — there is no stable 9 or 10 release. Storybook 10 for HTML is Vite-only.
This inverts how we've been reading the backlog. The closed PRs for css-loader v7, style-loader v4, mini-css-extract-plugin v2, postcss-loader v8, babel-loader v10, and sass-loader v17 are not blocked work waiting on a webpack 5 upgrade. On the Vite path those packages are deleted, not upgraded. Roughly ten dependencies leave package.json entirely.
We should not spend effort upgrading to webpack 5.
Spike results
A full spike (Storybook 10.5.9 + Vite 8 + Twing 7) builds and renders real Twig output. Findings:
The Twig source preview works, including syntax highlighting. This is the issue that killed [Review carefully] Update storybook monorepo to v6.5.16 #2006 — Storybook 10 exposes SyntaxHighlighter.registerLanguage, so registering the Twig Prism grammar is a supported hook rather than the fragile react-syntax-highlighter reach-in that broke between 6.5.9 and 6.5.16.
Twing 7 still renders synchronously via createSynchronousEnvironment. Stories stay () => template(args) — no async migration needed.
We must import twing/light. The full Twing build pulls in locutus → safe-buffer, which crashes under Vite (Cannot read properties of undefined (reading 'prototype')) because Vite does not polyfill Node builtins the way webpack 4 did. All 24 Twig filters our templates use were tested against light — no regressions.
The Vite plugin is simpler than what it replaces.twing-loader + source-inputs-loader + val-loader + virtual-bundled-environment collapse into roughly 60 lines. The source preview no longer needs the brittle string-replace against twing-loader's generated return template.render(context); we own the render wrapper, so path and args are recorded directly.
~~The custom Sass glob importer must be rewritten to Sass's modern canonicalize/load API (~40 lines). It's shared with gulp-sass, so gulp-sass v6 moves at the same time.~~ Superseded: the importer was deleted, not rewritten. A modern importer owns a URL namespace — everything reached through it comes back to it to resolve, with no fallback to Sass's own loader — so expanding one glob made us responsible for reimplementing Sass's _partial and index lookup rules by hand. All 57 stylesheets are committed source files, so src/index.scss now lists them explicitly and neither Vite nor gulp-sass needs any Sass configuration. gulp-sass v6 still moved at the same time.
Main cost: all 84 .stories.mdx files must migrate to CSF3 + .mdx. Storybook 10 has no indexer for .stories.mdx at all (No matching indexer found). button.stories.mdx also uses webpack's !!raw-loader! syntax, which becomes ?raw.
Node is separately unblockable
Twing 3's only incompatibility with modern Node is util.isNullOrUndefined, removed in Node 22. With a small shim, both the full test suite and the existing Storybook 6.5 + webpack 4 build pass on Node 24. That decouples the Node upgrade — and roughly 20 rate-limited updates behind it — from the Storybook project.
PR sequence
Revised. The original list was a risk ordering, not a dependency chain — only the Node
bump actually unblocked anything. We reprioritised to take Storybook 10 + Vite early,
since it deletes a large part of the backlog rather than upgrading it. The remaining
items (4–6) are independent of each other and of the work already shipped, so they can
land in any order.
4. Lint & format — prettier 3, eslint, stylelint 17 + configs, and eslint-plugin-mdx 3 (moved here from the Storybook PR — see below). Prettier 3
reformats the repo, so it wants its own PR. Prettier 3 also unblocks restoring @whitespace/storybook-addon-html at v9.
6. Remove Gulp — 282 lines across 5 tasks and 6 gulp packages, and
it is thinner than it looks: build-tokens.js is a 5-line wrapper that shells out
to npm run preprocess:tokens, and build-scripts.js is already plain Rollup API.
Deliberately kept out of the Storybook PR to keep that reviewable.
Changes forced by the Storybook work
@types/node is pinned to 20.19.9 via overrides.Resolved — never landed.
Webpack 4's dependency tree was holding it at 12.20.6; removing webpack let it float to
26.2.0, which TypeScript 4.9 could not parse (syntax errors in ffi.d.ts, and skipLibCheck does not suppress parse errors). Pinning was the cheaper option at the
time, but Upgrade the library build to Rollup 4 and TypeScript 5 #2406 landed TypeScript 5.9 first, so Upgrade to Storybook 10 + Vite, and Twing 7 #2405 was rebased onto it and dropped
the override. There is no overrides block in package.json on main.
@babel/preset-typescript is now a direct dependency.babel.config.js used it,
but it was only present transitively via the Storybook 6 packages.
eslint-plugin-mdx 3 moved out of the Storybook PR. It was blocked because MDX 1
allowed arbitrary top-level JS and .stories.mdx relied on it. Now that the 84 files
are MDX 3, the upgrade should be straightforward, so it belongs with the lint work.
src/**/*.stories.js is excluded from tsconfig. Converting MDX to JS brought
250+ story functions under checkJs for the first time — 269 errors. Stories are demo
code, are not published, and were not type-checked as MDX either, so excluding them
preserves existing coverage rather than expanding the PR. Type-checking stories would
be a real improvement, but it is its own piece of work.
The PR 5 split idea did not pan out
The earlier note suggested extracting CSF from MDX while still on Storybook 6, to split
the migration in two. Both halves of that turned out to be dead ends:
Storybook's own mdx-to-csf codemod cannot read our files. It parses with MDX 3,
which rejects MDX 1's top-level const declarations — the exact thing every one of
our story files used. The Storybook 10 CLI has also dropped the codemod entirely; it
only survives in @storybook/codemod@8. Worth recording so nobody retries this.
Migrating the files therefore needed a purpose-written transformer, which was going to
be the bulk of the work either way. Splitting would have added a throwaway
intermediate state without reducing the total effort.
Addon decisions needed in PR 5
storybook-addon-outline — flagged deprecated with no replacement; outlines are built into Storybook core now, so this can likely just be dropped.
storybook-mobile and @whitespace/storybook-addon-html — already commented out in .storybook/main.js "until they are fixed upstream". Drop them, or restore at v9.
@storybook/addon-essentials and @storybook/addon-postcss — both removed/obsolete in Storybook 10.
@etchteam/storybook-addon-status — needs v8.
Housekeeping
We run both Renovate and Dependabot, which is generating duplicate PRs for the same upgrades (lodash in #2368 and postcss in #2373 both duplicated Renovate security PRs at older versions, which is why they conflicted). Renovate is also configured with :automergeMinor, but branch protection requires an approving review, so its automerge can never fire. Aligning these would keep the backlog from rebuilding.
Smaller items noticed along the way
gulp-svgmin v4 emits a warning: You are trying to enable removeXMLNS which is not part of preset. The SVGO plugin config needs updating.
package.json still declares engines.node: ">=12.16.3". This describes what consumers of the published package need, not our dev environment, so it deliberately wasn't changed in PR 1 — but it's stale and deserves its own decision.
Storybook 6 → 10 API moves (for reference)
Collected while doing the migration:
Storybook 6
Storybook 10
docs.transformSource
docs.source.transform
docs.inlineStories
docs.story.inline
docs.prepareForInline
not needed — the HTML renderer inlines natively
viewport.viewports
viewport.options
@storybook/client-api
storybook/preview-api
@storybook/addons
storybook/manager-api
@storybook/theming/create
storybook/theming/create
@storybook/addon-viewport
storybook/viewport
<Description>{markdown}
<Markdown>{markdown} — Description now takes of={}
<ArgsTable>
<Controls>
ReactSyntaxHighlighter.registerLanguage
SyntaxHighlighter.registerLanguage from storybook/internal/components
require.context
import.meta.glob
!!raw-loader!x
x?raw
@svgr/webpack inline loader
vite-plugin-svgr with ?react
Two Twing 7 changes worth knowing: the filesystem loader is built from an injected fs
rather than a path, and path namespaces now include the sigil (@cloudfour, not cloudfour). Rendering can still be synchronous via createSynchronousEnvironment, so
story functions keep returning strings.
The browser build must import twing/light. The full build pulls in locutus, which
needs Node's Buffer; webpack 4 shimmed that automatically and Vite does not, so the
bundle dies on load. All 24 filters our templates use work in light.
Dependency modernization: Node 24, Vite, and Storybook 10
Our dependency backlog isn't a queue of independent updates — it's one blocked chain plus a set of genuinely independent upgrades. This issue records what's actually blocking what, and the PR sequence we're using to unwind it.
The core finding: webpack is a dead end for our Storybook
@storybook/html-webpack5stops at 8.6.14 — there is no stable 9 or 10 release. Storybook 10 for HTML is Vite-only.This inverts how we've been reading the backlog. The closed PRs for
css-loaderv7,style-loaderv4,mini-css-extract-pluginv2,postcss-loaderv8,babel-loaderv10, andsass-loaderv17 are not blocked work waiting on a webpack 5 upgrade. On the Vite path those packages are deleted, not upgraded. Roughly ten dependencies leavepackage.jsonentirely.We should not spend effort upgrading to webpack 5.
Spike results
A full spike (Storybook 10.5.9 + Vite 8 + Twing 7) builds and renders real Twig output. Findings:
SyntaxHighlighter.registerLanguage, so registering the Twig Prism grammar is a supported hook rather than the fragilereact-syntax-highlighterreach-in that broke between 6.5.9 and 6.5.16.createSynchronousEnvironment. Stories stay() => template(args)— no async migration needed.twing/light. The full Twing build pulls inlocutus→safe-buffer, which crashes under Vite (Cannot read properties of undefined (reading 'prototype')) because Vite does not polyfill Node builtins the way webpack 4 did. All 24 Twig filters our templates use were tested againstlight— no regressions.twing-loader+source-inputs-loader+val-loader+virtual-bundled-environmentcollapse into roughly 60 lines. The source preview no longer needs the brittle string-replace against twing-loader's generatedreturn template.render(context); we own the render wrapper, so path and args are recorded directly.canonicalize/loadAPI (~40 lines). It's shared withgulp-sass, sogulp-sassv6 moves at the same time.~~ Superseded: the importer was deleted, not rewritten. A modern importer owns a URL namespace — everything reached through it comes back to it to resolve, with no fallback to Sass's own loader — so expanding one glob made us responsible for reimplementing Sass's_partialandindexlookup rules by hand. All 57 stylesheets are committed source files, sosrc/index.scssnow lists them explicitly and neither Vite norgulp-sassneeds any Sass configuration.gulp-sassv6 still moved at the same time.Main cost: all 84
.stories.mdxfiles must migrate to CSF3 +.mdx. Storybook 10 has no indexer for.stories.mdxat all (No matching indexer found).button.stories.mdxalso uses webpack's!!raw-loader!syntax, which becomes?raw.Node is separately unblockable
Twing 3's only incompatibility with modern Node is
util.isNullOrUndefined, removed in Node 22. With a small shim, both the full test suite and the existing Storybook 6.5 + webpack 4 build pass on Node 24. That decouples the Node upgrade — and roughly 20 rate-limited updates behind it — from the Storybook project.PR sequence
Revised. The original list was a risk ordering, not a dependency chain — only the Node
bump actually unblocked anything. We reprioritised to take Storybook 10 + Vite early,
since it deletes a large part of the backlog rather than upgrading it. The remaining
items (4–6) are independent of each other and of the work already shipped, so they can
land in any order.
Storybook 6.5 → 10, React 17 → 19,
gulp-sass6, the 84-file MDX migration, anddeletion of the webpack toolchain (~10 dependencies removed, not upgraded).
@rollup/plugin-terser 1, @rollup/plugin-babel 7, @rollup/plugin-node-resolve 16,
TypeScript 5. Touched neither Storybook nor Twig. Landing this first let Upgrade to Storybook 10 + Vite, and Twing 7 #2405
drop the temporary
@types/nodeoverride instead of carrying it.eslint-plugin-mdx3 (moved here from the Storybook PR — see below). Prettier 3reformats the repo, so it wants its own PR. Prettier 3 also unblocks restoring
@whitespace/storybook-addon-htmlat v9.npm-run-all2 9, sass.
it is thinner than it looks:
build-tokens.jsis a 5-line wrapper that shells outto
npm run preprocess:tokens, andbuild-scripts.jsis already plain Rollup API.Deliberately kept out of the Storybook PR to keep that reviewable.
Changes forced by the Storybook work
Resolved — never landed.@types/nodeis pinned to 20.19.9 viaoverrides.Webpack 4's dependency tree was holding it at 12.20.6; removing webpack let it float to
26.2.0, which TypeScript 4.9 could not parse (syntax errors in
ffi.d.ts, andskipLibCheckdoes not suppress parse errors). Pinning was the cheaper option at thetime, but Upgrade the library build to Rollup 4 and TypeScript 5 #2406 landed TypeScript 5.9 first, so Upgrade to Storybook 10 + Vite, and Twing 7 #2405 was rebased onto it and dropped
the override. There is no
overridesblock inpackage.jsononmain.@babel/preset-typescriptis now a direct dependency.babel.config.jsused it,but it was only present transitively via the Storybook 6 packages.
eslint-plugin-mdx3 moved out of the Storybook PR. It was blocked because MDX 1allowed arbitrary top-level JS and
.stories.mdxrelied on it. Now that the 84 filesare MDX 3, the upgrade should be straightforward, so it belongs with the lint work.
src/**/*.stories.jsis excluded fromtsconfig. Converting MDX to JS brought250+ story functions under
checkJsfor the first time — 269 errors. Stories are democode, are not published, and were not type-checked as MDX either, so excluding them
preserves existing coverage rather than expanding the PR. Type-checking stories would
be a real improvement, but it is its own piece of work.
The PR 5 split idea did not pan out
The earlier note suggested extracting CSF from MDX while still on Storybook 6, to split
the migration in two. Both halves of that turned out to be dead ends:
mdx-to-csfcodemod cannot read our files. It parses with MDX 3,which rejects MDX 1's top-level
constdeclarations — the exact thing every one ofour story files used. The Storybook 10 CLI has also dropped the codemod entirely; it
only survives in
@storybook/codemod@8. Worth recording so nobody retries this.be the bulk of the work either way. Splitting would have added a throwaway
intermediate state without reducing the total effort.
Addon decisions needed in PR 5
storybook-addon-outline— flagged deprecated with no replacement; outlines are built into Storybook core now, so this can likely just be dropped.storybook-mobileand@whitespace/storybook-addon-html— already commented out in.storybook/main.js"until they are fixed upstream". Drop them, or restore at v9.@storybook/addon-essentialsand@storybook/addon-postcss— both removed/obsolete in Storybook 10.@etchteam/storybook-addon-status— needs v8.Housekeeping
We run both Renovate and Dependabot, which is generating duplicate PRs for the same upgrades (lodash in #2368 and postcss in #2373 both duplicated Renovate security PRs at older versions, which is why they conflicted). Renovate is also configured with
:automergeMinor, but branch protection requires an approving review, so its automerge can never fire. Aligning these would keep the backlog from rebuilding.Smaller items noticed along the way
gulp-svgminv4 emits a warning:You are trying to enable removeXMLNS which is not part of preset. The SVGO plugin config needs updating.package.jsonstill declaresengines.node: ">=12.16.3". This describes what consumers of the published package need, not our dev environment, so it deliberately wasn't changed in PR 1 — but it's stale and deserves its own decision.Storybook 6 → 10 API moves (for reference)
Collected while doing the migration:
docs.transformSourcedocs.source.transformdocs.inlineStoriesdocs.story.inlinedocs.prepareForInlineviewport.viewportsviewport.options@storybook/client-apistorybook/preview-api@storybook/addonsstorybook/manager-api@storybook/theming/createstorybook/theming/create@storybook/addon-viewportstorybook/viewport<Description>{markdown}<Markdown>{markdown}—Descriptionnow takesof={}<ArgsTable><Controls>ReactSyntaxHighlighter.registerLanguageSyntaxHighlighter.registerLanguagefromstorybook/internal/componentsrequire.contextimport.meta.glob!!raw-loader!xx?raw@svgr/webpackinline loadervite-plugin-svgrwith?reactTwo Twing 7 changes worth knowing: the filesystem loader is built from an injected
fsrather than a path, and path namespaces now include the sigil (
@cloudfour, notcloudfour). Rendering can still be synchronous viacreateSynchronousEnvironment, sostory functions keep returning strings.
The browser build must import
twing/light. The full build pulls inlocutus, whichneeds Node's
Buffer; webpack 4 shimmed that automatically and Vite does not, so thebundle dies on load. All 24 filters our templates use work in
light.