Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
@@ -0,0 +1,40 @@
import { getRspackCacheConfigs } from '../resetPersistentCache.js';

describe('getRspackCacheConfigs', () => {
it('should return the legacy experiments.cache config (Rspack 1)', () => {
const cache = { type: 'persistent' as const };
expect(getRspackCacheConfigs({ experiments: { cache } })).toEqual([cache]);
});

it('should return the top-level cache config (Rspack 2)', () => {
const cache = {
type: 'persistent' as const,
storage: { type: 'filesystem' as const, directory: '.cache/custom' },
};
expect(getRspackCacheConfigs({ cache })).toEqual([cache]);
});

it('should collect both locations when a config carries both keys', () => {
// e.g. a dual-major config or defaults merged with a user config -
// which location is active depends on the installed Rspack major,
// so both need to be considered when resetting the cache
const legacyCache = { type: 'persistent' as const };
const cache = {
type: 'persistent' as const,
storage: { type: 'filesystem' as const, directory: '.cache/custom' },
};
expect(
getRspackCacheConfigs({ cache, experiments: { cache: legacyCache } })
).toEqual([legacyCache, cache]);
});

it('should fall back to a single entry when no cache config is present', () => {
expect(getRspackCacheConfigs({})).toEqual([undefined]);
expect(getRspackCacheConfigs({ experiments: {} })).toEqual([undefined]);
});

it('should keep boolean cache configs', () => {
expect(getRspackCacheConfigs({ cache: true })).toEqual([true]);
expect(getRspackCacheConfigs({ cache: false })).toEqual([false]);
});
});
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');
});
});
33 changes: 31 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,39 @@ 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'];

/**
* Collects the persistent cache configurations from a Rspack config,
* covering both the Rspack 1 (`experiments.cache`) and
* Rspack 2 (top-level `cache`) locations. A merged or dual-major config
* can carry both keys and which one is active depends on the installed
* Rspack major, so `--reset-cache` clears both locations.
*/
export function getRspackCacheConfigs(
config: RspackConfigurationWithLegacyCache
): RspackCacheOptions[] {
const cacheConfigs = [config.experiments?.cache, config.cache].filter(
(cacheConfig) => cacheConfig !== undefined
);
// without an explicit cache config, the default cache location still applies
return cacheConfigs.length > 0 ? cacheConfigs : [undefined];
}

function getDefaultCacheDirectory(
bundler: 'rspack' | 'webpack',
rootDir: string
Expand All @@ -31,6 +59,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,
getRspackCacheConfigs,
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: getRspackCacheConfigs(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
Loading
Loading