From 2fa598f793732210f608f2e73177bfc8f6f76b15 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:01:37 +0800 Subject: [PATCH] feat: runtime entry option --- docs/API.md | 12 ++++++++++ lib/options.json | 3 +++ lib/types.js | 3 ++- lib/utils/getAdditionalEntries.js | 4 ++-- lib/utils/normalizeOptions.js | 1 + test/unit/getAdditionalEntries.test.js | 21 ++++++++++++++-- test/unit/normalizeOptions.test.js | 12 ++++++++++ test/unit/validateOptions.test.js | 33 +++++++++++++++++++++++++- types/lib/types.d.ts | 6 ++++- 9 files changed, 88 insertions(+), 7 deletions(-) diff --git a/docs/API.md b/docs/API.md index 1618a33a..e1e1fdd5 100644 --- a/docs/API.md +++ b/docs/API.md @@ -29,6 +29,7 @@ interface ReactRefreshPluginOptions { exclude?: string | RegExp | Array; include?: string | RegExp | Array; library?: string; + runtimeEntry?: string | false; esModule?: boolean | ESModuleOptions; overlay?: boolean | ErrorOverlayOptions; } @@ -78,6 +79,17 @@ This is similar to the `output.uniqueName` in Webpack 5 or the `output.library` It is most useful when multiple instances of React Refresh is running together simultaneously. +#### `runtimeEntry` + +Type: `string | false` + +Default: `'@pmmmwh/react-refresh-webpack-plugin/client/ReactRefreshEntry'` + +The **PATH** to a file/module that sets up the React Refresh runtime integration. + +This entry is prepended to Webpack entries by default. When set to `false`, no runtime entry will be injected automatically. +This is useful when the runtime needs to be registered manually, or when a target does not need it, such as a service worker entry. + #### `esModule` Type: `boolean | ESModuleOptions` diff --git a/lib/options.json b/lib/options.json index 0cfa5185..a6ef45d7 100644 --- a/lib/options.json +++ b/lib/options.json @@ -67,6 +67,9 @@ ] }, "library": { "type": "string" }, + "runtimeEntry": { + "anyOf": [{ "const": false }, { "$ref": "#/definitions/Path" }] + }, "overlay": { "anyOf": [{ "type": "boolean" }, { "$ref": "#/definitions/OverlayOptions" }] } diff --git a/lib/types.js b/lib/types.js index dcf4cf44..96803044 100644 --- a/lib/types.js +++ b/lib/types.js @@ -17,6 +17,7 @@ * @property {string | RegExp | Array} [include] Files to explicitly include for processing. * @property {string} [library] Name of the library bundle. * @property {boolean | ErrorOverlayOptions} [overlay] Modifies how the error overlay integration works in the plugin. + * @property {string | false} [runtimeEntry] Runtime entry to prepend to Webpack entries, or false to disable automatic injection. */ /** @@ -25,7 +26,7 @@ */ /** - * @typedef {import('type-fest').SetRequired, 'exclude' | 'include'> & OverlayOverrides} NormalizedPluginOptions + * @typedef {import('type-fest').SetRequired, 'exclude' | 'include' | 'runtimeEntry'> & OverlayOverrides} NormalizedPluginOptions */ module.exports = {}; diff --git a/lib/utils/getAdditionalEntries.js b/lib/utils/getAdditionalEntries.js index 109ad33a..345ba4a4 100644 --- a/lib/utils/getAdditionalEntries.js +++ b/lib/utils/getAdditionalEntries.js @@ -12,8 +12,8 @@ function getAdditionalEntries(options) { const prependEntries = [ // React-refresh runtime - require.resolve('../../client/ReactRefreshEntry'), - ]; + options.runtimeEntry && require.resolve(options.runtimeEntry), + ].filter(Boolean); const overlayEntries = [ // Error overlay runtime diff --git a/lib/utils/normalizeOptions.js b/lib/utils/normalizeOptions.js index 0130b58c..abe95cb5 100644 --- a/lib/utils/normalizeOptions.js +++ b/lib/utils/normalizeOptions.js @@ -10,6 +10,7 @@ const normalizeOptions = (options) => { d(options, 'include', /\.([cm]js|[jt]sx?|flow)$/i); d(options, 'forceEnable'); d(options, 'library'); + d(options, 'runtimeEntry', require.resolve('../../client/ReactRefreshEntry')); n(options, 'overlay', (overlay) => { /** @type {import('../types').NormalizedErrorOverlayOptions} */ diff --git a/test/unit/getAdditionalEntries.test.js b/test/unit/getAdditionalEntries.test.js index e361aa9f..53d2f123 100644 --- a/test/unit/getAdditionalEntries.test.js +++ b/test/unit/getAdditionalEntries.test.js @@ -5,16 +5,33 @@ const ReactRefreshEntry = require.resolve('../../client/ReactRefreshEntry'); describe('getAdditionalEntries', () => { it('should work with default settings', () => { - expect(getAdditionalEntries({ overlay: { entry: ErrorOverlayEntry } })).toStrictEqual({ + expect( + getAdditionalEntries({ + overlay: { entry: ErrorOverlayEntry }, + runtimeEntry: ReactRefreshEntry, + }) + ).toStrictEqual({ overlayEntries: [ErrorOverlayEntry], prependEntries: [ReactRefreshEntry], }); }); it('should skip overlay entries when overlay is false in options', () => { - expect(getAdditionalEntries({ overlay: false })).toStrictEqual({ + expect( + getAdditionalEntries({ + overlay: false, + runtimeEntry: ReactRefreshEntry, + }) + ).toStrictEqual({ overlayEntries: [], prependEntries: [ReactRefreshEntry], }); }); + + it('should skip prepend entries when runtimeEntry is false in options', () => { + expect(getAdditionalEntries({ overlay: false, runtimeEntry: false })).toStrictEqual({ + overlayEntries: [], + prependEntries: [], + }); + }); }); diff --git a/test/unit/normalizeOptions.test.js b/test/unit/normalizeOptions.test.js index 756997cd..67fafff1 100644 --- a/test/unit/normalizeOptions.test.js +++ b/test/unit/normalizeOptions.test.js @@ -1,9 +1,12 @@ const normalizeOptions = require('../../lib/utils/normalizeOptions'); +const ReactRefreshEntry = require.resolve('../../client/ReactRefreshEntry'); + /** @type {Partial} */ const DEFAULT_OPTIONS = { exclude: /node_modules/i, include: /\.([cm]js|[jt]sx?|flow)$/i, + runtimeEntry: ReactRefreshEntry, overlay: { entry: require.resolve('../../client/ErrorOverlayEntry'), module: require.resolve('../../overlay'), @@ -23,6 +26,7 @@ describe('normalizeOptions', () => { forceEnable: true, include: 'include', library: 'library', + runtimeEntry: 'runtimeEntry', overlay: { entry: 'entry', module: 'overlay', @@ -34,6 +38,7 @@ describe('normalizeOptions', () => { forceEnable: true, include: 'include', library: 'library', + runtimeEntry: 'runtimeEntry', overlay: { entry: 'entry', module: 'overlay', @@ -53,6 +58,13 @@ describe('normalizeOptions', () => { }); }); + it('should keep "runtimeEntry" when it is false', () => { + expect(normalizeOptions({ runtimeEntry: false })).toStrictEqual({ + ...DEFAULT_OPTIONS, + runtimeEntry: false, + }); + }); + it('should keep "overlay.entry" when it is false', () => { const options = { ...DEFAULT_OPTIONS }; options.overlay.entry = false; diff --git a/test/unit/validateOptions.test.js b/test/unit/validateOptions.test.js index fb0b02f1..7ae6f929 100644 --- a/test/unit/validateOptions.test.js +++ b/test/unit/validateOptions.test.js @@ -121,6 +121,37 @@ describe('validateOptions', () => { `); }); + it('should accept "runtimeEntry" when it is an absolute path string', () => { + expect(() => { + new ReactRefreshPlugin({ runtimeEntry: '/test' }); + }).not.toThrow(); + }); + + it('should accept "runtimeEntry" when it is a string', () => { + expect(() => { + new ReactRefreshPlugin({ runtimeEntry: 'test' }); + }).not.toThrow(); + }); + + it('should accept "runtimeEntry" when it is false', () => { + expect(() => { + new ReactRefreshPlugin({ runtimeEntry: false }); + }).not.toThrow(); + }); + + it('should reject "runtimeEntry" when it is not a string nor false', () => { + expect(() => { + new ReactRefreshPlugin({ runtimeEntry: true }); + }).toThrowErrorMatchingInlineSnapshot(` +"Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.runtimeEntry should be one of these: + false | string + Details: + * options.runtimeEntry should be equal to constant false. + * options.runtimeEntry should be a string." +`); + }); + it('should accept "overlay" when it is true', () => { expect(() => { new ReactRefreshPlugin({ overlay: true }); @@ -311,7 +342,7 @@ describe('validateOptions', () => { }).toThrowErrorMatchingInlineSnapshot(` "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { esModule?, exclude?, forceEnable?, include?, library?, overlay? }" + object { esModule?, exclude?, forceEnable?, include?, library?, runtimeEntry?, overlay? }" `); }); }); diff --git a/types/lib/types.d.ts b/types/lib/types.d.ts index 2ab2e6a7..0c261998 100644 --- a/types/lib/types.d.ts +++ b/types/lib/types.d.ts @@ -43,6 +43,10 @@ export type ReactRefreshPluginOptions = { * Modifies how the error overlay integration works in the plugin. */ overlay?: boolean | ErrorOverlayOptions | undefined; + /** + * Runtime entry to prepend to Webpack entries, or false to disable automatic injection. + */ + runtimeEntry?: string | false | undefined; }; export type OverlayOverrides = { /** @@ -52,6 +56,6 @@ export type OverlayOverrides = { }; export type NormalizedPluginOptions = import('type-fest').SetRequired< import('type-fest').Except, - 'exclude' | 'include' + 'exclude' | 'include' | 'runtimeEntry' > & OverlayOverrides;