Repository navigation
Core: Skip TypeScript loader hook registration under Jest - #36790
Conversation
Jest sandboxes module loading and throws 'module.register() is not supported in Jest' when Storybook's importModule registers its TypeScript loader hooks, killing every Jest test suite at setup (e.g. via @storybook/test-runner's jest-setup loading the user's .storybook/test-runner.ts). Jest compiles TypeScript itself, so the hooks are never needed there - skip registration when a Jest worker is detected.
Package BenchmarksCommit: The following packages have significant changes to their size or dependencies:
|
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 62 | 68 | 🚨 +6 🚨 |
| Self size | 18.94 MB | 21.99 MB | 🚨 +3.05 MB 🚨 |
| Dependency size | 25.95 MB | 27.06 MB | 🚨 +1.11 MB 🚨 |
| Bundle Size Analyzer | Link | Link |
@storybook/svelte-vite
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 12 | 12 | 0 |
| Self size | 56 KB | 56 KB | 🎉 -431 B 🎉 |
| Dependency size | 30.75 MB | 30.73 MB | 🎉 -13 KB 🎉 |
| Bundle Size Analyzer | Link | Link |
@storybook/sveltekit
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 13 | 13 | 0 |
| Self size | 59 KB | 58 KB | 🎉 -1 KB 🎉 |
| Dependency size | 30.80 MB | 30.79 MB | 🎉 -14 KB 🎉 |
| Bundle Size Analyzer | Link | Link |
@storybook/vue3-vite
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 95 | 95 | 0 |
| Self size | 32 KB | 32 KB | 🚨 +14 B 🚨 |
| Dependency size | 18.76 MB | 18.78 MB | 🚨 +21 KB 🚨 |
| Bundle Size Analyzer | Link | Link |
@storybook/web-components-vite
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 14 | 14 | 0 |
| Self size | 19 KB | 19 KB | 0 B |
| Dependency size | 25.50 MB | 25.52 MB | 🚨 +18 KB 🚨 |
| Bundle Size Analyzer | Link | Link |
@storybook/cli
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 174 | 174 | 0 |
| Self size | 891 KB | 884 KB | 🎉 -7 KB 🎉 |
| Dependency size | 81.25 MB | 83.26 MB | 🚨 +2.01 MB 🚨 |
| Bundle Size Analyzer | Link | Link |
@storybook/codemod
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 167 | 167 | 0 |
| Self size | 42 KB | 44 KB | 🚨 +3 KB 🚨 |
| Dependency size | 79.74 MB | 81.74 MB | 🚨 +2.00 MB 🚨 |
| Bundle Size Analyzer | Link | Link |
create-storybook
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 63 | 69 | 🚨 +6 🚨 |
| Self size | 1.07 MB | 1.07 MB | 🚨 +5 KB 🚨 |
| Dependency size | 44.88 MB | 49.04 MB | 🚨 +4.16 MB 🚨 |
| Bundle Size Analyzer | node | node |
@storybook/svelte
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 4 | 4 | 0 |
| Self size | 264 KB | 253 KB | 🎉 -11 KB 🎉 |
| Dependency size | 4.85 MB | 4.85 MB | 🎉 -10 B 🎉 |
| Bundle Size Analyzer | Link | Link |
@storybook/vue3
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 91 | 91 | 0 |
| Self size | 209 KB | 232 KB | 🚨 +23 KB 🚨 |
| Dependency size | 18.22 MB | 18.22 MB | 🎉 -10 B 🎉 |
| Bundle Size Analyzer | Link | Link |
@storybook/web-components
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 12 | 12 | 0 |
| Self size | 193 KB | 213 KB | 🚨 +20 KB 🚨 |
| Dependency size | 25.10 MB | 25.10 MB | 0 B |
| Bundle Size Analyzer | Link | Link |
Thanks for this! The diagnosis is right, and Stages run on head Check the capability, not
Suggested change: make hook registration best effort. If the runtime refuses it, log at debug level and let function registerTypescriptLoader() {
typescriptLoaderRegistration ??= (async () => {
/* existing Node-version branch, unchanged */
})().catch((error) => {
// Some runtimes (e.g. Jest's sandbox) forbid module hooks; importing may still succeed without them.
logger.debug(`Storybook's TypeScript loader hooks could not be registered: ${error}`);
});
return typescriptLoaderRegistration;
}
When a new commit is pushed, the review restarts at stage 1 on the new head. |
Replace the JEST_WORKER_ID environment guard with a best-effort catch. If the runtime forbids module hooks (e.g. Jest's sandbox), log at debug level and let import() decide instead of keying off an inherited environment variable. Cache the failed result: a runtime that refuses registration once refuses it every time.
|
Addressed the stage 1 findings in 0353c7f:
Verification: |
There was a problem hiding this comment.
🤖 I am the Storybook contribution review bot. This message was posted automatically.
Review phase: Architecture review (stage 1 of 3) and Thermo-nuclear code quality review (stage 2 of 3)We are trying a three-stage automated review: architecture, code quality, and runtime verification. This message states which stages ran and which were skipped. Messages are posted automatically, and a human maintainer makes the final decision. Please tell us whether the feedback and evidence are useful.
Inline findings for this round. Only the comment on module.ts line 10 is blocking; the others are optional.
Thanks for the quick update in Code quality review (stage 2) asks for one change before the review can continue. The new top-level Runtime verification (stage 3) has not run yet. It runs after the blocking item is fixed. A human maintainer makes the final decision on this PR. |
…t-module-register
The top-level `import { logger } from 'storybook/internal/node-logger'`
broke the core build: scripts/build/utils/generate-bundle.ts imports
resolvePackageDir from this module's source and runs under plain Node
while core itself is being built, when code/core/dist does not exist
yet, so loading the build script failed with ERR_MODULE_NOT_FOUND. Load
the logger lazily inside the failure handler instead, matching how the
module already lazy-imports storybook/internal/bin/loader.
Also applies the remaining round-2 review suggestions:
- keep the caught registration error and attach it as `cause` when
importModule throws, so a generic import failure does not hide the
real loader problem
- try registerHooks on Node >= 26 with an early return and a single
register fall-through, removing the duplicated resolve+register branch
- dedupe the register/registerHooks spy setup into one
stubHookRegistration helper that returns call counters; call
importModule twice so the exactly-once assertions prove memoization;
keep the inherited JEST_WORKER_ID case as an it.each row
Verified: with code/core/dist/node-logger absent, plain Node fails to
load this module source before the change (ERR_MODULE_NOT_FOUND) and
loads it after; oxlint 0 errors, oxfmt clean, core tsc reports no
errors in the touched files, vitest passes 3/3.
|
Round-2 response, pushed in S2-1 (blocking, module.ts:10) — fixed. Removed the top-level S1-4 (module.ts) — fixed. The caught registration error is kept in a module-level S2-4 (module.ts:66) — fixed. S2-2 (module.test.ts:53) — fixed. The allowed-runtime test calls S2-3 (module.test.ts:68) — fixed. One Gates on the touched files: oxlint 0 errors (3 warnings are pre-existing on untouched lines), Note for reviewers: the branch history also includes |
|
I ran this against real Jest 30.5.2, in a small project that loads
The failing I added |
…ule-register Core: Skip TypeScript loader hook registration under Jest (cherry picked from commit 8ccbb5b)
Closes #36116, closes #36269
What I did
Storybook's
importModuleunconditionally registers TypeScript-loading module hooks:module.registeron Node <26,module.registerHookson Node 26+. Jest sandboxes module loading and throws the moment that happens, so every Jest-based consumer that loads Storybook config throughimportModuledies at setup.Concrete case:
@storybook/test-runner'sjest-setuploads the user's.storybook/test-runner.tsthroughimportModule. Against 11.0.0-alpha.6, every test suite fails at setup before a single test runs (Tests: 0 total), with an identical stack on two independent real-world repos (reaviz/reaviz, reaviz/reablocks):Jest compiles TypeScript itself, so the hooks are never needed under Jest. This PR skips the registration when a Jest worker is detected:
function registerTypescriptLoader() { + // Jest forbids module hook registration inside its sandbox (it compiles TS itself). + if (process.env.JEST_WORKER_ID) { + return Promise.resolve(); + } if (!typescriptLoaderRegistration) {With this fix, the migration guide's claim that existing test-runner setups can keep running against 11 at their own risk holds again: the runner prints its end-of-support banner and then executes the suites. The banner behavior itself is unchanged.
Evidence
Before — minimal Jest fixture (Storybook 11.0.0-alpha.6 +
@storybook/test-runner@0.26.0, one trivial story,--index-json):After — same fixture, same command, Storybook dist rebuilt with this PR:
Both runs print the test-runner end-of-support banner and the Vitest-addon advisory; only the setup crash is gone.
Additional verification:
code/core/src/shared/utils/module.test.ts: the Jest regression test fails onnext(the Jest-forbiddenregistercall is reached and throws through the realimportModule) and passes with this fix; a second test asserts the TypeScript loader hooks still register exactly once outside Jest.yarn test shared/utils: 9 files, 104 tests, all passing; oxlint and oxfmt clean.storybook buildon a consumer repo with a TypeScript.storybookconfig, run against the rebuilt dist — exit 0.Merge Danger
Door: two-way
Blast Radius: Minimal
The guard only short-circuits when
JEST_WORKER_IDis set, and Jest never needs Storybook's hooks because it transforms TypeScript itself. Behavior outside Jest is unchanged: the registration call, its Node-version branch, and the caching are byte-identical. The only conceivable regression would be a Jest environment that also wanted the hooks registered — Jest forbids registering them there, so no such consumer can exist.Checklist for Contributors
Testing
The changes in this PR are covered in the following automated tests:
Manual testing
Caution
This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!
yarn install, thenyarn nx compile core..storybook/test-runner.ts, run the test runner against a dev server, e.g.npx @storybook/test-runner --url http://127.0.0.1:6006 --index-json.module.register() is not supported in JestandTests: 0 total.storybook devandstorybook buildand expect the TypeScript config to load exactly as before.Documentation
No documentation changes are needed: nothing user-facing changes, and no MIGRATION.md entry is required (the fix removes a regression that contradicts the existing "test runner support ended" entry).
Checklist for Maintainers
When this PR is ready for testing, make sure to add
ci:normal,ci:mergedorci:dailyGH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found incode/lib/cli-storybook/src/sandbox-templates.tsDeclare whether manual QA will be needed for this PR during the next release, through
qa:neededorqa:skipMake sure this PR contains one of the labels below:
Available labels
bug: Internal changes that fixes incorrect behavior.maintenance: User-facing maintenance tasks.dependencies: Upgrading (sometimes downgrading) dependencies.build: Internal-facing build tooling & test updates. Will not show up in release changelog.cleanup: Minor cleanup style change. Will not show up in release changelog.documentation: Documentation only changes. Will not show up in release changelog.feature request: Introducing a new feature.BREAKING CHANGE: Changes that break compatibility in some way with current major version.other: Changes that don't fit in the above categories.