Skip to content

Dependency modernization: Node 24, Vite, and Storybook 10 #2391

Description

@spaceninja

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

  • 1. Node 16 → 24Upgrade CI and local development to Node 24 #2392. Shipped.
  • 2. Storybook 10 + Vite + Twing 7Upgrade to Storybook 10 + Vite, and Twing 7 #2405. Shipped. Twing 7, a Vite Twig plugin,
    Storybook 6.5 → 10, React 17 → 19, gulp-sass 6, the 84-file MDX migration, and
    deletion of the webpack toolchain (~10 dependencies removed, not upgraded).
  • 3. Library buildUpgrade the library build to Rollup 4 and TypeScript 5 #2406. Shipped. rollup 2→4, rollup-plugin-dts 6,
    @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/node override instead of carrying it.
  • 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.
  • 5. Misc toolchain — jest 30, style-dictionary 5, cssnano 8, js-yaml 5,
    npm-run-all2 9, sass.
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions