[Bug]: `importModule()` calling `register()`/`registerHooks()` crashes under Jest 30.5+ (e.g. via `@storybook/test-runner`) — #35337's fix doesn't cover this
Describe the bug
Storybook's importModule() helper (used by serverRequire to load server-side config files like .storybook/main.ts, .storybook/test-runner.ts, etc.) unconditionally calls a Node module hook-registration API — module.registerHooks(), or module.register() as a fallback on Node <22.15 — the first time it's invoked, in order to set up Storybook's internal TypeScript loader:
// storybook/dist/_node-chunks/chunk-5G6TGQDE.js (compiled from
// code/core/src/shared/utils/module.ts)
async function importModule(path, { skipCache = false } = {}) {
if (!isTypescriptLoaderRegistered) {
let typescriptLoaderUrl = importMetaResolve("storybook/internal/bin/loader");
register(typescriptLoaderUrl, import.meta.url);
isTypescriptLoaderRegistered = true;
}
...
}
As of Jest 30.5.0 (jestjs/jest#16391), Jest deliberately throws if any code it loads calls module.register() or module.registerHooks(), because those hooks attach to the loader running Jest itself rather than the sandboxed loader used for test code, and would stay registered for every later test file in the worker:
module.register() is not supported in Jest: the hooks would attach to the
module loader running Jest itself, not to the sandboxed require/import used
by test code, and would stay registered for every later test file in this
worker. Use Jest's own customization points (transform, resolver,
moduleNameMapper) instead.
This makes @storybook/test-runner (which runs stories via Jest + Playwright, and calls serverRequire/importModule to load .storybook/test-runner.ts) completely unusable under Jest 30.5+: every story test suite fails immediately with the error above, before rendering anything.
I'm aware of #35336 / #35337, which already identified this unconditional call and fixed the Node 26 DEP0205 deprecation warning it caused — but that fix doesn't help here. It just changes which of the two APIs gets called (preferring registerHooks() over register() when available), and Jest's guard blocks both identically. So the crash reproduces on main/11.0.0-alpha.1 as well as on 10.6.0 — just with registerHooks() in the error message instead of register().
Reproduction link
https://github.com/mcshaman/storybook-issue
Reproduction steps
- An ESM project (
"type": "module") with Storybook +@storybook/react-webpack5(or any framework) and@storybook/test-runnerinstalled. - Add a minimal
.storybook/test-runner.ts(content doesn't matter —importModule()is called regardless of what's inside it, since it's what triggers the hook registration on first invocation):import type { TestRunnerConfig } from "@storybook/test-runner"; const config: TestRunnerConfig = {}; export default config; - Ensure Jest 30.5+ is resolved in
node_modules(it's pulled in transitively by@storybook/test-runner's ownjest: ^30.0.4dependency, so this can happen without a direct upgrade). - Build Storybook and run
test-storybookagainst it.
Expected behavior
test-storybook runs the stories normally.
Actual behavior
Every story's test suite fails immediately:
Test suite failed to run
module.register() is not supported in Jest: the hooks would attach to the
module loader running Jest itself, not to the sandboxed require/import used
by test code, and would stay registered for every later test file in this
worker. Use Jest's own customization points (transform, resolver,
moduleNameMapper) instead.
at throwHooksUnsupported (node_modules/jest-runtime/build/index.js:4102:17)
at importModule (node_modules/storybook/dist/_node-chunks/chunk-5G6TGQDE.js:735:5)
at serverRequire (node_modules/storybook/dist/_node-chunks/chunk-IQHYYTFR.js:14604:26)
at getTestRunnerConfig (node_modules/@storybook/test-runner/dist/index.js:7898:28)
at node_modules/@storybook/test-runner/playwright/jest-setup.js:3:32
System
- `storybook`: 10.6.0
- `@storybook/test-runner`: 0.24.5
- `jest-runtime` (resolved): 30.5.1
- Node: v24.11.0
- Project uses `"type": "module"`, Yarn Berry with `nodeLinker: node-modules`
Additional context
The core issue is that importModule() always tries to register a process-wide loader hook the first time it's called, with no way to opt out — this is fine standalone, but breaks anything that loads Storybook config from inside a Jest worker (not just @storybook/test-runner — anything using serverRequire/importModule in that context would hit this). A few possible directions:
- Detect a Jest worker (e.g.
process.env.JEST_WORKER_ID) and skip the hook registration in that environment, falling back to Storybook's own synchronous TS transform instead of relying on a loader hook. - Only attempt registration when the resolved config file actually needs TS stripping (currently it's attempted unconditionally on first call, regardless of the target file's extension).
- Expose a way for a consumer (like
@storybook/test-runner) to supply an already-loaded/transformed config directly, bypassingimportModule()'s TS-loader path entirely for this use case.
Source: storybookjs/storybook