|
| 1 | +# Implementation Notes |
| 2 | + |
| 3 | +What was actually built on `feat/rspack-2-support`, including the non-obvious |
| 4 | +technical details discovered during implementation that aren't captured in the |
| 5 | +research docs (01–07). Written so a fresh context can pick up the work without |
| 6 | +the original conversation. |
| 7 | + |
| 8 | +## Commits on this branch |
| 9 | + |
| 10 | +| Commit | Contents | |
| 11 | +| --- | --- | |
| 12 | +| `914d3053` | Research docs (01–05 + README) | |
| 13 | +| `8d741365` | React Refresh deep dive (doc 06) | |
| 14 | +| `7d8687f8` | Q1–Q5 decisions recorded | |
| 15 | +| `0b1a3a5a` | V1–V11 verification results (doc 07) | |
| 16 | +| `d0ea05d6` | **feat: support rspack 2 alongside rspack 1** — the core implementation | |
| 17 | +| `ba841a6a` | **refactor: replace type casts with proper typing** | |
| 18 | +| `f623076f` | docs moved to `agent_context/rspackv2-jul2026/` | |
| 19 | +| `2f50d03b` | agent_context lifecycle refinement | |
| 20 | + |
| 21 | +Draft PR for the whole branch: [#1393](https://github.com/callstack/repack/pull/1393) |
| 22 | +(to be closed in favor of the PR stack — see doc 09). |
| 23 | + |
| 24 | +## What was implemented (plan phases 0–2) |
| 25 | + |
| 26 | +1. **Version detection** — `packages/repack/src/helpers/rspackVersion.ts`: |
| 27 | + `getRspackVersion` / `getRspackMajorVersion` / `isRspack2` resolve |
| 28 | + `@rspack/core/package.json` instead of importing the package (safe on any |
| 29 | + Node version, safe when not installed); `getRspackMajorVersionFromCompiler` |
| 30 | + reads `compiler.webpack.rspackVersion` for plugin contexts. |
| 31 | + `profile/index.ts` refactored onto it. |
| 32 | +2. **Node guard + lazy commands** — `commands/rspack/ensureNodeCompat.ts` |
| 33 | + raises a clear `CLIError` for Rspack 2 on Node < 20.19 (instead of |
| 34 | + `ERR_REQUIRE_ESM`). `commands/rspack/index.ts` now lazy-imports |
| 35 | + `start`/`bundle` inside the command `func`s — the guard runs first, and |
| 36 | + loading `react-native.config.js` no longer touches `@rspack/core` at all. |
| 37 | +3. **Config generation** — `getRepackConfig`: `experiments.parallelLoader` |
| 38 | + only under v1; `module.parser.javascript.exportsPresence: 'auto'` under v2 |
| 39 | + (decision Q1). `checkParallelModeAvailable` in babelSwcLoader skips its |
| 40 | + probe under v2 (the global flag no longer exists; non-parallel is a valid |
| 41 | + choice there, not a misconfiguration). |
| 42 | +4. **Persistent cache** — `getRspackCacheConfig` reads both the v1 |
| 43 | + (`experiments.cache`) and v2 (top-level `cache`) locations; |
| 44 | + `migrateLegacyRspackCacheConfig` (called from `start`/`bundle` when |
| 45 | + `isRspack2`) moves a legacy value to `cache` with a one-time warning — |
| 46 | + because v2 *silently ignores* the legacy key (verified in doc 07). |
| 47 | +5. **React Refresh ** — per the doc 06 decision: `@rspack/[email protected]` |
| 48 | + dependency deleted; v2 plugin added as **optional peerDependency** (`^2.0.0`); |
| 49 | + `DevelopmentPlugin` splits on major — rspack≥2 applies the official plugin |
| 50 | + (`injectEntry: false`, `forceEnable: true`, |
| 51 | + `reactRefreshLoader: '@callstack/repack/react-refresh-loader'`, lazily |
| 52 | + `require`d inside the branch since the package is ESM-only), webpack + |
| 53 | + rspack 1 use the manual wiring pointed at client files **vendored** into |
| 54 | + `src/modules/reactRefresh/` (adapted from plugin v2.0.2, MIT; defines swap |
| 55 | + the removed overlay flags for `__reload_on_runtime_errors__: false`). |
| 56 | +6. **Tracing** — `profile-2.ts` defaults the trace layer to `'logger'` under |
| 57 | + v2 (published v2 binaries lack perfetto, verified V9). |
| 58 | +7. **MFv1 pre-check** — `ModuleFederationPluginV1.apply` verifies |
| 59 | + `@module-federation/runtime-tools` is resolvable under rspack≥2 with an |
| 60 | + actionable error (no longer auto-installed by `@rspack/core`). |
| 61 | +8. **Types** — `packages/repack` devDeps moved to `@rspack/core@^2.1.2` + |
| 62 | + `@swc/helpers@^0.5.23` (workspace catalog stays `^1.6.0` for everything |
| 63 | + else). `ConfigKeys` gained `cache` and `experiments` (it genuinely |
| 64 | + under-described what the commands read). |
| 65 | + |
| 66 | +## Non-obvious technical details (hard-won, do not rediscover) |
| 67 | + |
| 68 | +### Jest cannot load ESM-only @rspack/core — sandbox escape required |
| 69 | +Jest's CJS module runtime cannot `require(esm)`, and **`createRequire` inside |
| 70 | +the Jest sandbox is wrapped by Jest** — a bridge module calling |
| 71 | +`createRequire(__filename)('@rspack/core')` loops back through |
| 72 | +`moduleNameMapper` into itself (observed: the bridge received its own partial |
| 73 | +exports). The working escape: a custom test environment |
| 74 | +(`jest.environment.js`) — environments load *outside* the sandbox with Node's |
| 75 | +real module system — preloads the core via `await import()` and exposes it as |
| 76 | +`this.global.__RSPACK_CORE__`; `jest.rspack-core-bridge.js` (mapped via |
| 77 | +`moduleNameMapper`) reshapes it with `__esModule: true` so Babel's import |
| 78 | +interop keeps named imports working. Result: all suites run against the real |
| 79 | +Rspack 2 — 280 tests (previously 234; four suites weren't even loading). |
| 80 | + |
| 81 | +### require(esm) interop shape |
| 82 | +`require('@rspack/core')` under v2 returns **the callable `rspack` function |
| 83 | +itself** with all named exports attached (`core.rspack === core`) — Rspack |
| 84 | +uses Node's `module.exports` ESM-interop convention, so CJS consumers get a |
| 85 | +v1-identical shape. No `__esModule` marker, no `default` export. Babel-compiled |
| 86 | +named imports work unchanged. (`@rspack/plugin-react-refresh@2` does NOT do |
| 87 | +this — it has a **named export only**; a default import yields `undefined`.) |
| 88 | + |
| 89 | +### v2 type fallout patterns |
| 90 | +- `SwcLoaderOptions` became a **union discriminated on `detectSyntax`** — |
| 91 | + spread-and-override helpers can't reassemble a union. Fix: local non-union |
| 92 | + `SwcConfig` alias in `loaders/babelSwcLoader/options.ts` |
| 93 | + (`Omit<SwcLoaderOptions, 'jsc' | 'detectSyntax'> & { detectSyntax?: false; jsc?: SwcLoaderJscConfig }`). |
| 94 | +- The raw SWC `transformSync` options type doesn't accept |
| 95 | + `builtin:swc-loader`-only keys — `babelSwcLoader` now destructures |
| 96 | + `rspackExperiments`/`transformImport`/`collectTypeScriptInfo`/`detectSyntax` |
| 97 | + off before calling it (also more correct at runtime). `SwcOverrides` |
| 98 | + excludes them too. |
| 99 | +- Cache types are derived from source of truth: |
| 100 | + `RspackConfiguration['cache']` + `NonNullable<Configuration['experiments']> & { cache?: ... }` |
| 101 | + (the intersection also defeats TS's weak-type check that plagued |
| 102 | + structurally-typed attempts). |
| 103 | +- TS's **weak-type check** rejects all-optional parameter types when an |
| 104 | + argument's union members share no properties — this is why the cache |
| 105 | + migration call lives in `start`/`bundle` (concrete `Configuration[]`), not |
| 106 | + in `makeCompilerConfig` (inferred literal-union from `webpack-merge`). |
| 107 | + |
| 108 | +### Working agreement: no type casts |
| 109 | +Maintainer preference (Daniel, 2026-07-02): avoid `as X` and especially |
| 110 | +`as unknown as X`. Use `satisfies`, type narrowing, or fix the underlying |
| 111 | +mismatch; where genuinely impossible, keep the cast with the full reasoning |
| 112 | +in a comment and call it out in review. Current state: **one** irreducible |
| 113 | +cast remains, in `commands/rspack/start.ts` — Re.Pack's `devServer` type |
| 114 | +augmentation (`src/types/dev-server-options.d.ts`) vs Rspack 2's bundled |
| 115 | +`DevServer` type are incompatible solely because each pulls `proxy` types from |
| 116 | +a different copy of http-proxy-middleware; `devServer` can't be stripped there |
| 117 | +because the dev-server flow reads it back from `compiler.options`. The genuine |
| 118 | +fix would be aligning `@callstack/repack-dev-server`'s proxy types with |
| 119 | +rspack's bundled ones (possible future work). `bundle.ts` avoids the cast by |
| 120 | +rest-destructuring `devServer` off (it's not needed for bundling). |
| 121 | + |
| 122 | +### Misc |
| 123 | +- The `devServer` key is *accepted* by v2 at runtime (validation is loose — |
| 124 | + doc 07); the conflict is purely type-level. |
| 125 | +- Repack's `type: 'javascript/auto'` on transform rules is what makes its ESM |
| 126 | + `dist/modules/*` files bundle inside a `"type": "commonjs"` package scope — |
| 127 | + a bare config without such a rule parses them as CJS and fails. |
| 128 | +- Biome's `useOptionalChain` conflicts with TS narrowing on `false | DevServer`; |
| 129 | + `typeof x === 'object' && x.hot` satisfies both. |
| 130 | + |
| 131 | +## Verification performed |
| 132 | + |
| 133 | +- `pnpm typecheck` / `build` / `test` (280/280) / biome — clean; |
| 134 | + `turbo run build typecheck` green across all 12 workspace tasks. |
| 135 | +- **Smoke tests of the built dist** in isolated projects (Node 26), script |
| 136 | + kept in the session scratchpad (`smoke.cjs`, labs `v1-lab`/`v2-lab`): |
| 137 | + - Rspack **1.7.12**: parallelLoader kept, no parser override, cache |
| 138 | + accessor + migration, Node guard, full dev build with HMR + React Refresh |
| 139 | + via the **vendored files** — all PASS. |
| 140 | + - Rspack **2.1.2**: no parallelLoader, `exportsPresence: 'auto'`, cache |
| 141 | + migration, Node guard, full dev build with HMR + React Refresh via the |
| 142 | + **official v2 plugin** — all PASS. |
| 143 | + - Lab setup notes: react-native stubbed via `resolve.alias` (RN sources |
| 144 | + need the full flow/babel loader chain); loader resolved via |
| 145 | + `resolveLoader.alias`; rspack-2 users of the official plugin also need |
| 146 | + `react-refresh` installed (pnpm-strict layouts won't hoist repack's copy |
| 147 | + into the plugin's resolution scope). |
| 148 | +- NOT yet verified (phase 3): device HMR e2e, tester apps, metro-compat / |
| 149 | + resolver-cases suites under v2, CI matrix. |
0 commit comments