Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/test-main-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ on:
- 'website/**'
workflow_dispatch:

permissions:
contents: read

jobs:
test_main_matrix:
name: Tests [Node ${{ matrix.node_version }}]
Expand All @@ -23,5 +26,9 @@ jobs:
with:
node-version: ${{ matrix.node_version }}

# Rspack 2 requires Node ^20.19.0 || >=22.12.0, so the older Node
# lanes run the repack unit suite against Rspack 1
- name: Run tests
run: pnpm test
env:
RSPACK_MAJOR: ${{ contains(fromJSON('["18", "20"]'), matrix.node_version) && '1' || '2' }}
39 changes: 39 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ on:
- 'website/**'
workflow_dispatch:

permissions:
contents: read

jobs:
lint:
name: Lint
Expand Down Expand Up @@ -53,3 +56,39 @@ jobs:

- name: Run tests
run: pnpm test

test_pr_rspack1:
name: Tests [Rspack 1]
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2
- name: Setup workspace
uses: ./.github/actions/setup-pnpm-node
with:
node-version: '24'

- name: Run tests against Rspack 1
run: pnpm turbo run test:rspack1

test_pr_windows:
name: Tests [Windows]
if: github.event_name == 'pull_request'
runs-on: windows-latest

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2
- name: Setup workspace
uses: ./.github/actions/setup-pnpm-node
with:
node-version: '24'

# the require(esm) loading path of @rspack/core@2 and the version
# helpers' filesystem resolution are Windows-sensitive - run the
# repack unit suite under both majors
- name: Run repack tests
run: pnpm turbo run test --filter @callstack/repack

- name: Run repack tests against Rspack 1
run: pnpm turbo run test:rspack1
3 changes: 2 additions & 1 deletion packages/repack/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ module.exports = {
clearMocks: true,
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
'^@rspack/core$': '<rootDir>/jest.rspack-core-bridge.js',
},
setupFiles: ['./jest.setup.js'],
testEnvironment: 'node',
testEnvironment: '<rootDir>/jest.environment.js',
testMatch: ['**/__tests__/**/*.ts?(x)'],
};
32 changes: 32 additions & 0 deletions packages/repack/jest.environment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const { TestEnvironment: NodeEnvironment } = require('jest-environment-node');

/**
* @rspack/core v2 is published as a pure ESM package, which Jest's sandboxed
* CJS module runtime cannot load (even `createRequire` inside the sandbox is
* wrapped by Jest and loops back into the module registry).
*
* Test environments are loaded outside the Jest sandbox with Node's real
* module system, where loading ESM through `require()`/`import()` is
* supported. Load @rspack/core here and expose it to the sandbox via a
* global - see jest.rspack-core-bridge.js for the consuming side.
*
* The environment is parameterized on RSPACK_MAJOR (default 2) so the suite
* can run against both supported Rspack majors:
* - v2: `await import('@rspack/core')` (ESM-only package),
* - v1: plain `require` of the aliased `@rspack/core-v1` devDependency - the
* package is CJS, and requiring it directly sidesteps any reliance on
* cjs-module-lexer named-export synthesis.
* `__RSPACK_MAJOR__` is exposed alongside so tests can gate major-specific
* assertions.
*/
class RspackCoreEnvironment extends NodeEnvironment {
async setup() {
await super.setup();
const major = Number(process.env.RSPACK_MAJOR ?? '2');
this.global.__RSPACK_CORE__ =
major === 1 ? require('@rspack/core-v1') : await import('@rspack/core');
this.global.__RSPACK_MAJOR__ = major;
}
}

module.exports = RspackCoreEnvironment;
18 changes: 18 additions & 0 deletions packages/repack/jest.rspack-core-bridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// @rspack/core v2 is published as a pure ESM package, which Jest's sandboxed
// CJS module runtime cannot load. The real module is loaded natively by the
// custom test environment (jest.environment.js) and exposed via a global;
// this bridge maps `require('@rspack/core')` inside tests onto it.
//
// The `__esModule` marker keeps Babel's import interop treating this as an
// ESM-like namespace, so named imports (`import { rspack } from '@rspack/core'`)
// keep working.
const core = globalThis.__RSPACK_CORE__;

if (!core) {
throw new Error(
'@rspack/core was not preloaded by the test environment - ' +
'make sure jest.environment.js is set as the testEnvironment'
);
}

module.exports = { __esModule: true, ...core, default: core.default ?? core };
7 changes: 5 additions & 2 deletions packages/repack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"build:ts": "tsc -p tsconfig.build.json --emitDeclarationOnly",
"build": "pnpm run \"/^build:.*/\"",
"test": "jest",
"test:rspack1": "cross-env RSPACK_MAJOR=1 jest",
"typecheck": "tsc --noEmit",
"archive": "pnpm build && pnpm pack",
"clang-format": "pnpm clang-format:ios && pnpm clang-format:android",
Expand Down Expand Up @@ -113,8 +114,9 @@
"@babel/plugin-transform-modules-commonjs": "^7.23.2",
"@module-federation/enhanced": "0.8.9",
"@module-federation/sdk": "0.6.10",
"@rspack/core": "catalog:",
"@swc/helpers": "catalog:",
"@rspack/core": "^2.1.2",
"@rspack/core-v1": "npm:@rspack/core@^1.7.12",
"@swc/helpers": "^0.5.23",
"@types/babel__core": "^7.20.5",
"@types/dedent": "^0.7.0",
"@types/gradient-string": "^1.1.6",
Expand All @@ -127,6 +129,7 @@
"@types/shallowequal": "^1.1.1",
"babel-jest": "^29.7.0",
"clang-format": "^1.8.0",
"cross-env": "^7.0.3",
"jest": "^29.7.0",
"react": "catalog:",
"react-native": "catalog:",
Expand Down
10 changes: 10 additions & 0 deletions packages/repack/src/__tests__/rspackTestLane.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { rspackVersion } from '@rspack/core';

