From 781d3d099d79907736191759a71836643a94ff16 Mon Sep 17 00:00:00 2001 From: Federico Bartoli Date: Mon, 17 Aug 2026 11:44:11 +0200 Subject: [PATCH] fix(rspack): preload @rspack/core before loading rspack config files On Node 22.18+/24 with native TypeScript stripping, `@nx/rspack/plugin` loads `rspack.config.ts` files via `require()`, which Node treats as ESM. When that ESM link fails (e.g. an extensionless `.ts` sibling import such as `./module-federation.config`), Node drops the failing config from its loader cache but leaves already-linked dependencies like `@rspack/core` cached in the un-instantiated state. The loader then falls back to swc/ts-node, and the re-required `@rspack/core` fails with ERR_INTERNAL_ASSERTION: Unexpected module status 0. Cannot require() ES Module .../@rspack/core/dist/index.js because it is not yet fully loaded. taking down project graph creation for every command. Load `@rspack/core` before any user rspack config is loaded, in `resolveUserDefinedRspackConfig` (used by both the inference plugin and the executor). Once it is fully evaluated, a failed config link can no longer leave it half-loaded. Same approach as the `vitest/node` preload in #34261. Fixes #36685 --- e2e/rspack/tests/rspack.spec.ts | 61 +++++++++++++++++++ .../resolve-user-defined-rspack-config.ts | 14 +++++ 2 files changed, 75 insertions(+) diff --git a/e2e/rspack/tests/rspack.spec.ts b/e2e/rspack/tests/rspack.spec.ts index b56dbbc19b5..67566edcf60 100644 --- a/e2e/rspack/tests/rspack.spec.ts +++ b/e2e/rspack/tests/rspack.spec.ts @@ -6,7 +6,9 @@ import { updateFile, runCLI, createFile, + packageInstall, readJson, + removeFile, updateJson, } from '@nx/e2e-utils'; @@ -39,6 +41,65 @@ describe('rspack e2e', () => { expect(rspackPlugin).toBeDefined(); }); + it('should infer targets from a rspack.config.ts that imports @rspack/core and an extensionless .ts sibling', () => { + const appName = uniq('app'); + + runCLI( + `generate @nx/react:application --directory=apps/${appName} --bundler=rspack --e2eTestRunner=none` + ); + + // The extensionless `.ts` import fails Node's native ESM link, so the + // config falls back to swc-node (installed below) and re-requires + // @rspack/core, which the failed link left cached (see #36685). + packageInstall('@swc-node/register', undefined, '~1.11.1'); + packageInstall('@swc/core', undefined, '~1.15.5'); + removeFile(`apps/${appName}/rspack.config.js`); + createFile( + `apps/${appName}/rspack.shared.ts`, + `export const devServerPort = 4310;` + ); + createFile( + `apps/${appName}/rspack.config.ts`, + ` + import { NxAppRspackPlugin } from '@nx/rspack/app-plugin'; + import { NxReactRspackPlugin } from '@nx/rspack/react-plugin'; + import { CopyRspackPlugin } from '@rspack/core'; + import { join } from 'path'; + + import { devServerPort } from './rspack.shared'; + + export default { + output: { + path: join(__dirname, '../../dist/${appName}'), + }, + devServer: { + port: devServerPort, + }, + plugins: [ + new NxAppRspackPlugin({ + tsConfig: './tsconfig.app.json', + main: './src/main.tsx', + index: './src/index.html', + baseHref: '/', + assets: ['./src/favicon.ico', './src/assets'], + styles: ['./src/styles.scss'], + outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none', + optimization: process.env['NODE_ENV'] === 'production', + }), + new NxReactRspackPlugin(), + new CopyRspackPlugin({ patterns: [] }), + ], + };` + ); + + const project = JSON.parse( + runCLI(`show project ${appName} --json`, { daemon: false }) + ); + + expect(project.targets.build).toBeDefined(); + expect(project.targets['serve-static'].options.port).toBe(4310); + }); + describe('config types', () => { it('should support a standard config object', () => { const appName = uniq('app'); diff --git a/packages/rspack/src/utils/resolve-user-defined-rspack-config.ts b/packages/rspack/src/utils/resolve-user-defined-rspack-config.ts index 35965884399..9c071469312 100644 --- a/packages/rspack/src/utils/resolve-user-defined-rspack-config.ts +++ b/packages/rspack/src/utils/resolve-user-defined-rspack-config.ts @@ -6,5 +6,19 @@ export async function resolveUserDefinedRspackConfig( /** Skip require cache and return latest content */ reload = false ) { + await preloadRspackCore(); return await loadConfigFile(path); } + +// Node keeps @rspack/core cached un-instantiated when a config's require(esm) +// link fails (e.g. an extensionless `.ts` sibling import under native type +// stripping), and every later require('@rspack/core') then throws +// "Unexpected module status 0". See https://github.com/nrwl/nx/issues/36685 +async function preloadRspackCore(): Promise { + try { + await new Function('return import("@rspack/core")')(); + } catch { + // Not resolvable from here, or import() unavailable (e.g. under Jest). + // Whatever is actually wrong surfaces when the config itself loads. + } +}