Skip to content

Commit d0ea05d

Browse files
committed
feat: support rspack 2 alongside rspack 1
- detect installed @rspack/core major and adapt generated config: experiments.parallelLoader only under v1, exportsPresence 'auto' under v2 - read persistent cache config from both v1 (experiments.cache) and v2 (top-level cache) locations; auto-migrate legacy value under v2 with a warning - React Refresh: apply official @rspack/plugin-react-refresh v2 (optional peer) under rspack 2 via its integrator options (injectEntry: false + custom loader); vendor the client runtime files for webpack & rspack 1 manual wiring; drop the @rspack/plugin-react-refresh@1 dependency - load rspack commands lazily and fail fast with a clear error when running rspack 2 on Node < 20.19 (ESM-only package requires require(esm) support) - default --trace-* to the logger layer under rspack 2 (published binaries lack the perfetto trace layer) - verify @module-federation/runtime-tools is installed for MFv1 under rspack 2 - compile against @rspack/core@2 types; bridge ESM-only core into jest via a custom test environment
1 parent 0b1a3a5 commit d0ea05d

31 files changed

Lines changed: 1282 additions & 184 deletions

‎.changeset/rspack-2-support.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@callstack/repack": minor
3+
---
4+
5+
Add support for Rspack 2 while keeping Rspack 1 fully working.
6+
7+
Re.Pack now detects the installed `@rspack/core` major version and adapts:
8+
9+
- `experiments.parallelLoader` is only set under Rspack 1 (removed in Rspack 2)
10+
- `module.parser.javascript.exportsPresence` defaults to `'auto'` under Rspack 2 to keep Metro-like tolerance for invalid imports inside node_modules (overridable in your project config)
11+
- persistent cache configuration is read from both the Rspack 1 (`experiments.cache`) and Rspack 2 (top-level `cache`) locations; a legacy `experiments.cache` value is automatically applied to `cache` under Rspack 2 (with a warning), preventing silent loss of caching
12+
- React Refresh: under Rspack 2 Re.Pack applies the official `@rspack/plugin-react-refresh` v2 plugin (now an optional peer dependency - install it alongside `@rspack/core@2`); under Rspack 1 and webpack the client runtime files are bundled with Re.Pack, so the `@rspack/plugin-react-refresh@1` dependency is gone
13+
- `--trace-*` profiling defaults to the `logger` trace layer under Rspack 2 (published Rspack 2 binaries do not include the perfetto layer)
14+
- a clear error is raised when running Rspack 2 on Node.js older than 20.19 (Rspack 2 requires Node `^20.19.0 || >=22.12.0`)
15+
- `ModuleFederationPluginV1` verifies that `@module-federation/runtime-tools` is installed when running under Rspack 2 (it is no longer installed automatically by `@rspack/core`)

‎docs/rspack-2/README.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# Rspack 2.0 Support — Research & Planning
22

3-
> Status: **Research complete — all decisions made (Q1–Q5 in doc 04, React Refresh in
4-
> doc 06) and the V1–V11 verification checklist executed against 2.1.2 with no blockers
5-
> (doc 07). Ready for implementation.**
3+
> Status: **Implemented on this branch** — core dual-version support (plan phases 0–2)
4+
> is in `packages/repack`; all decisions recorded (Q1–Q5 in doc 04, React Refresh in
5+
> doc 06), V1–V11 verification executed with no blockers (doc 07).
6+
> Remaining follow-ups: CI matrix + tester-app/metro-compat validation under v2 (phase 3)
7+
> and flipping init/templates/website defaults to v2 (phase 4).
68
> Branch: `feat/rspack-2-support`
79
> Last updated: 2026-07-02
810

‎packages/repack/jest.config.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ module.exports = {
22
clearMocks: true,
33
moduleNameMapper: {
44
'^(\\.{1,2}/.*)\\.js$': '$1',
5+
'^@rspack/core$': '<rootDir>/jest.rspack-core-bridge.js',
56
},
67
setupFiles: ['./jest.setup.js'],
7-
testEnvironment: 'node',
8+
testEnvironment: '<rootDir>/jest.environment.js',
89
testMatch: ['**/__tests__/**/*.ts?(x)'],
910
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const { TestEnvironment: NodeEnvironment } = require('jest-environment-node');
2+
3+
/**
4+
* @rspack/core v2 is published as a pure ESM package, which Jest's sandboxed
5+
* CJS module runtime cannot load (even `createRequire` inside the sandbox is
6+
* wrapped by Jest and loops back into the module registry).
7+
*
8+
* Test environments are loaded outside the Jest sandbox with Node's real
9+
* module system, where loading ESM through `require()`/`import()` is
10+
* supported. Load @rspack/core here and expose it to the sandbox via a
11+
* global - see jest.rspack-core-bridge.js for the consuming side.
12+
*/
13+
class RspackCoreEnvironment extends NodeEnvironment {
14+
async setup() {
15+
await super.setup();
16+
this.global.__RSPACK_CORE__ = await import('@rspack/core');
17+
}
18+
}
19+
20+
module.exports = RspackCoreEnvironment;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// @rspack/core v2 is published as a pure ESM package, which Jest's sandboxed
2+
// CJS module runtime cannot load. The real module is loaded natively by the
3+
// custom test environment (jest.environment.js) and exposed via a global;
4+
// this bridge maps `require('@rspack/core')` inside tests onto it.
5+
//
6+
// The `__esModule` marker keeps Babel's import interop treating this as an
7+
// ESM-like namespace, so named imports (`import { rspack } from '@rspack/core'`)
8+
// keep working.
9+
const core = globalThis.__RSPACK_CORE__;
10+
11+
if (!core) {
12+
throw new Error(
13+
'@rspack/core was not preloaded by the test environment - ' +
14+
'make sure jest.environment.js is set as the testEnvironment'
15+
);
16+
}
17+
18+
module.exports = { __esModule: true, ...core, default: core.default ?? core };

‎packages/repack/package.json‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"peerDependencies": {
6666
"@module-federation/enhanced": ">=0.6.10",
6767
"@rspack/core": ">=1",
68+
"@rspack/plugin-react-refresh": "^2.0.0",
6869
"react-native": ">=0.74",
6970
"webpack": ">=5.90"
7071
},
@@ -75,14 +76,16 @@
7576
"@rspack/core": {
7677
"optional": true
7778
},
79+
"@rspack/plugin-react-refresh": {
80+
"optional": true
81+
},
7882
"webpack": {
7983
"optional": true
8084
}
8185
},
8286
"dependencies": {
8387
"@callstack/repack-dev-server": "workspace:*",
8488
"@discoveryjs/json-ext": "^0.5.7",
85-
"@rspack/plugin-react-refresh": "1.0.0",
8689
"babel-loader": "^9.2.1",
8790
"colorette": "^2.0.20",
8891
"dedent": "^0.7.0",
@@ -113,8 +116,9 @@
113116
"@babel/plugin-transform-modules-commonjs": "^7.23.2",
114117
"@module-federation/enhanced": "0.8.9",
115118
"@module-federation/sdk": "0.6.10",
116-
"@rspack/core": "catalog:",
117-
"@swc/helpers": "catalog:",
119+
"@rspack/core": "^2.1.2",
120+
"@rspack/plugin-react-refresh": "^2.0.2",
121+
"@swc/helpers": "^0.5.23",
118122
"@types/babel__core": "^7.20.5",
119123
"@types/dedent": "^0.7.0",
120124
"@types/gradient-string": "^1.1.6",

‎packages/repack/src/commands/common/config/getRepackConfig.ts‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,45 @@
1+
import { getRspackMajorVersion } from '../../../helpers/index.js';
12
import { getMinimizerConfig } from './getMinimizerConfig.js';
23

3-
function getExperimentsConfig(bundler: 'rspack' | 'webpack') {
4+
function getExperimentsConfig(bundler: 'rspack' | 'webpack', rootDir: string) {
45
if (bundler === 'rspack') {
5-
return { parallelLoader: true };
6+
const rspackMajor = getRspackMajorVersion(rootDir) ?? 1;
7+
if (rspackMajor < 2) {
8+
return { parallelLoader: true };
9+
}
10+
// Rspack 2 removed `experiments.parallelLoader` (parallel loading is
11+
// stable and opt-in per rule via `use[].parallel`) - nothing to set.
12+
}
13+
}
14+
15+
function getModuleConfig(bundler: 'rspack' | 'webpack', rootDir: string) {
16+
if (bundler === 'rspack') {
17+
const rspackMajor = getRspackMajorVersion(rootDir) ?? 1;
18+
if (rspackMajor >= 2) {
19+
// Rspack 2 changed the `exportsPresence` default from 'warn' to 'error',
20+
// which breaks builds on invalid imports inside node_modules - a common
21+
// occurrence in the React Native ecosystem that Metro tolerates.
22+
// Restore Metro-like leniency by default; users can override this
23+
// in their project config.
24+
return {
25+
parser: { javascript: { exportsPresence: 'auto' as const } },
26+
};
27+
}
628
}
729
}
830

931
export async function getRepackConfig(
1032
bundler: 'rspack' | 'webpack',
1133
rootDir: string
1234
) {
13-
const experiments = getExperimentsConfig(bundler);
35+
const experiments = getExperimentsConfig(bundler, rootDir);
36+
const moduleConfiguration = getModuleConfig(bundler, rootDir);
1437
const minimizerConfiguration = await getMinimizerConfig(bundler, rootDir);
1538

1639
return {
1740
devtool: 'source-map',
1841
experiments,
42+
module: moduleConfiguration,
1943
output: {
2044
clean: true,
2145
hashFunction: 'xxhash64',

‎packages/repack/src/commands/common/config/makeCompilerConfig.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { merge } from 'webpack-merge';
2+
import { isRspack2 } from '../../../helpers/index.js';
23
import type {
34
BundleArguments,
45
ConfigurationObject,
56
StartArguments,
67
} from '../../types.js';
8+
import { migrateLegacyRspackCacheConfig } from '../migrateLegacyRspackCacheConfig.js';
79
import { getCliOverrides } from './getCliOverrides.js';
810
import { getCommandConfig } from './getCommandConfig.js';
911
import { getConfigFilePath } from './getConfigFilePath.js';
@@ -70,6 +72,12 @@ export async function makeCompilerConfig<C extends ConfigurationObject>(
7072
normalizeConfig(config, options.platforms[index])
7173
);
7274

75+
// Rspack 2 silently ignores the legacy `experiments.cache` option -
76+
// honor it by moving it to the top-level `cache` option & warn the user
77+
if (bundler === 'rspack' && isRspack2(rootDir)) {
78+
migrateLegacyRspackCacheConfig(normalizedConfigs);
79+
}
80+
7381
const plugins = normalizedConfigs.flatMap((config) =>
7482
'plugins' in config ? config.plugins : []
7583
);

‎packages/repack/src/commands/common/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './getDevMiddleware.js';
22
export * from './getMaxWorkers.js';
33
export * from './getMimeType.js';
4+
export * from './migrateLegacyRspackCacheConfig.js';
45
export * from './parseUrl.js';
56
export * from './resetPersistentCache.js';
67
export * from './resolveProjectPath.js';
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as colorette from 'colorette';
2+
3+
interface CacheLikeConfiguration {
4+
cache?: unknown;
5+
experiments?: { cache?: unknown };
6+
}
7+
8+
let warningDisplayed = false;
9+
10+
/**
11+
* Rspack 2 moved the persistent cache configuration from `experiments.cache`
12+
* to the top-level `cache` option. The legacy key is not validated against -
13+
* it's silently ignored, which means users migrating a Rspack 1 config would
14+
* lose persistent caching without any signal.
15+
*
16+
* To prevent that, when running Rspack 2 we honor a legacy `experiments.cache`
17+
* value by moving it to `cache` (unless `cache` is already set) and warn the
18+
* user to update their project config.
19+
*/
20+
export function migrateLegacyRspackCacheConfig(configs: object[]) {
21+
for (const config of configs as CacheLikeConfiguration[]) {
22+
const legacyCacheConfig = config.experiments?.cache;
23+
if (legacyCacheConfig === undefined) continue;
24+
25+
if (config.cache === undefined) {
26+
config.cache = legacyCacheConfig;
27+
}
28+
delete config.experiments?.cache;
29+
30+
if (!warningDisplayed) {
31+
warningDisplayed = true;
32+
console.warn(
33+
colorette.yellow(
34+
"In Rspack 2 'experiments.cache' was moved to the top-level 'cache' option " +
35+
'and the legacy key is silently ignored. ' +
36+
"Re.Pack applied your 'experiments.cache' value to 'cache' automatically - " +
37+
"please update your project config to use the top-level 'cache' option.\n"
38+
)
39+
);
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)