// Guards the dual-major test wiring itself: the custom Jest environment
// (jest.environment.js) loads @rspack/core v2 by default and the aliased
// @rspack/core-v1 under RSPACK_MAJOR=1. If the env var stops being honored,
// the "v1 lane" silently tests v2 twice - this catches that.
test('the loaded @rspack/core major matches the requested RSPACK_MAJOR lane', () => {
const requested = Number(process.env.RSPACK_MAJOR ?? '2');
expect(Number(rspackVersion.split('.')[0])).toBe(requested);
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import path from 'node:path';
import { resolveProjectPath } from '../resolveProjectPath.js';

describe('resolveProjectPath', () => {
Expand All @@ -6,7 +7,9 @@ describe('resolveProjectPath', () => {
expected: string,
root = '/project/root'
) => {
expect(resolveProjectPath(input, root)).toBe(expected);
// path.resolve makes the expectation platform-agnostic - on Windows the
// resolved paths carry a drive letter and win32 separators
expect(resolveProjectPath(input, root)).toBe(path.resolve(expected));
};

it('should resolve [projectRoot] prefix correctly', () => {
Expand Down Expand Up @@ -41,4 +44,13 @@ describe('resolveProjectPath', () => {
'/a/b/c/d/e/f'
);
});

it('should resolve correctly when up-level navigation reaches the filesystem root', () => {
// regression: string-replace + path.join composition produced UNC-like
// `\\...` paths on Windows once the `../` navigation collapsed rootDir
// to the bare filesystem root
expectResolved('[projectRoot^1]/src/index.js', '/src/index.js', '/project');
expectResolved('[projectRoot^2]/index.js', '/index.js', '/project/root');
expectResolved('[projectRoot^4]/index.js', '/index.js', '/a/b');
});
});
27 changes: 25 additions & 2 deletions packages/repack/src/commands/common/resetPersistentCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,33 @@ import type { Configuration as RspackConfiguration } from '@rspack/core';
import * as colorette from 'colorette';
import type { Configuration as WebpackConfiguration } from 'webpack';

type RspackCacheOptions = NonNullable<
// Rspack 2 moved the persistent cache configuration from `experiments.cache`
// to the top-level `cache` option (same shape). The v2 types no longer
// declare the legacy location, so extend them with it to support configs
// written for either major.
type RspackCacheOptions = RspackConfiguration['cache'];
type RspackExperimentsWithLegacyCache = NonNullable<
RspackConfiguration['experiments']
>['cache'];
> & { cache?: RspackCacheOptions };

export interface RspackConfigurationWithLegacyCache {
cache?: RspackCacheOptions;
experiments?: RspackExperimentsWithLegacyCache;
}

type WebpackCacheOptions = WebpackConfiguration['cache'];

/**
* Reads the persistent cache configuration from a Rspack config,
* supporting both the Rspack 1 (`experiments.cache`) and
* Rspack 2 (top-level `cache`) locations.
*/
export function getRspackCacheConfig(
config: RspackConfigurationWithLegacyCache
): RspackCacheOptions {
return config.experiments?.cache ?? config.cache;
Comment thread
dannyhw marked this conversation as resolved.
Outdated
}

function getDefaultCacheDirectory(
bundler: 'rspack' | 'webpack',
rootDir: string
Expand All @@ -31,6 +53,7 @@ function getRspackCachePaths(
for (const cacheConfig of cacheConfigs) {
if (
typeof cacheConfig === 'object' &&
cacheConfig !== null &&
'storage' in cacheConfig &&
cacheConfig.storage?.directory
) {
Expand Down
10 changes: 7 additions & 3 deletions packages/repack/src/commands/common/resolveProjectPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ export function resolveProjectPath(filepath: string, rootDir: string) {
if (!match) return filepath;

const [prefix, upLevels] = match;
const upPath = '../'.repeat(Number(upLevels ?? 0));
const rootPath = path.join(rootDir, upPath);
return path.resolve(filepath.replace(prefix, rootPath));
const upSegments = Array.from({ length: Number(upLevels ?? 0) }, () => '..');
const restPath = filepath.slice(prefix.length + 1);
Comment thread
dannyhw marked this conversation as resolved.
Outdated
// resolve segment-by-segment instead of string-replace + join composition:
// on Windows, joining enough `../` segments to reach the filesystem root
// yields a bare root ending in a separator, and concatenating the rest of
// the path after it produced a double-separator (UNC-like `\\...`) prefix
return path.resolve(rootDir, ...upSegments, restPath);
}
26 changes: 14 additions & 12 deletions packages/repack/src/commands/rspack/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CLIError } from '../../helpers/index.js';
import { makeCompilerConfig } from '../common/config/makeCompilerConfig.js';
import {
getMaxWorkers,
getRspackCacheConfig,
normalizeStatsOptions,
resetPersistentCache,
setupEnvironment,
Expand All @@ -25,17 +26,15 @@ export async function bundle(
cliConfig: CliConfig,
args: BundleArguments
) {
const [config] = await makeCompilerConfig<Configuration>({
args: args,
bundler: 'rspack',
command: 'bundle',
rootDir: cliConfig.root,
platforms: [args.platform],
reactNativePath: cliConfig.reactNativePath,
});

// remove devServer configuration to avoid schema validation errors
delete config.devServer;
const [{ devServer: _devServer, ...config }] =
await makeCompilerConfig<Configuration>({
args: args,
bundler: 'rspack',
command: 'bundle',
rootDir: cliConfig.root,
platforms: [args.platform],
reactNativePath: cliConfig.reactNativePath,
});

// expose selected args as environment variables
setupEnvironment(args);
Expand All @@ -51,7 +50,7 @@ export async function bundle(
resetPersistentCache({
bundler: 'rspack',
rootDir: cliConfig.root,
cacheConfigs: [config.experiments?.cache],
cacheConfigs: [getRspackCacheConfig(config)],
});
}

Expand Down Expand Up @@ -86,6 +85,9 @@ export async function bundle(
}
};

// `devServer` is split off above - it's not needed for bundling, and
// Re.Pack's own dev server options (augmented onto `Configuration`) are
// not assignable to Rspack's bundled `DevServer` type
const compiler = rspack(config);

return new Promise<void>((resolve) => {
Expand Down
21 changes: 18 additions & 3 deletions packages/repack/src/commands/rspack/start.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Configuration } from '@rspack/core';
import type { Configuration, MultiRspackOptions } from '@rspack/core';
import packageJson from '../../../package.json';
import { VERBOSE_ENV_KEY } from '../../env.js';
import { CLIError, isTruthyEnv } from '../../helpers/index.js';
Expand All @@ -14,6 +14,7 @@ import {
getDevMiddleware,
getMaxWorkers,
getMimeType,
getRspackCacheConfig,
parseUrl,
resetPersistentCache,
resolveProjectPath,
Expand Down Expand Up @@ -83,7 +84,7 @@ export async function start(
resetPersistentCache({
bundler: 'rspack',
rootDir: cliConfig.root,
cacheConfigs: configs.map((config) => config.experiments?.cache),
cacheConfigs: configs.map(getRspackCacheConfig),
});
}

Expand All @@ -96,7 +97,21 @@ export async function start(
);
}

const compiler = new Compiler(configs, reporter, cliConfig.root);
// CAST - no clean solution available here:
// Re.Pack augments `Configuration.devServer` with its own dev server options
// (src/types/dev-server-options.d.ts), while Rspack 2 types `devServer` with
// its bundled `DevServer` type. The two are structurally incompatible solely
// because each pulls `proxy` types from a different copy of
// http-proxy-middleware, so no narrowing or `satisfies` can bridge them.
// Unlike `bundle`, `devServer` cannot be stripped from the config here -
// the dev server flow reads it back from `compiler.options`. At runtime
// Rspack accepts & preserves the key (validation is permissive, verified
// in agent_context/rspackv2-jul2026/07-verification-results.md).
const compiler = new Compiler(
configs as unknown as MultiRspackOptions,
reporter,
cliConfig.root
);

const { createServer } = await import('@callstack/repack-dev-server');
const { start, stop } = await createServer({
Expand Down
Loading
Loading