Visitar URL original
USWDS - Storybook: Upgrade to Storybook 9 and migrate from Webpack to Vite by ericsorenson · Pull Request #6715 · uswds/uswds · GitHub
Skip to content

USWDS - Storybook: Upgrade to Storybook 9 and migrate from Webpack to Vite - #6715

Merged
ethangardner merged 15 commits into
uswds:developfrom
ericsorenson:feat/storybook-9-upgrade
Jul 17, 2026
Merged

ethangardner merged 15 commits into
uswds:developfrom
ericsorenson:feat/storybook-9-upgrade

Conversation

@ericsorenson

Copy link
Copy Markdown
Contributor

Upgrades Storybook from 8.6 to 9.1 and migrates the build pipeline from Webpack to Vite. Storybook 9 removed @storybook/html-webpack5, so this migrates the framework to @storybook/html-vite. Because USWDS renders Twig templates client-side and ships component JavaScript as CommonJS, two custom Vite plugins were written to preserve existing behavior without modifying component source files.

What changed

Framework & dependencies

  • Upgraded storybook 8.6 → 9.1.20
  • Replaced @storybook/html-webpack5 with @storybook/html-vite
  • Removed @storybook/addon-essentials (controls, viewport, backgrounds, actions are now built into Storybook 9 core)
  • Bumped @storybook/addon-a11y and @storybook/test-runner (0.21 → 0.23) to SB9-compatible versions
  • Removed now-unused Webpack loaders: css-loader, file-loader, postcss-loader, resolve-url-loader, sass-loader, style-loader

New build tooling (tasks/)

  • vite-plugin-twig.mjs — compiles .twig templates into ESM render functions; resolves @components/@templates namespace aliases and {% include %}, {% extends %}, {% embed %}, {% from %}, {% import %} dependencies; cascades HMR through the template dependency graph
  • vite-plugin-uswds-cjs.mjs — transforms USWDS component CJS (require/module.exports) into ESM at build time (with source maps via MagicString), avoiding a Rollup commonjs-plugin crash on the circular uswds-core dependency graph
  • twig-esm-wrapper.mjs — provides ESM-compatible named exports for the CJS-only twig package in production builds

Configuration

  • .storybook/main.js — rewritten for html-vite with a viteFinal hook (Twig plugin, CJS plugin, SCSS asset-path overrides, optimizeDeps, resolve aliases)
  • .storybook/preview.js — updated a11y parameter from deprecated a11y.manual to a11y.test.disable

Notes / out of scope

  • The separate build:html Webpack pipeline (webpack.twig.config.js, tasks/webpack-twig-loader.js) is unchanged
  • .storybook/main.js remains CommonJS because Storybook 9 loads config via esbuild-register, which cannot evaluate native ESM config files (this is resolved in Storybook 10)
  • CSF2 → CSF3 story migration is intentionally deferred to a separate PR to keep this diff focused on the framework migration

How to Test

  1. Check out this branch and run a clean install:
    npm install
  2. Dev server — start Storybook and confirm it boots on html-vite:
    npm start
    • Storybook should report Storybook 9.1.20 for html-vite started
    • Open localhost:6006 and verify the sidebar shows all categories: Design Tokens, Components, Patterns, Pages
  3. Verify rendering across template patterns — open and confirm each renders with full USWDS styling and no console errors:
    • Simple template: Components → Alert → Default
    • {% include %}: Components → Header → Default
    • {% from %} macros: Components → Pagination → Default
    • {% embed %} blocks: Components → Footer → Big Footer
  4. Verify interactive JS components work (toggle/open behavior):
    • Accordion (expand/collapse), Date Picker (calendar opens), Combo Box (dropdown opens), Modal (opens on trigger)
  5. HMR — with the dev server running, edit a .twig file and confirm the story updates
  6. Production build — confirm the static build succeeds:
    npm run build:storybook
  7. Accessibility tests — confirm the a11y suite passes against the built output:
    npm run test:a11y
  8. Confirm the separate HTML build is unaffected:
    npm run build:html

Comment thread tasks/vite-plugin-uswds-cjs.mjs Fixed
@ericsorenson
ericsorenson marked this pull request as ready for review June 17, 2026 18:38
@github-actions

