-
Notifications
You must be signed in to change notification settings - Fork 159
feat: restructure react refresh support for rspack 2 #1402
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-types-test-infra
Are you sure you want to change the base?
Changes from 1 commit
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,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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
| */ | ||
|
|
@@ -87,6 +93,113 @@ 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({ | ||
|
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: 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({ | ||
|
dannyhw marked this conversation as resolved.
|
||
| test: REACT_REFRESH_LOADER_TEST, | ||
| exclude: REACT_REFRESH_LOADER_EXCLUDE, | ||
| use: '@callstack/repack/react-refresh-loader', | ||
| }); | ||
|
|
||
| return reactRefreshEntryPath; | ||
| } | ||
|
|
||
| apply(compiler: RspackCompiler): void; | ||
| apply(compiler: WebpackCompiler): void; | ||
|
|
||
|
|
@@ -121,38 +234,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, | ||
|
|
||
| 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. |
| 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 }; |
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: 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.