Skip to content

feat: restructure react refresh support for rspack 2#1402

Open
dannyhw wants to merge 2 commits into
feat/rspack-2-types-test-infrafrom
feat/rspack-2-react-refresh
Open

feat: restructure react refresh support for rspack 2#1402
dannyhw wants to merge 2 commits into
feat/rspack-2-types-test-infrafrom
feat/rspack-2-react-refresh

Conversation

@dannyhw

@dannyhw dannyhw commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Part of the Rspack 2 support stack (reference implementation: #1393, design doc: agent_context/rspackv2-jul2026/design.md).

Restructures React Refresh so DevelopmentPlugin works under both Rspack majors and webpack. This is the only PR in the stack that touches packages/repack/package.json's dependency surface.

Design: split on the rspack major

@rspack/plugin-react-refresh@1 cannot stay: its module-load-time deprecated_runtimePaths access hard-crashes when v2 of the plugin is installed, and depending on v1 while needing ^2 as a peer is unresolvable (same package name). Per the design doc's React Refresh decision (maintainer-validated in the #1393 review — vendoring the client files is fine, deprecated_runtimePaths was only ever a convenience):

  • Rspack >= 2: apply the official @rspack/plugin-react-refresh v2 with its integrator options — injectEntry: false (Re.Pack controls entry placement per entrypoint), forceEnable: true (DevelopmentPlugin already gates on devServer.hot), and reactRefreshLoader pointed at Re.Pack's React Native-aware react-refresh-loader. The package is ESM-only (named export only), so it is require()d lazily inside the rspack-2 branch — it is never evaluated for webpack/Rspack 1 users — and resolved from the project context with an actionable error when missing.
  • webpack + Rspack 1: keep the existing manual wiring, now pointed at client runtime files vendored from @rspack/plugin-react-refresh@2.0.2 (MIT) instead of the removed v1 dependency.

The behavioral fork is a single ternary on getRspackMajorVersionFromCompiler(compiler); everything else is extraction. A per-bundler plugin split (floated in the #1393 review) is not warranted for one ~10-line fork.

Vendor directory placement (review feedback)

The vendored files live at package-root packages/repack/vendor/react-refresh/ — outside src/, per the review ask. They are plain JS client-runtime modules bundled by the user's compiler, so they ship as-is via package.json#files (no babel processing) and are excluded from biome. LICENSE in the directory carries the upstream MIT license plus a provenance note: the files are verbatim upstream v2.0.2 apart from provenance headers and repo formatting (no functional edits; re-derivation instructions for future bumps included).

Footprint check (review feedback: minimize the DevelopmentPlugin diff)

Verified with git diff --color-moved=dimmed-zebra --color-moved-ws=allow-indentation-change origin/main...HEAD -- packages/repack/src/plugins/DevelopmentPlugin.ts: the manual wiring (setupManualReactRefresh) registers as a move of the old inline block (193 moved lines). The only non-moved lines inside it are the three intentional deltas:

  1. Runtime-file source: ReactRefreshPlugin.deprecated_runtimePaths destructure → require.resolve('../../vendor/react-refresh/*.js') (resolves identically from src/plugins and dist/plugins).
  2. Define-flag swap: __react_refresh_error_overlay__: false + __react_refresh_socket__: false__reload_on_runtime_errors__: false — the v2.0.2 client files dropped the overlay/socket knobs and read the new flag instead. Value semantics unchanged: all web-oriented behaviors stay off; RN surfaces errors via LogBox.
  3. Loader rule matcher: include:test: with the same regex (both match the resource path here; test is the semantically correct key).

Two non-substantive accompaniments, listed for completeness rather than hidden: the loader-rule regexes are hoisted into module constants (REACT_REFRESH_LOADER_TEST / REACT_REFRESH_LOADER_EXCLUDE, identical literal values) because both branches now share them, and comments were updated around the moved block. Everything else in the file diff is genuinely new code for the rspack-2 branch (resolveReactRefreshPluginRequest, setupOfficialReactRefresh).

Dependency changes

Package Before After Notes
@rspack/plugin-react-refresh dependency 1.0.0 optional peerDependency ^2.0.0 Rspack 2 users install it in their project; webpack/Rspack 1 users need nothing
@rspack/plugin-react-refresh (devDep) ^2.0.2 so the workspace/tests exercise the official-plugin path
package.json#files + "vendor" ships the vendored client runtime

What users must do:

  • Rspack 2: install @rspack/plugin-react-refresh@^2 and react-refresh (pnpm-strict layouts don't hoist Re.Pack's react-refresh copy into the official plugin's resolution scope).
  • Rspack 1 / webpack: nothing. Runtime behavior is preserved via the vendored upstream v2.0.2 client files (MIT, LICENSE + provenance included) — same wiring shape (ProvidePlugin ×2 + DefinePlugin + loader rule + entry), same values modulo the enumerated define-flag rename that the v2.0.2 client files require.

Stack position

Parallel set, based on #1398 (types + dual-major test infra) ← #1397 (Node guard) ← #1394 (version helpers). No shared files with the other parallel-set PRs (#1399, #1400, #1401) apart from uniquely-named changesets; this is the only one touching packages/repack/package.json and the lockfile.

Validation

  • pnpm turbo run typecheck test --force: 17/17 tasks green (includes tester-app bundle/start tests, which exercise the dev server under both the rspack and webpack lanes).
  • packages/repack: pnpm test (v2 lane) and pnpm test:rspack1 (v1 lane) — 282/282 each.
  • pnpm lint:ci: clean (vendored files excluded as intended).
  • Webpack-path evidence (review feedback: test both bundler paths): started tester-app with USE_WEBPACK=1 and fetched the served dev bundle — it contains all three vendored runtime modules (vendor/react-refresh/reactRefresh.js ×90 refs, reactRefreshEntry.js ×3, refreshUtils.js ×3) and zero references to @rspack/plugin-react-refresh.
  • Vendored files diffed against the actual @rspack/plugin-react-refresh@2.0.2 client/ files from the registry: identical apart from provenance headers and formatting (no functional deltas).
  • On the reference branch, this exact code was device-validated (Pixel 7 + iPhone 17 simulator): live React Refresh edit+revert under Rspack 2 (official plugin) and Rspack 1 (vendored runtime).

Pending manual verification: a device HMR pass on this branch under Rspack 1, Rspack 2, and webpack. The reference branch (#1393) passed the Rspack 1 + Rspack 2 device HMR checks with identical code, but this branch has not yet had its own on-device pass — flagging so it can happen before/at merge.

DevelopmentPlugin now splits on the detected rspack major:

- rspack >= 2 applies the official @rspack/plugin-react-refresh (v2) with
  injectEntry: false (Re.Pack controls entry placement per entrypoint),
  forceEnable: true, and Re.Pack's own react-refresh-loader. The package
  is ESM-only, so it is loaded lazily inside the rspack-2 branch and
  resolved from the project context with an actionable error when missing.
- webpack and rspack 1 keep the existing manual wiring, now pointed at
  client runtime files vendored from @rspack/plugin-react-refresh@2.0.2
  (MIT) into the package-root vendor/react-refresh/ directory, shipped
  as-is via package.json#files and excluded from biome.

The @rspack/plugin-react-refresh@1.0.0 dependency is removed (its
module-load-time deprecated_runtimePaths access crashes under the v2
plugin); the v2 plugin becomes an optional peer dependency instead, so
rspack 2 users install it (plus react-refresh) in their project.
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
repack-website Ready Ready Preview, Comment Jul 6, 2026 7:23pm

Request Review

@changeset-bot

changeset-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 05b9027

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@callstack/repack Patch
@callstack/repack-plugin-expo-modules Patch
@callstack/repack-plugin-nativewind Patch
@callstack/repack-plugin-reanimated Patch
@callstack/repack-dev-server Patch
@callstack/repack-init Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment thread packages/repack/src/plugins/DevelopmentPlugin.ts
…oader rule

The manual React Refresh rule only excluded node_modules, so in
workspace (symlinked) installs the vendored runtime files under
packages/repack/vendor/react-refresh matched the rule and were
processed by the react-refresh loader themselves. Mirror the upstream
plugin's self-exclusion by also excluding the three vendored file
paths. The official plugin path (Rspack 2) already appends these
exclusions internally.

@dannyhw dannyhw left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

codex review: Posting the actionable findings from the latest re-review.

// where require() of an ESM module is supported
const { ReactRefreshRspackPlugin } = require(pluginPath);

new ReactRefreshRspackPlugin({

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

codex review: Correction to the previous comment: the stripped identifiers were ReactRefreshRspackPlugin and @rspack/plugin-react-refresh. The requested coverage should prove that the Rspack 2 branch applies ReactRefreshRspackPlugin with Re.Pack's loader and injected entry path; webpack/Rspack 1 use the vendored runtime without evaluating @rspack/plugin-react-refresh; and Rspack 2 reports the intended actionable error when that optional peer is absent.

"peerDependencies": {
"@module-federation/enhanced": ">=0.6.10",
"@rspack/core": ">=1",
"@rspack/plugin-react-refresh": "^2.0.0",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

codex review: Correction to the previous comment: the stripped package name was @rspack/plugin-react-refresh. This PR makes @rspack/plugin-react-refresh an optional peer, but the current latest docs still say the plugin is shipped with Re.Pack and configured automatically. Please update the user-facing docs/install guidance in this PR or in the same stack so the package contract and docs agree.

// where require() of an ESM module is supported
const { ReactRefreshRspackPlugin } = require(pluginPath);

new ReactRefreshRspackPlugin({

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

codex review: Correction to the previous comment: the stripped identifiers were ReactRefreshRspackPlugin and @rspack/plugin-react-refresh. The requested coverage should prove that the Rspack 2 branch applies ReactRefreshRspackPlugin with Re.Pack's loader and injected entry path; webpack/Rspack 1 use the vendored runtime without evaluating @rspack/plugin-react-refresh; and Rspack 2 reports the intended actionable error when that optional peer is absent.

"peerDependencies": {
"@module-federation/enhanced": ">=0.6.10",
"@rspack/core": ">=1",
"@rspack/plugin-react-refresh": "^2.0.0",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

codex review: Correction to the previous comment: the stripped package name was @rspack/plugin-react-refresh. This PR makes @rspack/plugin-react-refresh an optional peer, but the current latest docs still say the plugin is shipped with Re.Pack and configured automatically. Please update the user-facing docs/install guidance in this PR or in the same stack so the package contract and docs agree.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant