-
Notifications
You must be signed in to change notification settings - Fork 159
test: add dual-major rspack compatibility smoke suite #1404
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/rspack-2-integration
Are you sure you want to change the base?
Changes from all commits
330c7f7
753e255
8995b68
6aa39e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| --- | ||
| "@callstack/repack": minor | ||
| --- | ||
|
|
||
| Add Rspack 2 support. Re.Pack now works with both Rspack 1.x and 2.x from a single release - it detects the installed `@rspack/core` major and adjusts its behavior automatically. Rspack 1 and webpack setups are unaffected: nothing changes unless you upgrade. | ||
|
|
||
| To move a project to Rspack 2: | ||
|
|
||
| - Use Node.js `^20.19.0 || >=22.12.0` - required by Rspack 2 itself; Re.Pack raises a clear error on older versions instead of failing with `ERR_REQUIRE_ESM` | ||
| - Install `@rspack/plugin-react-refresh@^2` and `react-refresh` - under Rspack 2, development/HMR support is wired through the official React Refresh plugin | ||
| - Move any `experiments.cache` configuration to the top-level `cache` option - Rspack 2 ignores the legacy location, and Re.Pack warns when it detects it |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # per-run artifacts - the fixtures are standalone projects that get a fresh | ||
| # repack tarball installed on every run | ||
| .pack/ | ||
| fixtures/*/node_modules/ | ||
| fixtures/*/pnpm-lock.yaml | ||
| fixtures/*/callstack-repack.tgz | ||
| fixtures/*/dist/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| # rspack-compat | ||
|
|
||
| Built-dist smoke tests for `@callstack/repack` against both supported | ||
| `@rspack/core` majors (1.x and 2.x). | ||
|
|
||
| Unit tests exercise sources through the Jest/Babel pipeline; the failure | ||
| modes this suite covers — `require(esm)` interop, loader resolution, React | ||
| Refresh runtime source selection — only reproduce against the **built dist** | ||
| installed in a real project layout. The runner therefore always builds | ||
| `packages/repack`, packs it into a tarball, installs the tarball into each | ||
| fixture, and runs the assertions there. | ||
|
|
||
| ## Running | ||
|
|
||
| ```sh | ||
| pnpm --filter rspack-compat-test test:smoke # both fixtures | ||
| pnpm --filter rspack-compat-test test:smoke rspack-2 # a single fixture | ||
| ``` | ||
|
|
||
| The suite performs network installs and full repack builds, so it is | ||
| deliberately **not** part of the default `test` pipeline — CI runs it in a | ||
| dedicated lane. | ||
|
|
||
| ## What is asserted (per major) | ||
|
|
||
| 1. `getRepackConfig` routing — `experiments.parallelLoader` kept under | ||
| Rspack 1 / dropped under Rspack 2; `exportsPresence: 'auto'` parser | ||
| override under Rspack 2 only. | ||
| 2. Legacy `experiments.cache` handling, composed with the `isRspack2` gate | ||
| the commands use — the gate reads the fixture's installed major, Rspack 1 | ||
| sees no warning, Rspack 2 warns exactly once under the warn-only policy, | ||
| the config is never mutated, and the cache accessor reads the legacy | ||
| location. | ||
| 3. `ensureNodeSupportsRspack` passes on a supported Node version. | ||
| 4. The compiled command modules (`dist/commands/rspack/{bundle,start,Compiler}.js`) | ||
| load from the tarball-installed package — the eager | ||
| `require('@rspack/core')` they compile to (`require(esm)` under | ||
| Rspack 2), the `rspack` named export they call, and the lazy | ||
| `@callstack/repack/commands/rspack` entry. | ||
| 5. A full dev build through `DevelopmentPlugin` — HMR client and React | ||
| Refresh wiring present in the bundle, with the refresh runtime coming | ||
| from the correct source per major: the vendored client files | ||
| (`packages/repack/vendor/react-refresh/`) under Rspack 1 with zero | ||
| official-plugin references, the official `@rspack/plugin-react-refresh` | ||
| under Rspack 2 with zero vendored references. | ||
|
|
||
| ## Why the fixtures are standalone (tarball install) | ||
|
|
||
| Inside the pnpm workspace, repack is linked as a symlink, so its own | ||
| `@rspack/core@^2` devDependency shadows any fixture's v1 pin — bare | ||
| `import '@rspack/core'` from `packages/repack/dist` resolves repack's copy | ||
| before the project's. An in-workspace v1 fixture would therefore silently | ||
| test v2 twice. Published installs don't have this problem (`@rspack/core` | ||
| is a peer dependency there), and shipped code deliberately carries no | ||
| monorepo-aware resolution workarounds, so the fixtures replicate the | ||
| published layout instead: each one is its own pnpm workspace root | ||
| (`pnpm-workspace.yaml` with `packages: []`) with its own `node_modules`, | ||
| installing repack from a packed tarball — the same pattern as | ||
| `apps/tester-app-rspack1`. | ||
|
|
||
| `smoke.cjs` enforces this as a **lane guard** before running any behavior | ||
| assertions: the fixture's installed `@rspack/core` major must match the | ||
| major declared in its manifest, the `@rspack/core` that *repack itself* | ||
| resolves must be that same copy, and the resolved repack package must live | ||
| inside the fixture's `node_modules` (never a workspace link back to | ||
| `packages/repack`). Any violation aborts the run with a non-zero exit. | ||
|
|
||
| `react-native` is stubbed (`fixtures/shared/rn-stub`) with just the modules | ||
| Re.Pack's development runtime imports — real RN sources need the full | ||
| flow/babel loader chain, which is out of scope for wiring-level smoke tests. | ||
|
|
||
| See `agent_context/rspackv2-jul2026/design.md` for the overall dual-major | ||
| support design. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "name": "rspack-compat-fixture-rspack-1", | ||
| "version": "0.0.1", | ||
| "description": "Rspack 1 smoke fixture - installs @callstack/repack from a packed tarball like a real user project", | ||
| "private": true, | ||
| "devDependencies": { | ||
| "@callstack/repack": "file:./callstack-repack.tgz", | ||
| "@rspack/core": "^1.7.12", | ||
| "react-native": "file:../shared/rn-stub" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Marks this directory as its own workspace root, so pnpm treats it as a | ||
| # standalone project instead of a member of the repository workspace. | ||
| # See ../../README.md for why the fixtures must stay outside the workspace. | ||
| packages: [] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export function App() { | ||
| return null; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "name": "rspack-compat-fixture-rspack-2", | ||
| "version": "0.0.1", | ||
| "description": "Rspack 2 smoke fixture - installs @callstack/repack from a packed tarball like a real user project", | ||
| "private": true, | ||
| "devDependencies": { | ||
| "@callstack/repack": "file:./callstack-repack.tgz", | ||
| "@rspack/core": "^2.1.2", | ||
| "@rspack/plugin-react-refresh": "^2.0.2", | ||
| "@swc/helpers": "^0.5.23", | ||
| "react-native": "file:../shared/rn-stub", | ||
| "react-refresh": "^0.18.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Marks this directory as its own workspace root, so pnpm treats it as a | ||
| # standalone project instead of a member of the repository workspace. | ||
| # See ../../README.md for why the fixtures must stay outside the workspace. | ||
| packages: [] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export function App() { | ||
| return null; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export default { | ||
| hide() {}, | ||
| showMessage() {}, | ||
| dismissRedbox() {}, | ||
| reportException() {}, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export default { | ||
| hide() {}, | ||
| showMessage() {}, | ||
| dismissRedbox() {}, | ||
| reportException() {}, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export default { | ||
| hide() {}, | ||
| showMessage() {}, | ||
| dismissRedbox() {}, | ||
| reportException() {}, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export const DevSettings = { reload() {} }; | ||
| export const LogBox = { clearAllLogs() {} }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "name": "react-native", | ||
| "version": "0.84.1", | ||
| "description": "Stub of react-native for the rspack-compat smoke fixtures - covers the modules imported by Re.Pack's development runtime (real RN sources need the full flow/babel loader chain, which is out of scope for wiring-level smoke tests)", | ||
| "private": true, | ||
| "main": "index.js" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "name": "rspack-compat-test", | ||
| "version": "0.0.1", | ||
| "description": "Built-dist smoke tests for @callstack/repack against both @rspack/core majors", | ||
| "private": true, | ||
| "type": "module", | ||
| "engines": { | ||
| "node": ">=22" | ||
| }, | ||
| "scripts": { | ||
| "test:smoke": "node run.mjs" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| // Drives the dual-major smoke fixtures: builds @callstack/repack, packs it | ||
| // into a tarball, installs that tarball into each standalone fixture and | ||
| // runs smoke.cjs against the result. | ||
| // | ||
| // Usage: node run.mjs [fixture...] (default: all fixtures) | ||
| import { execFileSync } from 'node:child_process'; | ||
| import fs from 'node:fs'; | ||
| import path from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| const suiteDir = path.dirname(fileURLToPath(import.meta.url)); | ||
| const repoRoot = path.resolve(suiteDir, '../..'); | ||
| const repackDir = path.join(repoRoot, 'packages/repack'); | ||
| const packDir = path.join(suiteDir, '.pack'); | ||
|
|
||
| const allFixtures = ['rspack-1', 'rspack-2']; | ||
| const requested = process.argv.slice(2); | ||
| const fixtures = requested.length > 0 ? requested : allFixtures; | ||
|
|
||
| for (const fixture of fixtures) { | ||
| if (!allFixtures.includes(fixture)) { | ||
| console.error( | ||
| `Unknown fixture '${fixture}' - expected one of: ${allFixtures.join(', ')}` | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| function run(command, args, options = {}) { | ||
| console.log(`\n$ ${command} ${args.join(' ')}`); | ||
| execFileSync(command, args, { stdio: 'inherit', ...options }); | ||
| } | ||
|
|
||
| // The lane guard in smoke.cjs verifies the *installed* major against the | ||
| // fixture's declared intent - derive that intent from the fixture manifest | ||
| // so the two can never drift apart silently. | ||
| function expectedMajorOf(fixtureDir) { | ||
| const manifest = JSON.parse( | ||
| fs.readFileSync(path.join(fixtureDir, 'package.json'), 'utf8') | ||
| ); | ||
| const range = manifest.devDependencies['@rspack/core']; | ||
| const major = Number.parseInt(range.replace(/^\D*/, ''), 10); | ||
| if (!Number.isInteger(major)) { | ||
| throw new Error( | ||
| `Cannot derive the expected @rspack/core major from range '${range}'` | ||
| ); | ||
| } | ||
| return major; | ||
| } | ||
|
|
||
| // 1. Build and pack repack. The suite exists to test the *built dist* - the | ||
| // failure modes it covers (require(esm) interop, loader resolution, refresh | ||
| // runtime source selection) don't reproduce through the Jest/Babel pipeline, | ||
| // so it must never run against stale sources. Built through turbo so that | ||
| // workspace dependencies (repack-dev-server) are built first. | ||
| run('pnpm', ['turbo', 'run', 'build', '--filter', '@callstack/repack'], { | ||
| cwd: repoRoot, | ||
| }); | ||
| fs.rmSync(packDir, { recursive: true, force: true }); | ||
| fs.mkdirSync(packDir, { recursive: true }); | ||
| run('pnpm', ['pack', '--pack-destination', packDir], { cwd: repackDir }); | ||
|
|
||
| const tarball = fs.readdirSync(packDir).find((file) => file.endsWith('.tgz')); | ||
| if (!tarball) { | ||
| throw new Error('pnpm pack produced no tarball'); | ||
| } | ||
|
|
||
| // 2. Install the tarball into each fixture and run the assertions. | ||
| const failed = []; | ||
|
|
||
| for (const fixture of fixtures) { | ||
| const fixtureDir = path.join(suiteDir, 'fixtures', fixture); | ||
| console.log(`\n=== fixture: ${fixture} ===`); | ||
|
|
||
| fs.copyFileSync( | ||
| path.join(packDir, tarball), | ||
| path.join(fixtureDir, 'callstack-repack.tgz') | ||
| ); | ||
|
|
||
| try { | ||
| run('pnpm', ['install', '--no-frozen-lockfile'], { cwd: fixtureDir }); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. codex review: This runner shells out to the first |
||
| run('node', [path.join(suiteDir, 'smoke.cjs')], { | ||
| cwd: fixtureDir, | ||
| env: { | ||
| ...process.env, | ||
| RSPACK_COMPAT_EXPECTED_MAJOR: String(expectedMajorOf(fixtureDir)), | ||
| }, | ||
| }); | ||
| console.log(`=== fixture ${fixture}: PASS ===`); | ||
| } catch { | ||
| failed.push(fixture); | ||
| console.error(`=== fixture ${fixture}: FAIL ===`); | ||
| } | ||
| } | ||
|
|
||
| if (failed.length > 0) { | ||
| console.error(`\nSmoke suite failed for: ${failed.join(', ')}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| console.log(`\nSmoke suite passed for: ${fixtures.join(', ')}`); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
codex review: These standalone fixture workspaces do not carry over the repo’s
autoInstallPeers: falsepolicy, so generated fixture installs default toautoInstallPeers: true. That makes the smoke less strict than this repo’s package-manager policy and can mask accidental imports of optional peers: in my run, Re.Pack resolved auto-installed peers likewebpack/@babel/corefrom the fixture virtual store even though the fixture manifests do not declare them. The current smoke still passes if I reinstall with--config.auto-install-peers=false, so please set that policy in both fixturepnpm-workspace.yamlfiles or pass it during install to keep future regressions from being hidden.