github-actions Bot commented Jun 23, 2026 •

Copy link
Copy Markdown
Contributor

✅ All commits on this PR have a verified signature. Thanks!

@github-actions github-actions Bot added the Needs: Author Response 🔴 Waiting on a specific author action; also used by commit-signature automation. label Jun 23, 2026
@ericsorenson

Copy link
Copy Markdown
Contributor Author

Hi @ericsorenson — thanks for your contribution!

There is one thing that may be holding this up: for security reasons, USWDS requires that all commits to this repo have a verified commit. The following commit(s) on this PR aren't verified:

  • 2dcf683 — updating contributors list
  • c0aa47e — fix: add role=presentation to contributors table and update contributor count

Here's what's needed:

  1. Set up signature verification in GitHub. Use one of the following GitHub guides to set up your verification signature:

  2. Rebase your commits with your verified signature. Once your signature is set up, complete the following steps in the terminal to update the verification status of your previous commits:

    git checkout feat/storybook-9-upgrade
    git rebase origin/develop
    git push --force-with-lease

Please let us know if you have any questions. We appreciate your help here. This comment will update automatically once your commits are verified.

These aren't commits from me?

@ethangardner

ethangardner commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

@ericsorenson - You're right. I'm not sure what happened there. There's an open issue with the workflow (bot) that created the commits, and it's paused until we get it sorted out. Would you mind rebasing and dropping those commits? You can force push with the --force-with-lease flag once those bot commits are dropped. We can pair on it if you'd like to get it figured out together. I'll message you.

Replace the previous CJS-wrapper approach with an esbuild-bundled virtual twig runtime to provide reliable CJS→ESM interop in both dev and production.

- Removed tasks/twig-esm-wrapper.mjs (no longer needed).
- Updated tasks/vite-plugin-twig.mjs:
  - Add esbuild buildSync to pre-bundle the twig package into a single ESM virtual module (virtual:uswds-twig-runtime) and serve it via load().
  - Use the bundled default export (Twig) in generated .twig module transforms instead of importing { twig } from the raw CJS package.
  - Lazy-cache the bundle and stub Node built-ins in the esbuild bundle.
  - Removed logic that redirected to the old wrapper and removed isBuild flag usage.
- Updated .storybook/main.js to remove the previous optimizeDeps "twig" pre-bundle and Rollup commonjsOptions workaround, since the plugin now handles bundling.
- Minor security/performance fix in tasks/vite-plugin-uswds-cjs.mjs: tighten the regex used for CJS destructure detection to avoid catastrophic backtracking (ReDoS) and added an explanatory comment.

These changes fix runtime failures caused by Rollup/@rollup/plugin-commonjs failing to statically analyze twig's internal require graph and unify behavior across dev and build.
@ericsorenson
ericsorenson force-pushed the feat/storybook-9-upgrade branch from 7f07019 to 64af115 Compare June 25, 2026 17:24
@github-actions github-actions Bot removed the Needs: Author Response 🔴 Waiting on a specific author action; also used by commit-signature automation. label Jun 25, 2026
@ericsorenson

Copy link
Copy Markdown
Contributor Author

@ericsorenson - You're right. I'm not sure what happened there. There's an open issue with the workflow (bot) that created the commits, and it's paused until we get it sorted out. Would you mind rebasing and dropping those commits? You can force push with the --force-with-lease flag once those bot commits are dropped. We can pair on it if you'd like to get it figured out together. I'll message you.

This should be good to go.

@ethangardner

Copy link
Copy Markdown
Contributor

Thanks, @ericsorenson. I'm teeing this one up for review. I'll give you input within the next day or two.

@chandracarney chandracarney added the Storybook Work on component stories, previews, or the Storybook development environment. label Jun 30, 2026
@ethangardner ethangardner moved this from Triage to Peer Review in USWDS Core Project Data Jun 30, 2026
@ethangardner ethangardner moved this from Peer Review to In progress in USWDS Core Project Data Jun 30, 2026
@ethangardner
ethangardner self-requested a review June 30, 2026 19:29

@ethangardner ethangardner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution! I appreciated the comments you added throughout. They made it much easier for me to review. I like the general approach here. There were some questions and changes I had in this but this is overall a very solid start.

Comment thread tasks/vite-plugin-twig.mjs Resolved
Comment thread tasks/vite-plugin-twig.mjs Resolved
Comment thread tasks/vite-plugin-uswds-cjs.mjs Resolved
Comment thread tasks/vite-plugin-twig.mjs Resolved
Comment thread .storybook/main.js Resolved
@ethangardner ethangardner moved this from In progress to Initial Fed Review in USWDS Core Project Data Jun 30, 2026
@ethangardner ethangardner self-assigned this Jul 1, 2026
Update Storybook SCSS comments and remove forced asset path overrides so USWDS relative $theme-image-path/$theme-font-path resolve correctly when Storybook is served from non-root paths. Rewrite the CJS->ESM plugin transform for `const { ... } = require(...)`: replace an error-prone multi-line regex with a linear index-scan that finds destructure openers, matches the closing brace, validates the body, and rewrites to `import _modN` + destructure. This avoids ReDoS risks, handles multi-line patterns reliably, and cleans up formatting and residual-CJS warnings.
Replace synchronous buildSync with async build to enable proper promise caching for concurrent bundling. Cache the bundling promise (not the resolved value) so concurrent load() calls dedupe onto a single esbuild run. Add stubNodeBuiltins esbuild plugin to resolve Node built-ins (fs, path, module) to empty modules, preventing console warnings from twig's try/catch error handling.
Add build-tooling tests for repo tasks. tasks/test.js introduces tasksTests() to run ESM specs in tasks/**/*.spec.mjs; gulpfile.js exports tasksTests and includes it in the main test series; package.json adds an npm script "test:tasks". Adds a new spec tasks/vite-plugin-uswds-cjs.spec.mjs that verifies the CJS→ESM Vite plugin transforms (including skip rules and ReDoS regression checks).
Add esbuild to package.json and package-lock.json. Add new unit tests at tasks/vite-plugin-twig.spec.mjs that cover the Vite Twig plugin's resolveId, transform, and load hooks (including ensuring the bundled runtime has no throwing require("fs") shim). Run with: npm run test:tasks.

@ethangardner ethangardner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @ericsorenson! I spotted one issue for HMR that I uncovered in testing and added the steps to reproduce that. If you can resolve that, I think we'll be all set here.

Looking at the cjs plugin you wrote, I'm going to suggest a follow up PR once and issue so we can remove the UMD-related checks from the plugin. There's only 1 UMD file in the repo at this point, and I think that'll help us dial down the complexity for things we're adding. I'll create the issue for it and hold off on doing it so you're not dealing with conflicts or things changing while this work is in progress.

Comment thread tasks/vite-plugin-twig.mjs Resolved
@github-project-automation github-project-automation Bot moved this from Initial Fed Review to In progress in USWDS Core Project Data Jul 8, 2026
Move `Twig.cache(false)` from the build-time Node instance to the bundled browser runtime. The Node-side instance is only used to compile .twig source files; the browser runtime is what actually executes templates and registers ids. Without cache disabled on the bundled runtime, HMR re-execution of a .twig module throws "There is already a template with the ID" when re-registering the template. Inject cache disabling into the bundled runtime via regex substitution. Add ESLint support for .spec.mjs files and end-to-end tests verifying HMR re-registration works.
Turn off Twig's Node-side cache during build/HMR by calling Twig.cache(false). During HMR, transform() re-runs Twig.twig({ id, data }) on the build-time instance; with caching enabled re-registering a template id throws "There is already a template with the ID …". Add a comment explaining the rationale.

@ethangardner ethangardner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the strong work on this and working through the issues. Thanks for your patience waiting for me to get this reviewed. This is ready to go and looks good to me!

@ethangardner
ethangardner merged commit 7e3a507 into uswds:develop Jul 17, 2026
6 checks passed
@github-project-automation github-project-automation Bot moved this from In progress to Fed Final Review in USWDS Core Project Data Jul 17, 2026
@chandracarney chandracarney moved this from Fed Final Review to Done in USWDS Core Project Data Jul 20, 2026
@ethangardner ethangardner added this to the uswds 3.14.0 - Aug 2026 milestone Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Storybook Work on component stories, previews, or the Storybook development environment.

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants