Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions .changeset/rspack-2-react-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@callstack/repack": patch
---

Restructure React Refresh support to work under both Rspack 1 and Rspack 2.

- Under Rspack 2, Re.Pack applies the official `@rspack/plugin-react-refresh` v2 plugin. The plugin is now an **optional peer dependency** (`^2.0.0`) instead of a regular dependency - Rspack 2 users must install it (along with `react-refresh`) in their project.
- Under Rspack 1 and webpack, nothing changes: the React Refresh client runtime files are now bundled with Re.Pack (vendored from `@rspack/plugin-react-refresh@2.0.2`, MIT), so the `@rspack/plugin-react-refresh@1` dependency is no longer needed and has been removed.
13 changes: 11 additions & 2 deletions biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"bracketSpacing": true,
"ignore": [
"pnpm-lock.yaml",
"packages/repack/vendor/**",
"tests/metro-compat/**/__tests__/**",
"website/src/2.x/**",
"website/src/3.x/**",
Expand All @@ -27,7 +28,11 @@
},
"organizeImports": {
"enabled": true,
"ignore": ["templates/*", "tests/metro-compat/**/__tests__/**"]
"ignore": [
"templates/*",
"packages/repack/vendor/**",
"tests/metro-compat/**/__tests__/**"
]
},
"linter": {
"enabled": true,
Expand Down Expand Up @@ -55,7 +60,11 @@
"noConfusingVoidType": "off"
}
},
"ignore": ["templates/*", "tests/metro-compat/**/__tests__/**"]
"ignore": [
"templates/*",
"packages/repack/vendor/**",
"tests/metro-compat/**/__tests__/**"
]
},
"javascript": {
"formatter": {
Expand Down
7 changes: 6 additions & 1 deletion packages/repack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"client",
"commands",
"mf",
"vendor",
"callstack-repack.podspec",
"src/modules/ScriptManager/NativeScriptManager.ts"
],
Expand Down Expand Up @@ -66,6 +67,7 @@
"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.

"react-native": ">=0.74",
"webpack": ">=5.90"
},
Expand All @@ -76,14 +78,16 @@
"@rspack/core": {
"optional": true
},
"@rspack/plugin-react-refresh": {
"optional": true
},
"webpack": {
"optional": true
}
},
"dependencies": {
"@callstack/repack-dev-server": "workspace:*",
"@discoveryjs/json-ext": "^0.5.7",
"@rspack/plugin-react-refresh": "1.0.0",
"babel-loader": "^9.2.1",
"colorette": "^2.0.20",
"dedent": "^0.7.0",
Expand Down Expand Up @@ -116,6 +120,7 @@
"@module-federation/sdk": "0.6.10",
"@rspack/core": "^2.1.2",
"@rspack/core-v1": "npm:@rspack/core@^1.7.12",
"@rspack/plugin-react-refresh": "^2.0.2",
"@swc/helpers": "^0.5.23",
"@types/babel__core": "^7.20.5",
"@types/dedent": "^0.7.0",
Expand Down
166 changes: 135 additions & 31 deletions packages/repack/src/plugins/DevelopmentPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@ import type {
Plugins,
Compiler as RspackCompiler,
} from '@rspack/core';
import ReactRefreshPlugin from '@rspack/plugin-react-refresh';
import type { Compiler as WebpackCompiler } from 'webpack';
import { isRspackCompiler, moveElementBefore } from '../helpers/index.js';

const [reactRefreshEntryPath, reactRefreshPath, refreshUtilsPath] =
ReactRefreshPlugin.deprecated_runtimePaths;
import {
getRspackMajorVersionFromCompiler,
isRspackCompiler,
moveElementBefore,
} from '../helpers/index.js';

type PackageJSON = { version: string };

// matches the file conditions of Re.Pack's JS transform rules
// (includes .flow files, unlike the react-refresh plugin defaults)
const REACT_REFRESH_LOADER_TEST = /\.([cm]js|[jt]sx?|flow)$/i;
const REACT_REFRESH_LOADER_EXCLUDE = /node_modules/i;

/**
* {@link DevelopmentPlugin} configuration options.
*/
Expand Down Expand Up @@ -87,6 +93,120 @@ export class DevelopmentPlugin {
return 'http';
}

private resolveReactRefreshPluginRequest(context: string, request: string) {
try {
return require.resolve(request, { paths: [context] });
} catch {
try {
// fallback to Re.Pack's own resolution paths
return require.resolve(request);
} catch {
const error = new Error(
'[RepackDevelopmentPlugin] ' +
"Dependency named '@rspack/plugin-react-refresh' (version 2.x) is required " +
'when using React Refresh with Rspack 2 but is not found in your project. ' +
'Did you forget to install it?'
);
// remove the stack trace to make the error more readable
error.stack = undefined;
throw error;
}
}
}

/**
* Sets up React Refresh using the official `@rspack/plugin-react-refresh` (v2)
* with its integrator options: entry injection is left to Re.Pack (to control
* placement per entrypoint) and the loader is swapped for Re.Pack's own
* React Native-aware react-refresh-loader.
*
* The plugin package is ESM-only, so it's loaded lazily inside this branch -
* it must never be evaluated on setups that don't support `require(esm)`
* (webpack or Rspack 1 users on Node 18).
*
* @returns Path to the react-refresh entry module.
*/
private setupOfficialReactRefresh(compiler: RspackCompiler): string {
const pluginPath = this.resolveReactRefreshPluginRequest(
compiler.context,
'@rspack/plugin-react-refresh'
);
// Rspack 2 requires Node >= 20.19 (enforced by Re.Pack commands),
// 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.

// entries are injected by Re.Pack per entrypoint to control their order
injectEntry: false,
// DevelopmentPlugin gates on devServer.hot already - don't let
// the plugin second-guess based on `mode`
forceEnable: true,
reactRefreshLoader: '@callstack/repack/react-refresh-loader',
test: REACT_REFRESH_LOADER_TEST,
exclude: REACT_REFRESH_LOADER_EXCLUDE,
}).apply(compiler);

return this.resolveReactRefreshPluginRequest(
compiler.context,
'@rspack/plugin-react-refresh/react-refresh-entry'
);
}

/**
* Sets up React Refresh manually using the client runtime files vendored
* from `@rspack/plugin-react-refresh` (package-root `vendor/` directory,
* shipped as-is) - used for webpack and Rspack 1 compilers, where the
* official v2 plugin cannot be applied.
*
* @returns Path to the react-refresh entry module.
*/
private setupManualReactRefresh(compiler: RspackCompiler): string {
// resolves from both src/plugins and dist/plugins to the package root
const reactRefreshPath = require.resolve(
'../../vendor/react-refresh/reactRefresh.js'
);
const refreshUtilsPath = require.resolve(
'../../vendor/react-refresh/refreshUtils.js'
);
const reactRefreshEntryPath = require.resolve(
'../../vendor/react-refresh/reactRefreshEntry.js'
);

new compiler.webpack.ProvidePlugin({
$ReactRefreshRuntime$: reactRefreshPath,
}).apply(compiler);

new compiler.webpack.DefinePlugin({
__react_refresh_library__: JSON.stringify(
compiler.webpack.Template.toIdentifier(
compiler.options.output.uniqueName || compiler.options.output.library
)
),
// full reload on unrecoverable runtime errors is a web-oriented
// behavior - in React Native errors are surfaced through LogBox
__reload_on_runtime_errors__: false,
}).apply(compiler);

new compiler.webpack.ProvidePlugin({
__react_refresh_utils__: refreshUtilsPath,
}).apply(compiler);

compiler.options.module.rules.unshift({
Comment thread
dannyhw marked this conversation as resolved.
test: REACT_REFRESH_LOADER_TEST,
// like upstream, exclude the refresh runtime files themselves - they
// live outside of node_modules in workspace (symlinked) installs
exclude: [
REACT_REFRESH_LOADER_EXCLUDE,
reactRefreshPath,
refreshUtilsPath,
reactRefreshEntryPath,
],
use: '@callstack/repack/react-refresh-loader',
});

return reactRefreshEntryPath;
}

apply(compiler: RspackCompiler): void;
apply(compiler: WebpackCompiler): void;

Expand Down Expand Up @@ -121,38 +241,22 @@ export class DevelopmentPlugin {
// setup HMR
new compiler.webpack.HotModuleReplacementPlugin().apply(compiler);

// setup React Refresh manually instead of using the official plugin
// to avoid issues with placement of reactRefreshEntry
new compiler.webpack.ProvidePlugin({
$ReactRefreshRuntime$: reactRefreshPath,
}).apply(compiler);

new compiler.webpack.DefinePlugin({
__react_refresh_error_overlay__: false,
__react_refresh_socket__: false,
__react_refresh_library__: JSON.stringify(
compiler.webpack.Template.toIdentifier(
compiler.options.output.uniqueName ||
compiler.options.output.library
)
),
}).apply(compiler);

new compiler.webpack.ProvidePlugin({
__react_refresh_utils__: refreshUtilsPath,
}).apply(compiler);

// alias react-refresh to Re.Pack's own copy to keep a single,
// version-controlled instance of the refresh runtime
const refreshPath = path.dirname(require.resolve('react-refresh'));
compiler.options.resolve.alias = {
'react-refresh': refreshPath,
...compiler.options.resolve.alias,
};

compiler.options.module.rules.unshift({
include: /\.([cm]js|[jt]sx?|flow)$/i,
exclude: /node_modules/i,
use: '@callstack/repack/react-refresh-loader',
});
// Rspack 2 gets the official react-refresh plugin (driven through
// its integrator options), webpack & Rspack 1 get manual wiring
// based on the vendored client runtime files
const rspackMajor = getRspackMajorVersionFromCompiler(compiler);
const reactRefreshEntryPath =
rspackMajor !== null && rspackMajor >= 2
? this.setupOfficialReactRefresh(compiler)
: this.setupManualReactRefresh(compiler);

const devEntries = [
reactRefreshEntryPath,
Expand Down
63 changes: 63 additions & 0 deletions packages/repack/vendor/react-refresh/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
These files are vendored from @rspack/plugin-react-refresh@2.0.2
(https://github.com/rspack-contrib/rspack-plugin-react-refresh, the `client/`
runtime files), which is itself based on @pmmmwh/react-refresh-webpack-plugin
(https://github.com/pmmmwh/react-refresh-webpack-plugin).

They are used by the manual React Refresh wiring in Re.Pack's
DevelopmentPlugin for webpack and Rspack 1 compilers - under Rspack 2 the
official @rspack/plugin-react-refresh plugin provides these files instead.

Local changes relative to upstream v2.0.2 (re-derive on future bumps):
- provenance headers added to each file
- formatting aligned with this repository (no functional edits)
- note: upstream's `__reload_on_runtime_errors__` global is defined as
`false` by DevelopmentPlugin (full reload on unrecoverable runtime errors
is a web-oriented behavior; React Native surfaces errors through LogBox)

--------------------------------------------------------------------------

MIT License

Copyright (c) 2022-present Bytedance, Inc. and its affiliates.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

--------------------------------------------------------------------------

MIT License (@pmmmwh/react-refresh-webpack-plugin)

Copyright (c) 2019 Michael Mok

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
36 changes: 36 additions & 0 deletions packages/repack/vendor/react-refresh/reactRefresh.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Vendored from @rspack/plugin-react-refresh@2.0.2 (client runtime files),
* which is itself based on @pmmmwh/react-refresh-webpack-plugin.
*
* Used by the manual React Refresh wiring in DevelopmentPlugin for
* webpack and Rspack 1 compilers - under Rspack 2 the official
* @rspack/plugin-react-refresh plugin provides these files instead.
*
* MIT Licensed
* Copyright (c) 2019 Michael Mok (react-refresh-webpack-plugin)
* Copyright (c) 2022-present Bytedance, Inc. and its affiliates (rspack-plugin-react-refresh)
*/

import {
createSignatureFunctionForTransform,
register,
} from 'react-refresh/runtime';
import { executeRuntime, getModuleExports } from './refreshUtils.js';

function refresh(moduleId, hot) {
const currentExports = getModuleExports(moduleId);
const runRefresh = (moduleExports) => {
const testMode =
typeof __react_refresh_test__ !== 'undefined'
? __react_refresh_test__
: undefined;
executeRuntime(moduleExports, moduleId, hot, testMode);
};
if (typeof Promise !== 'undefined' && currentExports instanceof Promise) {
currentExports.then(runRefresh);
} else {
runRefresh(currentExports);
}
}

export { createSignatureFunctionForTransform, refresh, register };
Loading
Loading