diff --git a/.eslintrc.json b/.eslintrc.json index e061df24..dbfebfc4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -26,7 +26,11 @@ } }, { - "files": ["test/sandbox/*.js", "test/**/*.test.js"], + "files": [ + "test/jest-test-setup.js", + "test/helpers/{,!(fixtures)*/}*.js", + "test/**/*.test.js" + ], "env": { "jest": true }, @@ -37,10 +41,21 @@ } }, { - "files": ["test/sandbox/runtime/*.js", "test/conformance/**/*.test.js"], + "files": ["test/helpers/**/fixtures/*.js", "test/conformance/**/*.test.js"], "env": { "browser": true } + }, + { + "files": ["test/**/fixtures/*.esm.js"], + "parserOptions": { + "ecmaVersion": 2015, + "sourceType": "module" + }, + "env": { + "commonjs": false, + "es6": true + } } ] } diff --git a/client/LegacyWDSSocketEntry.js b/client/LegacyWDSSocketEntry.js index 7137fe9e..e199b60a 100644 --- a/client/LegacyWDSSocketEntry.js +++ b/client/LegacyWDSSocketEntry.js @@ -20,7 +20,7 @@ SockJSClient.prototype.onClose = function onClose(fn) { /** * Creates a handler to handle socket message events. - * @param {function(data: *): void} fn + * @param {function(*): void} fn */ SockJSClient.prototype.onMessage = function onMessage(fn) { this.socket.onmessage = function onMessageHandler(event) { diff --git a/client/errorEventHandlers.js b/client/errorEventHandlers.js index 7bf05588..c85968b6 100644 --- a/client/errorEventHandlers.js +++ b/client/errorEventHandlers.js @@ -51,7 +51,7 @@ function createRejectionHandler(callback) { * Creates a handler that registers an EventListener on window for a valid type * and calls a callback when the event fires. * @param {string} eventType A valid DOM event type. - * @param {function(callback: EventCallback): EventHandler} createHandler A function that creates an event handler. + * @param {function(EventCallback): EventHandler} createHandler A function that creates an event handler. * @returns {register} A function that registers the EventListener given a callback. */ function createWindowEventHandler(eventType, createHandler) { diff --git a/jest.config.js b/jest.config.js index af805763..aaabc847 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,8 +1,8 @@ module.exports = { globalSetup: '/jest-global-setup.js', globalTeardown: '/jest-global-teardown.js', - modulePaths: [], rootDir: 'test', + setupFilesAfterEnv: ['/jest-test-setup.js'], testEnvironment: '/jest-environment.js', testMatch: ['/**/*.test.js'], testRunner: 'jest-circus/runner', diff --git a/lib/index.js b/lib/index.js index 2c31c431..c39feb31 100644 --- a/lib/index.js +++ b/lib/index.js @@ -87,6 +87,7 @@ class ReactRefreshPlugin { compiler.options.entry = injectRefreshEntry(compiler.options.entry, this.options); // Inject necessary modules to bundle's global scope + /** @type {Record} */ let providedModules = { __react_refresh_utils__: require.resolve('./runtime/RefreshUtils'), }; @@ -248,7 +249,11 @@ class ReactRefreshPlugin { } } - // Transform global calls into Webpack runtime calls + /** + * Transform global calls into Webpack runtime calls. + * @param {*} parser + * @returns {void} + */ const parserHandler = (parser) => { Object.entries(REPLACEMENTS).forEach(([key, info]) => { parser.hooks.expression diff --git a/lib/runtime/RefreshUtils.js b/lib/runtime/RefreshUtils.js index 055e5d4b..dc8b2630 100644 --- a/lib/runtime/RefreshUtils.js +++ b/lib/runtime/RefreshUtils.js @@ -48,7 +48,7 @@ function createDebounceUpdate() { * A cached setTimeout handler. * @type {number | void} */ - let refreshTimeout = undefined; + let refreshTimeout; /** * Performs react refresh on a delay and clears the error overlay. @@ -56,7 +56,7 @@ function createDebounceUpdate() { * @returns {void} */ function enqueueUpdate(callback) { - if (refreshTimeout === undefined) { + if (typeof refreshTimeout === 'undefined') { refreshTimeout = setTimeout(function () { refreshTimeout = undefined; Refresh.performReactRefresh(); diff --git a/lib/utils/getParserHelpers.js b/lib/utils/getParserHelpers.js index df5f2c80..f7a85c13 100644 --- a/lib/utils/getParserHelpers.js +++ b/lib/utils/getParserHelpers.js @@ -3,7 +3,7 @@ const { webpackVersion } = require('../globals'); /** * @callback EvaluateToString * @param {string} value - * @returns {function(expr: *): *} + * @returns {function(*): *} */ /** @@ -11,7 +11,7 @@ const { webpackVersion } = require('../globals'); * @param {*} parser * @param {string} value * @param {string[]} [runtimeRequirements] - * @returns {function(expr: *): boolean} + * @returns {function(*): boolean} */ /** diff --git a/lib/utils/getRefreshGlobal.js b/lib/utils/getRefreshGlobal.js index 718fa3d3..c9aa7b89 100644 --- a/lib/utils/getRefreshGlobal.js +++ b/lib/utils/getRefreshGlobal.js @@ -3,9 +3,9 @@ const { refreshGlobal } = require('../globals'); /** * @typedef {Object} RuntimeTemplate - * @property {function(args: string, body: string[]): string} basicFunction + * @property {function(string, string[]): string} basicFunction * @property {function(): boolean} supportsConst - * @property {function(returnValue: string, args: string): string} returningFunction + * @property {function(string, string=): string} returningFunction */ /** @type {RuntimeTemplate} */ diff --git a/lib/utils/normalizeOptions.js b/lib/utils/normalizeOptions.js index 2232f71d..136aaa01 100644 --- a/lib/utils/normalizeOptions.js +++ b/lib/utils/normalizeOptions.js @@ -5,22 +5,28 @@ * @param {T} object An object. * @param {Property} property A property of the provided object. * @param {T[Property]} defaultValue The default value to set for the property. - * @returns {T[Property]} + * @returns {T[Property]} The defaulted property value. */ const d = (object, property, defaultValue) => { - return object[property] === undefined ? defaultValue : object[property]; + if (typeof object[property] === 'undefined' && typeof defaultValue !== 'undefined') { + object[property] = defaultValue; + } + return object[property]; }; /** * Resolves the value for a nested object option. * @template T + * @template {keyof T} Property * @template Result - * @param {T | undefined} value The option value. - * @param {function(value: T | undefined): Result} fn The handler to resolve the option's value. + * @param {T} object An object. + * @param {Property} property A property of the provided object. + * @param {function(T | undefined): Result} fn The handler to resolve the property's value. * @returns {Result} The resolved option value. */ -const nestedOption = (value, fn) => { - return fn(value === undefined ? {} : value); +const nestedOption = (object, property, fn) => { + object[property] = fn(object[property]); + return object[property]; }; /** @@ -29,8 +35,9 @@ const nestedOption = (value, fn) => { * @returns {import('../types').NormalizedPluginOptions} Normalized plugin options. */ const normalizeOptions = (options) => { - // Show deprecation notice and remove the option before any processing + // Show deprecation notice for the `disableRefreshCheck` option and remove it if (typeof options.disableRefreshCheck !== 'undefined') { + delete options.disableRefreshCheck; console.warn( [ 'The "disableRefreshCheck" option has been deprecated and will not have any effect on how the plugin parses files.', @@ -39,36 +46,37 @@ const normalizeOptions = (options) => { ); } - return { - exclude: d(options, 'exclude', /node_modules/), - forceEnable: options.forceEnable, - include: d(options, 'include', /\.([jt]sx?|flow)$/), - overlay: nestedOption(options.overlay, (overlay) => { - /** @type {import('../types').NormalizedErrorOverlayOptions} */ - const defaults = { - entry: require.resolve('../../client/ErrorOverlayEntry'), - module: require.resolve('../../overlay'), - sockIntegration: 'wds', - }; + d(options, 'exclude', /node_modules/); + d(options, 'include', /\.([jt]sx?|flow)$/); + d(options, 'forceEnable'); + d(options, 'useLegacyWDSSockets'); + + nestedOption(options, 'overlay', (overlay) => { + /** @type {import('../types').NormalizedErrorOverlayOptions} */ + const defaults = { + entry: require.resolve('../../client/ErrorOverlayEntry'), + module: require.resolve('../../overlay'), + sockIntegration: 'wds', + }; + + if (overlay === false) { + return false; + } + if (typeof overlay === 'undefined' || overlay === true) { + return defaults; + } + + d(overlay, 'entry', defaults.entry); + d(overlay, 'module', defaults.module); + d(overlay, 'sockIntegration', defaults.sockIntegration); + d(overlay, 'sockHost'); + d(overlay, 'sockPath'); + d(overlay, 'sockPort'); - if (overlay === false) { - return false; - } - if (overlay === true) { - return defaults; - } + return overlay; + }); - return { - entry: d(overlay, 'entry', defaults.entry), - module: d(overlay, 'module', defaults.module), - sockHost: overlay.sockHost, - sockIntegration: d(overlay, 'sockIntegration', defaults.sockIntegration), - sockPath: overlay.sockPath, - sockPort: overlay.sockPort, - }; - }), - useLegacyWDSSockets: options.useLegacyWDSSockets, - }; + return options; }; module.exports = normalizeOptions; diff --git a/loader/index.js b/loader/index.js index 55efc383..c0cd0b27 100644 --- a/loader/index.js +++ b/loader/index.js @@ -53,6 +53,12 @@ const RefreshModuleRuntime = getTemplate(require('./RefreshModule.runtime')); function ReactRefreshLoader(source, inputSourceMap, meta) { const callback = this.async(); + /** + * @this {import('webpack').loader.LoaderContext} + * @param {string} source + * @param {import('source-map').RawSourceMap} [inputSourceMap] + * @returns {Promise<[string, import('source-map').RawSourceMap]>} + */ async function _loader(source, inputSourceMap) { if (this.sourceMap) { let originalSourceMap = inputSourceMap; diff --git a/overlay/components/RuntimeErrorFooter.js b/overlay/components/RuntimeErrorFooter.js index 165e1ad3..d7a27ef2 100644 --- a/overlay/components/RuntimeErrorFooter.js +++ b/overlay/components/RuntimeErrorFooter.js @@ -5,9 +5,9 @@ const Spacer = require('./Spacer'); * @typedef {Object} RuntimeErrorFooterProps * @property {string} [initialFocus] * @property {boolean} multiple - * @property {function(event: MouseEvent): void} onClickCloseButton - * @property {function(event: MouseEvent): void} onClickNextButton - * @property {function(event: MouseEvent): void} onClickPrevButton + * @property {function(MouseEvent): void} onClickCloseButton + * @property {function(MouseEvent): void} onClickNextButton + * @property {function(MouseEvent): void} onClickPrevButton */ /** diff --git a/package.json b/package.json index dae9ad9b..7acc970d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "types" ], "scripts": { - "pretest": "yarn link && yarn link \"@pmmmwh/react-refresh-webpack-plugin\"", + "pretest": "patch-package && yarn link && yarn link \"@pmmmwh/react-refresh-webpack-plugin\"", "posttest": "yarn unlink \"@pmmmwh/react-refresh-webpack-plugin\"", "test": "node scripts/test.js", "lint": "eslint --report-unused-disable-directives --ext .js .", @@ -74,11 +74,14 @@ "jest-environment-node": "^26.1.0", "jest-junit": "^11.0.1", "jest-watch-typeahead": "^0.6.0", + "memfs": "^3.2.0", "nanoid": "^3.1.10", + "patch-package": "^6.2.2", "prettier": "^2.0.5", "puppeteer": "^3.3.0", "react-refresh": "^0.8.3", "rimraf": "^3.0.2", + "sourcemap-validator": "^2.1.0", "type-fest": "^0.15.1", "typescript": "^3.9.5", "webpack": "^4.43.0", diff --git a/patches/jest-snapshot+26.1.0.patch b/patches/jest-snapshot+26.1.0.patch new file mode 100644 index 00000000..6e71b8ce --- /dev/null +++ b/patches/jest-snapshot+26.1.0.patch @@ -0,0 +1,16 @@ +diff --git a/node_modules/jest-snapshot/build/index.js b/node_modules/jest-snapshot/build/index.js +index 9094081..db825d4 100644 +--- a/node_modules/jest-snapshot/build/index.js ++++ b/node_modules/jest-snapshot/build/index.js +@@ -504,7 +504,10 @@ const toThrowErrorMatchingInlineSnapshot = function ( + return _toThrowErrorMatchingSnapshot( + { + context: this, +- inlineSnapshot, ++ inlineSnapshot: ++ inlineSnapshot !== undefined ++ ? stripAddedIndentation(inlineSnapshot) ++ : undefined, + isInline: true, + matcherName, + received diff --git a/sockets/WDSSocket.js b/sockets/WDSSocket.js index 4a01f5cd..7a9711df 100644 --- a/sockets/WDSSocket.js +++ b/sockets/WDSSocket.js @@ -5,7 +5,7 @@ const getResourceQuery = require('./utils/getResourceQuery'); /** * Initializes a socket server for HMR for webpack-dev-server. - * @param {function(message: *): void} messageHandler A handler to consume Webpack compilation messages. + * @param {function(*): void} messageHandler A handler to consume Webpack compilation messages. * @returns {void} */ function initWDSSocket(messageHandler) { diff --git a/sockets/WHMEventSource.js b/sockets/WHMEventSource.js index 9632eeef..7040c438 100644 --- a/sockets/WHMEventSource.js +++ b/sockets/WHMEventSource.js @@ -7,7 +7,7 @@ const singletonKey = '__webpack_hot_middleware_reporter__'; /** * Initializes a socket server for HMR for webpack-hot-middleware. - * @param {function(message: *): void} messageHandler A handler to consume Webpack compilation messages. + * @param {function(*): void} messageHandler A handler to consume Webpack compilation messages. * @returns {void} */ function initWHMEventSource(messageHandler) { diff --git a/sockets/WPSSocket.js b/sockets/WPSSocket.js index 400a501c..b07ffdec 100644 --- a/sockets/WPSSocket.js +++ b/sockets/WPSSocket.js @@ -2,7 +2,7 @@ /** * Initializes a socket server for HMR for webpack-plugin-serve. - * @param {function(message: *): void} messageHandler A handler to consume Webpack compilation messages. + * @param {function(*): void} messageHandler A handler to consume Webpack compilation messages. * @returns {void} */ function initWPSSocket(messageHandler) { diff --git a/test/conformance/ReactRefreshRequire.test.js b/test/conformance/ReactRefreshRequire.test.js index 06639064..c5469c65 100644 --- a/test/conformance/ReactRefreshRequire.test.js +++ b/test/conformance/ReactRefreshRequire.test.js @@ -1,8 +1,8 @@ -const createSandbox = require('../sandbox'); +const getSandbox = require('../helpers/sandbox'); // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1028-L1087 it('re-runs accepted modules', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); // Bootstrap test and reload session to not rely on auto-refresh semantics await session.write('index.js', `module.exports = function Noop() { return null; };`); @@ -47,7 +47,7 @@ it('re-runs accepted modules', async () => { // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1089-L1176 it('propagates a hot update to closest accepted module', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); await session.write('index.js', `module.exports = function Noop() { return null; };`); await session.reload(); @@ -128,7 +128,7 @@ it('propagates a hot update to closest accepted module', async () => { // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1178-L1346 it('propagates hot update to all inverse dependencies', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); await session.write('index.js', `module.exports = function Noop() { return null; };`); await session.reload(); @@ -218,7 +218,7 @@ it('propagates hot update to all inverse dependencies', async () => { // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1348-L1445 it('runs dependencies before dependents', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); await session.write('index.js', `module.exports = function Noop() { return null; };`); await session.reload(); @@ -282,7 +282,7 @@ it('runs dependencies before dependents', async () => { // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1447-L1537 it('provides fresh value for module.exports in parents', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); await session.write('index.js', `module.exports = function Noop() { return null; };`); await session.reload(); @@ -341,7 +341,7 @@ it('provides fresh value for module.exports in parents', async () => { // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1539-L1629 it('provides fresh value for exports.* in parents', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); await session.write('index.js', `module.exports = function Noop() { return null; };`); await session.reload(); @@ -402,7 +402,7 @@ it('provides fresh value for exports.* in parents', async () => { // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1631-L1727 it('provides fresh value for ES6 named import in parents', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); await session.write('root.js', `export default function Noop() { return null; };`); await session.write('index.js', `import Root from './root'; Root();`); @@ -464,7 +464,7 @@ it('provides fresh value for ES6 named import in parents', async () => { // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1729-L1825 it('provides fresh value for ES6 default import in parents', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); await session.write('root.js', `export default function Noop() { return null; };`); await session.write('index.js', `import Root from './root'; Root();`); @@ -528,7 +528,7 @@ it('provides fresh value for ES6 default import in parents', async () => { // but rather stops execution in parent after the errored module. // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1827-L1938 it('stops execution after module-level errors', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); await session.write('index.js', `module.exports = function Noop() { return null; };`); await session.reload(); @@ -589,7 +589,7 @@ it('stops execution after module-level errors', async () => { // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1940-L2049 it('can continue hot updates after module-level errors with module.exports', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); await session.write('index.js', `module.exports = function Noop() { return null; };`); await session.reload(); @@ -634,7 +634,7 @@ it('can continue hot updates after module-level errors with module.exports', asy // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2051-L2162 it('can continue hot updates after module-level errors with ES6 exports', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); await session.write('root.js', `export default function Noop() { return null; };`); await session.write('index.js', `import Root from './root'; Root();`); @@ -680,7 +680,7 @@ it('can continue hot updates after module-level errors with ES6 exports', async // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2164-L2272 it('does not accumulate stale exports over time', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); await session.write('index.js', `module.exports = function Noop() { return null; };`); await session.reload(); @@ -746,7 +746,7 @@ it('does not accumulate stale exports over time', async () => { // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2274-L2318 it('bails out if update bubbles to the root via the only path', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); await session.write('index.js', `module.exports = () => null;`); await session.reload(); @@ -783,7 +783,7 @@ it('bails out if update bubbles to the root via the only path', async () => { // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2320-L2410 it('bails out if the update bubbles to the root via one of the paths', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); await session.write('index.js', `module.exports = () => null;`); await session.reload(); @@ -848,7 +848,7 @@ it('bails out if the update bubbles to the root via one of the paths', async () // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2412-L2511 it('propagates a module that stops accepting in next version', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); await session.write('index.js', `module.exports = () => null;`); await session.reload(); @@ -930,7 +930,7 @@ it('propagates a module that stops accepting in next version', async () => { // https://github.com/facebook/metro/blob/c083da2a9465ef53f10ded04bb7c0b748c8b90cb/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2513-L2562 it('can replace a module before it is loaded', async () => { - const [session] = await createSandbox(); + const [session] = await getSandbox(); await session.write('index.js', `module.exports = function Noop() { return null; };`); await session.reload(); diff --git a/test/helpers/compilation/fixtures/source-map-loader.js b/test/helpers/compilation/fixtures/source-map-loader.js new file mode 100644 index 00000000..d6b20093 --- /dev/null +++ b/test/helpers/compilation/fixtures/source-map-loader.js @@ -0,0 +1,4 @@ +module.exports = function sourceMapLoader(source) { + const callback = this.async(); + callback(null, source, this.query.sourceMap); +}; diff --git a/test/helpers/compilation/index.js b/test/helpers/compilation/index.js new file mode 100644 index 00000000..99977a15 --- /dev/null +++ b/test/helpers/compilation/index.js @@ -0,0 +1,159 @@ +const path = require('path'); +const { createFsFromVolume, Volume } = require('memfs'); +const webpack = require('webpack'); +const normalizeErrors = require('./normalizeErrors'); + +const BUNDLE_FILENAME = 'main'; +const CONTEXT_PATH = path.join(__dirname, '../..', 'loader/fixtures'); +const OUTPUT_PATH = path.join(__dirname, 'dist'); + +/** + * @typedef {Object} CompilationModule + * @property {string} execution + * @property {string} parsed + * @property {string} [sourceMap] + */ + +/** + * @typedef {Object} CompilationSession + * @property {*[]} errors + * @property {*[]} warnings + * @property {CompilationModule} module + */ + +/** + * Gets a Webpack compiler instance to test loader operations. + * @param {string} fixtureFile + * @param {Object} [options] + * @param {boolean} [options.devtool] + * @param {*} [options.prevSourceMap] + * @returns {CompilationSession} + */ +async function getCompilation(fixtureFile, options = {}) { + const compiler = webpack({ + mode: 'development', + context: CONTEXT_PATH, + devtool: options.devtool || false, + entry: { + [BUNDLE_FILENAME]: path.join(CONTEXT_PATH, fixtureFile), + }, + output: { + filename: '[name].js', + path: OUTPUT_PATH, + }, + module: { + rules: [ + { + test: /\.js$/, + use: [ + require.resolve('@pmmmwh/react-refresh-webpack-plugin/loader'), + !!options.devtool && + Object.prototype.hasOwnProperty.call(options, 'prevSourceMap') && { + loader: path.join(__dirname, 'fixtures/source-map-loader.js'), + options: { + sourceMap: options.prevSourceMap, + }, + }, + ].filter(Boolean), + }, + ], + }, + plugins: [new webpack.HotModuleReplacementPlugin()], + // Options below forces Webpack to: + // 1. Move Webpack runtime into the runtime chunk; + // 2. Move node_modules into the vendor chunk with a stable name. + optimization: { + runtimeChunk: 'single', + splitChunks: { + chunks: 'all', + name: (module, chunks, cacheGroupKey) => cacheGroupKey, + }, + }, + }); + + // Use an in-memory file system to prevent emitting files + compiler.outputFileSystem = createFsFromVolume(new Volume()); + if (WEBPACK_VERSION !== 5) { + compiler.outputFileSystem.join = path.join.bind(path); + } + + /** @type {import('memfs').IFs} */ + const compilerOutputFs = compiler.outputFileSystem; + /** @type {import('webpack').Stats | undefined} */ + let compilationStats; + + await new Promise((resolve, reject) => { + compiler.run((error, stats) => { + if (error) { + reject(error); + return; + } + + compilationStats = stats; + + if (WEBPACK_VERSION !== 5) { + resolve(); + } else { + // The compiler have to be explicitly closed in Webpack 5 + compiler.close(() => { + resolve(); + }); + } + }); + }); + + return { + /** @type {*[]} */ + get errors() { + return normalizeErrors(compilationStats.compilation.errors); + }, + /** @type {*[]} */ + get warnings() { + return normalizeErrors(compilationStats.compilation.errors); + }, + /** @type {CompilationModule} */ + get module() { + const parsed = compilationStats + .toJson({ source: true }) + .modules.find(({ name }) => name === fixtureFile); + if (!parsed) { + throw new Error('Fixture module is not found in compilation stats!'); + } + + let execution; + try { + execution = compilerOutputFs + .readFileSync(path.join(OUTPUT_PATH, `${BUNDLE_FILENAME}.js`)) + .toString(); + } catch (error) { + execution = error.toString(); + } + + /** @type {string | undefined} */ + let sourceMap; + const [, sourceMapUrl] = execution.match(/\/\/# sourceMappingURL=(.*)$/) || []; + const isInlineSourceMap = !!sourceMapUrl && /^data:application\/json;/.test(sourceMapUrl); + if (!isInlineSourceMap) { + try { + sourceMap = JSON.stringify( + JSON.parse( + compilerOutputFs.readFileSync(path.join(OUTPUT_PATH, sourceMapUrl)).toString() + ), + null, + 2 + ); + } catch (error) { + sourceMap = error.toString(); + } + } + + return { + parsed: parsed.source, + execution, + sourceMap, + }; + }, + }; +} + +module.exports = getCompilation; diff --git a/test/helpers/compilation/normalizeErrors.js b/test/helpers/compilation/normalizeErrors.js new file mode 100644 index 00000000..4f5570f1 --- /dev/null +++ b/test/helpers/compilation/normalizeErrors.js @@ -0,0 +1,18 @@ +function removeCwd(str) { + let cwd = process.cwd(); + let result = str; + + const isWin = process.platform === 'win32'; + if (isWin) { + cwd = cwd.replace(/\\/g, '/'); + result = result.replace(/\\/g, '/'); + } + + return result.replace(new RegExp(cwd, 'g'), ''); +} + +function normalizeErrors(errors) { + return errors.map((error) => removeCwd(error.toString().split('\n').slice(0, 2).join('\n'))); +} + +module.exports = normalizeErrors; diff --git a/test/sandbox/configs.js b/test/helpers/sandbox/configs.js similarity index 95% rename from test/sandbox/configs.js rename to test/helpers/sandbox/configs.js index 2e24cad9..ba0d0dd2 100644 --- a/test/sandbox/configs.js +++ b/test/helpers/sandbox/configs.js @@ -37,7 +37,7 @@ module.exports = { devtool: false, entry: { '${BUNDLE_FILENAME}': [ - '${path.join(__dirname, './runtime/hot-notifier.js')}', + '${path.join(__dirname, 'fixtures/hmr-notifier.js')}', './index.js', ], }, diff --git a/test/sandbox/runtime/hot-notifier.js b/test/helpers/sandbox/fixtures/hmr-notifier.js similarity index 100% rename from test/sandbox/runtime/hot-notifier.js rename to test/helpers/sandbox/fixtures/hmr-notifier.js diff --git a/test/sandbox/index.js b/test/helpers/sandbox/index.js similarity index 93% rename from test/sandbox/index.js rename to test/helpers/sandbox/index.js index 5366c615..6fa17f2a 100644 --- a/test/sandbox/index.js +++ b/test/helpers/sandbox/index.js @@ -44,14 +44,14 @@ const sleep = (ms) => { * @property {*[]} errors * @property {*[]} logs * @property {function(): void} resetState - * @property {function(filename: string, content: string): Promise} write - * @property {function(filename: string, content: string): Promise} patch - * @property {function(filename: string): Promise} remove - * @property {function(fn: *, args: ...*): Promise<*>} evaluate + * @property {function(string, string): Promise} write + * @property {function(string, string): Promise} patch + * @property {function(string): Promise} remove + * @property {function(*, ...*=): Promise<*>} evaluate * @property {function(): Promise} reload */ -const rootSandboxDir = path.join(__dirname, '..', '__tmp__'); +const rootSandboxDir = path.join(__dirname, '../..', '__tmp__'); const spawnFn = WEBPACK_VERSION === 5 ? spawnWebpackServe : spawnWDS; /** @@ -61,7 +61,7 @@ const spawnFn = WEBPACK_VERSION === 5 ? spawnWebpackServe : spawnWDS; * @param {Map} [options.initialFiles] * @returns {Promise<[SandboxSession, function(): Promise]>} */ -async function sandbox({ id = nanoid(), initialFiles = new Map() } = {}) { +async function getSandbox({ id = nanoid(), initialFiles = new Map() } = {}) { const port = await getPort(); // Get sandbox directory paths @@ -146,15 +146,15 @@ async function sandbox({ id = nanoid(), initialFiles = new Map() } = {}) { return [ { - /** @returns {boolean} */ + /** @type {boolean} */ get didFullRefresh() { return didFullRefresh; }, - /** @returns {*[]} */ + /** @type {*[]} */ get errors() { return errors; }, - /** @returns {*[]} */ + /** @type {*[]} */ get logs() { return logs; }, @@ -256,4 +256,4 @@ async function sandbox({ id = nanoid(), initialFiles = new Map() } = {}) { ]; } -module.exports = sandbox; +module.exports = getSandbox; diff --git a/test/sandbox/spawn.js b/test/helpers/sandbox/spawn.js similarity index 98% rename from test/sandbox/spawn.js rename to test/helpers/sandbox/spawn.js index 501d16d9..8c0522c5 100644 --- a/test/sandbox/spawn.js +++ b/test/helpers/sandbox/spawn.js @@ -11,7 +11,7 @@ const spawn = require('cross-spawn'); * @returns {Promise} */ function spawnTestProcess(processPath, argv, options = {}) { - const cwd = options.cwd || path.resolve(__dirname, '../..'); + const cwd = options.cwd || path.resolve(__dirname, '../../..'); const env = { ...process.env, NODE_ENV: 'development', diff --git a/test/jest-test-setup.js b/test/jest-test-setup.js new file mode 100644 index 00000000..a0ad5165 --- /dev/null +++ b/test/jest-test-setup.js @@ -0,0 +1,30 @@ +/** + * Skips a test block conditionally. + * @param {boolean} condition The condition to skip the test block. + * @param {string} blockName The name of the test block. + * @param {import('@jest/types').Global.BlockFn} blockFn The test block function. + * @return {void} + */ +describe.skipIf = (condition, blockName, blockFn) => { + if (condition) { + return describe.skip(blockName, blockFn); + } + return describe(blockName, blockFn); +}; + +/** + * Skips a test conditionally. + * @param {boolean} condition The condition to skip the test. + * @param {string} testName The name of the test. + * @param {import('@jest/types').Global.TestFn} fn The test function. + * @param {number} [timeout] The time to wait before aborting. + * @return {void} + */ +test.skipIf = (condition, testName, fn, timeout) => { + if (condition) { + return test.skip(testName, fn); + } + return test(testName, fn, timeout); +}; + +it.skipIf = test.skipIf; diff --git a/test/loader/fixtures/index.cjs.js b/test/loader/fixtures/index.cjs.js new file mode 100644 index 00000000..dfb28e91 --- /dev/null +++ b/test/loader/fixtures/index.cjs.js @@ -0,0 +1 @@ +module.exports = 'Test'; diff --git a/test/loader/fixtures/index.esm.js b/test/loader/fixtures/index.esm.js new file mode 100644 index 00000000..d1985dfa --- /dev/null +++ b/test/loader/fixtures/index.esm.js @@ -0,0 +1 @@ +export default 'Test'; diff --git a/test/loader/loader.test.js b/test/loader/loader.test.js new file mode 100644 index 00000000..6211ef53 --- /dev/null +++ b/test/loader/loader.test.js @@ -0,0 +1,796 @@ +const validate = require('sourcemap-validator'); +const getCompilation = require('../helpers/compilation'); + +describe('loader', () => { + describe.skipIf(WEBPACK_VERSION !== 4, 'on Webpack 4', () => { + it('should work for CommonJS', async () => { + const compilation = await getCompilation('./index.cjs.js'); + const { execution, parsed } = compilation.module; + + expect(parsed).toMatchInlineSnapshot(` + "$RefreshRuntime$ = require('react-refresh/runtime'); + $RefreshSetup$(module.id); + + module.exports = 'Test'; + + + const currentExports = __react_refresh_utils__.getModuleExports(module.id); + + __react_refresh_utils__.registerExportsForReactRefresh(currentExports, module.id); + + if (module.hot) { + const isHotUpdate = !!module.hot.data; + const prevExports = isHotUpdate ? module.hot.data.prevExports : null; // This is a workaround for webpack/webpack#11057 + // FIXME: Revert after webpack/webpack#11059 is merged + + const isTestMode = typeof __react_refresh_test__ !== 'undefined' && __react_refresh_test__; + + if (__react_refresh_utils__.isReactRefreshBoundary(currentExports)) { + module.hot.dispose( + /** + * A callback to performs a full refresh if React has unrecoverable errors, + * and also caches the to-be-disposed module. + * @param {*} data A hot module data object from Webpack HMR. + * @returns {void} + */ + function hotDisposeCallback(data) { + // We have to mutate the data object to get data registered and cached + data.prevExports = currentExports; + }); + module.hot.accept( + /** + * An error handler to allow self-recovering behaviours. + * @param {Error} error An error occurred during evaluation of a module. + * @returns {void} + */ + function hotErrorHandler(error) { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.handleRuntimeError(error); + } + + if (isTestMode) { + if (window.onHotAcceptError) { + window.onHotAcceptError(error.message); + } + } + + __webpack_require__.c[module.id].hot.accept(hotErrorHandler); + }); + + if (isHotUpdate) { + if (__react_refresh_utils__.isReactRefreshBoundary(prevExports) && __react_refresh_utils__.shouldInvalidateReactRefreshBoundary(prevExports, currentExports)) { + module.hot.invalidate(); + } else { + __react_refresh_utils__.enqueueUpdate( + /** + * A function to dismiss the error overlay after performing React refresh. + * @returns {void} + */ + function updateCallback() { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.clearRuntimeErrors(); + } + }); + } + } + } else { + if (isHotUpdate && __react_refresh_utils__.isReactRefreshBoundary(prevExports)) { + module.hot.invalidate(); + } + } + }" + `); + expect(execution).toMatchInlineSnapshot(` + "(window[\\"webpackJsonp\\"] = window[\\"webpackJsonp\\"] || []).push([[\\"main\\"],{ + + /***/ \\"./index.cjs.js\\": + /*!**********************!*\\\\ + !*** ./index.cjs.js ***! + \\\\**********************/ + /*! no static exports found */ + /***/ (function(module, exports, __webpack_require__) { + + $RefreshRuntime$ = __webpack_require__(/*! react-refresh/runtime */ \\"../../../node_modules/react-refresh/runtime.js\\"); + $RefreshSetup$(module.i); + + module.exports = 'Test'; + + + const currentExports = __react_refresh_utils__.getModuleExports(module.i); + + __react_refresh_utils__.registerExportsForReactRefresh(currentExports, module.i); + + if (true) { + const isHotUpdate = !!module.hot.data; + const prevExports = isHotUpdate ? module.hot.data.prevExports : null; // This is a workaround for webpack/webpack#11057 + // FIXME: Revert after webpack/webpack#11059 is merged + + const isTestMode = typeof __react_refresh_test__ !== 'undefined' && __react_refresh_test__; + + if (__react_refresh_utils__.isReactRefreshBoundary(currentExports)) { + module.hot.dispose( + /** + * A callback to performs a full refresh if React has unrecoverable errors, + * and also caches the to-be-disposed module. + * @param {*} data A hot module data object from Webpack HMR. + * @returns {void} + */ + function hotDisposeCallback(data) { + // We have to mutate the data object to get data registered and cached + data.prevExports = currentExports; + }); + module.hot.accept( + /** + * An error handler to allow self-recovering behaviours. + * @param {Error} error An error occurred during evaluation of a module. + * @returns {void} + */ + function hotErrorHandler(error) { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.handleRuntimeError(error); + } + + if (isTestMode) { + if (window.onHotAcceptError) { + window.onHotAcceptError(error.message); + } + } + + __webpack_require__.c[module.i].hot.accept(hotErrorHandler); + }); + + if (isHotUpdate) { + if (__react_refresh_utils__.isReactRefreshBoundary(prevExports) && __react_refresh_utils__.shouldInvalidateReactRefreshBoundary(prevExports, currentExports)) { + module.hot.invalidate(); + } else { + __react_refresh_utils__.enqueueUpdate( + /** + * A function to dismiss the error overlay after performing React refresh. + * @returns {void} + */ + function updateCallback() { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.clearRuntimeErrors(); + } + }); + } + } + } else { + if (isHotUpdate && __react_refresh_utils__.isReactRefreshBoundary(prevExports)) { + module.hot.invalidate(); + } + } + } + + /***/ }) + + },[[\\"./index.cjs.js\\",\\"runtime\\",\\"vendors\\"]]]);" + `); + + expect(compilation.errors).toStrictEqual([]); + expect(compilation.warnings).toStrictEqual([]); + }); + + it('should work for ES Modules', async () => { + const compilation = await getCompilation('./index.esm.js'); + const { execution, parsed } = compilation.module; + + expect(parsed).toMatchInlineSnapshot(` + "$RefreshRuntime$ = require('react-refresh/runtime'); + $RefreshSetup$(module.id); + + export default 'Test'; + + + const currentExports = __react_refresh_utils__.getModuleExports(module.id); + + __react_refresh_utils__.registerExportsForReactRefresh(currentExports, module.id); + + if (module.hot) { + const isHotUpdate = !!module.hot.data; + const prevExports = isHotUpdate ? module.hot.data.prevExports : null; // This is a workaround for webpack/webpack#11057 + // FIXME: Revert after webpack/webpack#11059 is merged + + const isTestMode = typeof __react_refresh_test__ !== 'undefined' && __react_refresh_test__; + + if (__react_refresh_utils__.isReactRefreshBoundary(currentExports)) { + module.hot.dispose( + /** + * A callback to performs a full refresh if React has unrecoverable errors, + * and also caches the to-be-disposed module. + * @param {*} data A hot module data object from Webpack HMR. + * @returns {void} + */ + function hotDisposeCallback(data) { + // We have to mutate the data object to get data registered and cached + data.prevExports = currentExports; + }); + module.hot.accept( + /** + * An error handler to allow self-recovering behaviours. + * @param {Error} error An error occurred during evaluation of a module. + * @returns {void} + */ + function hotErrorHandler(error) { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.handleRuntimeError(error); + } + + if (isTestMode) { + if (window.onHotAcceptError) { + window.onHotAcceptError(error.message); + } + } + + __webpack_require__.c[module.id].hot.accept(hotErrorHandler); + }); + + if (isHotUpdate) { + if (__react_refresh_utils__.isReactRefreshBoundary(prevExports) && __react_refresh_utils__.shouldInvalidateReactRefreshBoundary(prevExports, currentExports)) { + module.hot.invalidate(); + } else { + __react_refresh_utils__.enqueueUpdate( + /** + * A function to dismiss the error overlay after performing React refresh. + * @returns {void} + */ + function updateCallback() { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.clearRuntimeErrors(); + } + }); + } + } + } else { + if (isHotUpdate && __react_refresh_utils__.isReactRefreshBoundary(prevExports)) { + module.hot.invalidate(); + } + } + }" + `); + expect(execution).toMatchInlineSnapshot(` + "(window[\\"webpackJsonp\\"] = window[\\"webpackJsonp\\"] || []).push([[\\"main\\"],{ + + /***/ \\"./index.esm.js\\": + /*!**********************!*\\\\ + !*** ./index.esm.js ***! + \\\\**********************/ + /*! exports provided: default */ + /***/ (function(module, __webpack_exports__, __webpack_require__) { + + \\"use strict\\"; + __webpack_require__.r(__webpack_exports__); + $RefreshRuntime$ = __webpack_require__(/*! react-refresh/runtime */ \\"../../../node_modules/react-refresh/runtime.js\\"); + $RefreshSetup$(module.i); + + /* harmony default export */ __webpack_exports__[\\"default\\"] = ('Test'); + + + const currentExports = __react_refresh_utils__.getModuleExports(module.i); + + __react_refresh_utils__.registerExportsForReactRefresh(currentExports, module.i); + + if (true) { + const isHotUpdate = !!module.hot.data; + const prevExports = isHotUpdate ? module.hot.data.prevExports : null; // This is a workaround for webpack/webpack#11057 + // FIXME: Revert after webpack/webpack#11059 is merged + + const isTestMode = typeof __react_refresh_test__ !== 'undefined' && __react_refresh_test__; + + if (__react_refresh_utils__.isReactRefreshBoundary(currentExports)) { + module.hot.dispose( + /** + * A callback to performs a full refresh if React has unrecoverable errors, + * and also caches the to-be-disposed module. + * @param {*} data A hot module data object from Webpack HMR. + * @returns {void} + */ + function hotDisposeCallback(data) { + // We have to mutate the data object to get data registered and cached + data.prevExports = currentExports; + }); + module.hot.accept( + /** + * An error handler to allow self-recovering behaviours. + * @param {Error} error An error occurred during evaluation of a module. + * @returns {void} + */ + function hotErrorHandler(error) { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.handleRuntimeError(error); + } + + if (isTestMode) { + if (window.onHotAcceptError) { + window.onHotAcceptError(error.message); + } + } + + __webpack_require__.c[module.i].hot.accept(hotErrorHandler); + }); + + if (isHotUpdate) { + if (__react_refresh_utils__.isReactRefreshBoundary(prevExports) && __react_refresh_utils__.shouldInvalidateReactRefreshBoundary(prevExports, currentExports)) { + module.hot.invalidate(); + } else { + __react_refresh_utils__.enqueueUpdate( + /** + * A function to dismiss the error overlay after performing React refresh. + * @returns {void} + */ + function updateCallback() { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.clearRuntimeErrors(); + } + }); + } + } + } else { + if (isHotUpdate && __react_refresh_utils__.isReactRefreshBoundary(prevExports)) { + module.hot.invalidate(); + } + } + } + + /***/ }) + + },[[\\"./index.esm.js\\",\\"runtime\\",\\"vendors\\"]]]);" + `); + + expect(compilation.errors).toStrictEqual([]); + expect(compilation.warnings).toStrictEqual([]); + }); + + it('should generate valid source map when the "devtool" option is specified', async () => { + const compilation = await getCompilation('./index.cjs.js', { devtool: 'source-map' }); + const { execution, sourceMap } = compilation.module; + + expect(sourceMap).toMatchInlineSnapshot(` + "{ + \\"version\\": 3, + \\"sources\\": [ + \\"webpack:///./index.cjs.js\\" + ], + \\"names\\": [], + \\"mappings\\": \\";;;;;;;;;;;;AAAA\\", + \\"file\\": \\"main.js\\", + \\"sourcesContent\\": [ + \\"module.exports = 'Test';\\\\n\\" + ], + \\"sourceRoot\\": \\"\\" + }" + `); + expect(() => { + validate(execution, sourceMap); + }).not.toThrow(); + }); + }); + + describe.skipIf(WEBPACK_VERSION !== 5, 'on Webpack 5', () => { + it('should work for CommonJS', async () => { + const compilation = await getCompilation('./index.cjs.js'); + const { execution, parsed } = compilation.module; + + expect(parsed).toMatchInlineSnapshot(` + "$RefreshRuntime$ = require('react-refresh/runtime'); + $RefreshSetup$(module.id); + + module.exports = 'Test'; + + + const currentExports = __react_refresh_utils__.getModuleExports(module.id); + + __react_refresh_utils__.registerExportsForReactRefresh(currentExports, module.id); + + if (module.hot) { + const isHotUpdate = !!module.hot.data; + const prevExports = isHotUpdate ? module.hot.data.prevExports : null; // This is a workaround for webpack/webpack#11057 + // FIXME: Revert after webpack/webpack#11059 is merged + + const isTestMode = typeof __react_refresh_test__ !== 'undefined' && __react_refresh_test__; + + if (__react_refresh_utils__.isReactRefreshBoundary(currentExports)) { + module.hot.dispose( + /** + * A callback to performs a full refresh if React has unrecoverable errors, + * and also caches the to-be-disposed module. + * @param {*} data A hot module data object from Webpack HMR. + * @returns {void} + */ + function hotDisposeCallback(data) { + // We have to mutate the data object to get data registered and cached + data.prevExports = currentExports; + }); + module.hot.accept( + /** + * An error handler to allow self-recovering behaviours. + * @param {Error} error An error occurred during evaluation of a module. + * @returns {void} + */ + function hotErrorHandler(error) { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.handleRuntimeError(error); + } + + if (isTestMode) { + if (window.onHotAcceptError) { + window.onHotAcceptError(error.message); + } + } + + __webpack_require__.c[module.id].hot.accept(hotErrorHandler); + }); + + if (isHotUpdate) { + if (__react_refresh_utils__.isReactRefreshBoundary(prevExports) && __react_refresh_utils__.shouldInvalidateReactRefreshBoundary(prevExports, currentExports)) { + module.hot.invalidate(); + } else { + __react_refresh_utils__.enqueueUpdate( + /** + * A function to dismiss the error overlay after performing React refresh. + * @returns {void} + */ + function updateCallback() { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.clearRuntimeErrors(); + } + }); + } + } + } else { + if (isHotUpdate && __react_refresh_utils__.isReactRefreshBoundary(prevExports)) { + module.hot.invalidate(); + } + } + }" + `); + expect(execution).toMatchInlineSnapshot(` + "(window[\\"webpackJsonp\\"] = window[\\"webpackJsonp\\"] || []).push([[\\"main\\"],{ + + /***/ \\"./index.cjs.js\\": + /*!**********************!*\\\\ + !*** ./index.cjs.js ***! + \\\\**********************/ + /*! unknown exports (runtime-defined) */ + /*! exports [maybe provided (runtime-defined)] [maybe used (runtime-defined)] */ + /*! runtime requirements: module, __webpack_require__, module.id */ + /***/ ((module, __unused_webpack_exports, __webpack_require__) => { + + $RefreshRuntime$ = __webpack_require__(/*! react-refresh/runtime */ \\"../../../node_modules/react-refresh/runtime.js\\"); + $RefreshSetup$(module.id); + + module.exports = 'Test'; + + + const currentExports = __react_refresh_utils__.getModuleExports(module.id); + + __react_refresh_utils__.registerExportsForReactRefresh(currentExports, module.id); + + if (true) { + const isHotUpdate = !!module.hot.data; + const prevExports = isHotUpdate ? module.hot.data.prevExports : null; // This is a workaround for webpack/webpack#11057 + // FIXME: Revert after webpack/webpack#11059 is merged + + const isTestMode = typeof __react_refresh_test__ !== 'undefined' && __react_refresh_test__; + + if (__react_refresh_utils__.isReactRefreshBoundary(currentExports)) { + module.hot.dispose( + /** + * A callback to performs a full refresh if React has unrecoverable errors, + * and also caches the to-be-disposed module. + * @param {*} data A hot module data object from Webpack HMR. + * @returns {void} + */ + function hotDisposeCallback(data) { + // We have to mutate the data object to get data registered and cached + data.prevExports = currentExports; + }); + module.hot.accept( + /** + * An error handler to allow self-recovering behaviours. + * @param {Error} error An error occurred during evaluation of a module. + * @returns {void} + */ + function hotErrorHandler(error) { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.handleRuntimeError(error); + } + + if (isTestMode) { + if (window.onHotAcceptError) { + window.onHotAcceptError(error.message); + } + } + + __webpack_require__.c[module.id].hot.accept(hotErrorHandler); + }); + + if (isHotUpdate) { + if (__react_refresh_utils__.isReactRefreshBoundary(prevExports) && __react_refresh_utils__.shouldInvalidateReactRefreshBoundary(prevExports, currentExports)) { + module.hot.invalidate(); + } else { + __react_refresh_utils__.enqueueUpdate( + /** + * A function to dismiss the error overlay after performing React refresh. + * @returns {void} + */ + function updateCallback() { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.clearRuntimeErrors(); + } + }); + } + } + } else { + if (isHotUpdate && __react_refresh_utils__.isReactRefreshBoundary(prevExports)) { + module.hot.invalidate(); + } + } + } + + /***/ }) + + },[[\\"./index.cjs.js\\",\\"runtime\\",\\"defaultVendors\\"]]]);" + `); + + expect(compilation.errors).toStrictEqual([]); + expect(compilation.warnings).toStrictEqual([]); + }); + + it('should work for ES Modules', async () => { + const compilation = await getCompilation('./index.esm.js'); + const { execution, parsed } = compilation.module; + + expect(parsed).toMatchInlineSnapshot(` + "$RefreshRuntime$ = require('react-refresh/runtime'); + $RefreshSetup$(module.id); + + export default 'Test'; + + + const currentExports = __react_refresh_utils__.getModuleExports(module.id); + + __react_refresh_utils__.registerExportsForReactRefresh(currentExports, module.id); + + if (module.hot) { + const isHotUpdate = !!module.hot.data; + const prevExports = isHotUpdate ? module.hot.data.prevExports : null; // This is a workaround for webpack/webpack#11057 + // FIXME: Revert after webpack/webpack#11059 is merged + + const isTestMode = typeof __react_refresh_test__ !== 'undefined' && __react_refresh_test__; + + if (__react_refresh_utils__.isReactRefreshBoundary(currentExports)) { + module.hot.dispose( + /** + * A callback to performs a full refresh if React has unrecoverable errors, + * and also caches the to-be-disposed module. + * @param {*} data A hot module data object from Webpack HMR. + * @returns {void} + */ + function hotDisposeCallback(data) { + // We have to mutate the data object to get data registered and cached + data.prevExports = currentExports; + }); + module.hot.accept( + /** + * An error handler to allow self-recovering behaviours. + * @param {Error} error An error occurred during evaluation of a module. + * @returns {void} + */ + function hotErrorHandler(error) { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.handleRuntimeError(error); + } + + if (isTestMode) { + if (window.onHotAcceptError) { + window.onHotAcceptError(error.message); + } + } + + __webpack_require__.c[module.id].hot.accept(hotErrorHandler); + }); + + if (isHotUpdate) { + if (__react_refresh_utils__.isReactRefreshBoundary(prevExports) && __react_refresh_utils__.shouldInvalidateReactRefreshBoundary(prevExports, currentExports)) { + module.hot.invalidate(); + } else { + __react_refresh_utils__.enqueueUpdate( + /** + * A function to dismiss the error overlay after performing React refresh. + * @returns {void} + */ + function updateCallback() { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.clearRuntimeErrors(); + } + }); + } + } + } else { + if (isHotUpdate && __react_refresh_utils__.isReactRefreshBoundary(prevExports)) { + module.hot.invalidate(); + } + } + }" + `); + expect(execution).toMatchInlineSnapshot(` + "(window[\\"webpackJsonp\\"] = window[\\"webpackJsonp\\"] || []).push([[\\"main\\"],{ + + /***/ \\"./index.esm.js\\": + /*!**********************!*\\\\ + !*** ./index.esm.js ***! + \\\\**********************/ + /*! namespace exports */ + /*! export default [provided] [unused] [could be renamed] */ + /*! other exports [not provided] [unused] */ + /*! runtime requirements: module, __webpack_require__, module.id */ + /***/ ((module, __unused_webpack___webpack_exports__, __webpack_require__) => { + + \\"use strict\\"; + $RefreshRuntime$ = __webpack_require__(/*! react-refresh/runtime */ \\"../../../node_modules/react-refresh/runtime.js\\"); + $RefreshSetup$(module.id); + + /* unused harmony default export */ var _unused_webpack_default_export = ('Test'); + + + const currentExports = __react_refresh_utils__.getModuleExports(module.id); + + __react_refresh_utils__.registerExportsForReactRefresh(currentExports, module.id); + + if (true) { + const isHotUpdate = !!module.hot.data; + const prevExports = isHotUpdate ? module.hot.data.prevExports : null; // This is a workaround for webpack/webpack#11057 + // FIXME: Revert after webpack/webpack#11059 is merged + + const isTestMode = typeof __react_refresh_test__ !== 'undefined' && __react_refresh_test__; + + if (__react_refresh_utils__.isReactRefreshBoundary(currentExports)) { + module.hot.dispose( + /** + * A callback to performs a full refresh if React has unrecoverable errors, + * and also caches the to-be-disposed module. + * @param {*} data A hot module data object from Webpack HMR. + * @returns {void} + */ + function hotDisposeCallback(data) { + // We have to mutate the data object to get data registered and cached + data.prevExports = currentExports; + }); + module.hot.accept( + /** + * An error handler to allow self-recovering behaviours. + * @param {Error} error An error occurred during evaluation of a module. + * @returns {void} + */ + function hotErrorHandler(error) { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.handleRuntimeError(error); + } + + if (isTestMode) { + if (window.onHotAcceptError) { + window.onHotAcceptError(error.message); + } + } + + __webpack_require__.c[module.id].hot.accept(hotErrorHandler); + }); + + if (isHotUpdate) { + if (__react_refresh_utils__.isReactRefreshBoundary(prevExports) && __react_refresh_utils__.shouldInvalidateReactRefreshBoundary(prevExports, currentExports)) { + module.hot.invalidate(); + } else { + __react_refresh_utils__.enqueueUpdate( + /** + * A function to dismiss the error overlay after performing React refresh. + * @returns {void} + */ + function updateCallback() { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { + __react_refresh_error_overlay__.clearRuntimeErrors(); + } + }); + } + } + } else { + if (isHotUpdate && __react_refresh_utils__.isReactRefreshBoundary(prevExports)) { + module.hot.invalidate(); + } + } + } + + /***/ }) + + },[[\\"./index.esm.js\\",\\"runtime\\",\\"defaultVendors\\"]]]);" + `); + + expect(compilation.errors).toStrictEqual([]); + expect(compilation.warnings).toStrictEqual([]); + }); + + it('should generate valid source map when the "devtool" option is specified', async () => { + const compilation = await getCompilation('./index.cjs.js', { devtool: 'source-map' }); + const { execution, sourceMap } = compilation.module; + + expect(sourceMap).toMatchInlineSnapshot(` + "{ + \\"version\\": 3, + \\"sources\\": [ + \\"webpack:///./index.cjs.js\\" + ], + \\"names\\": [], + \\"mappings\\": \\";;;;;;;;;;;;;;AAAA\\", + \\"file\\": \\"main.js\\", + \\"sourcesContent\\": [ + \\"module.exports = 'Test';\\\\n\\" + ], + \\"sourceRoot\\": \\"\\" + }" + `); + expect(() => { + validate(execution, sourceMap); + }).not.toThrow(); + }); + }); + + it('should generate valid source map when undefined source map is provided', async () => { + const compilation = await getCompilation('./index.cjs.js', { + devtool: 'source-map', + prevSourceMap: undefined, + }); + const { execution, sourceMap } = compilation.module; + + expect(() => { + validate(execution, sourceMap); + }).not.toThrow(); + }); + + it('should generate valid source map when null source map is provided', async () => { + const compilation = await getCompilation('./index.cjs.js', { + devtool: 'source-map', + prevSourceMap: null, + }); + const { execution, sourceMap } = compilation.module; + + expect(() => { + validate(execution, sourceMap); + }).not.toThrow(); + }); + + it('should generate valid source map when source map string is provided', async () => { + const compilation = await getCompilation('./index.cjs.js', { + devtool: 'source-map', + prevSourceMap: JSON.stringify({ + version: 3, + sources: ['./index.cjs.js'], + names: [], + mappings: 'AAAA;AACA', + sourcesContent: ["module.exports = 'Test';\n"], + }), + }); + const { execution, sourceMap } = compilation.module; + + expect(() => { + validate(execution, sourceMap); + }).not.toThrow(); + }); + + it('should generate valid source map when source map object is provided', async () => { + const compilation = await getCompilation('./index.cjs.js', { + devtool: 'source-map', + prevSourceMap: { + version: 3, + sources: ['./index.cjs.js'], + names: [], + mappings: 'AAAA;AACA', + sourcesContent: ["module.exports = 'Test';\n"], + }, + }); + const { execution, sourceMap } = compilation.module; + + expect(() => { + validate(execution, sourceMap); + }).not.toThrow(); + }); +}); diff --git a/test/unit/getRefreshGlobal.test.js b/test/unit/getRefreshGlobal.test.js new file mode 100644 index 00000000..3ceb3f3d --- /dev/null +++ b/test/unit/getRefreshGlobal.test.js @@ -0,0 +1,84 @@ +const getRefreshGlobal = require('../../lib/utils/getRefreshGlobal'); + +describe('getRefreshGlobal', () => { + beforeEach(() => { + global.__webpack_require__ = {}; + }); + + afterAll(() => { + delete global.__webpack_require__; + }); + + it('should return refresh global template without providing runtime template', () => { + const refreshGlobal = getRefreshGlobal(); + expect(refreshGlobal).toMatchInlineSnapshot(` + "__webpack_require__.$Refresh$ = { + cleanup: function() { return undefined; }, + register: function() { return undefined; }, + runtime: {}, + setup: function(currentModuleId) { + var prevCleanup = __webpack_require__.$Refresh$.cleanup; + var prevReg = __webpack_require__.$Refresh$.register; + var prevSig = __webpack_require__.$Refresh$.signature; + + __webpack_require__.$Refresh$.register = function(type, id) { + var typeId = currentModuleId + \\" \\" + id; + __webpack_require__.$Refresh$.runtime.register(type, typeId); + } + + __webpack_require__.$Refresh$.signature = __webpack_require__.$Refresh$.runtime.createSignatureFunctionForTransform; + + __webpack_require__.$Refresh$.cleanup = function() { + __webpack_require__.$Refresh$.register = prevReg; + __webpack_require__.$Refresh$.signature = prevSig; + __webpack_require__.$Refresh$.cleanup = prevCleanup; + } + }, + signature: function() { return function(type) { return type; }; } + };" + `); + expect(() => { + eval(refreshGlobal); + }).not.toThrow(); + }); + + it.skipIf( + WEBPACK_VERSION !== 5, + 'should return refresh global template with provided runtime template', + () => { + const RuntimeTemplate = require('webpack/lib/RuntimeTemplate'); + const refreshGlobal = getRefreshGlobal( + new RuntimeTemplate({ ecmaVersion: 6 }, { shorten: (item) => item }) + ); + expect(refreshGlobal).toMatchInlineSnapshot(` + "__webpack_require__.$Refresh$ = { + cleanup: () => undefined, + register: () => undefined, + runtime: {}, + setup: (currentModuleId) => { + const prevCleanup = __webpack_require__.$Refresh$.cleanup; + const prevReg = __webpack_require__.$Refresh$.register; + const prevSig = __webpack_require__.$Refresh$.signature; + + __webpack_require__.$Refresh$.register = (type, id) => { + const typeId = currentModuleId + \\" \\" + id; + __webpack_require__.$Refresh$.runtime.register(type, typeId); + } + + __webpack_require__.$Refresh$.signature = __webpack_require__.$Refresh$.runtime.createSignatureFunctionForTransform; + + __webpack_require__.$Refresh$.cleanup = () => { + __webpack_require__.$Refresh$.register = prevReg; + __webpack_require__.$Refresh$.signature = prevSig; + __webpack_require__.$Refresh$.cleanup = prevCleanup; + } + }, + signature: () => (type) => type + };" + `); + expect(() => { + eval(refreshGlobal); + }).not.toThrow(); + } + ); +}); diff --git a/test/unit/getResourceQuery.test.js b/test/unit/getResourceQuery.test.js index ff9bba6e..184497c7 100644 --- a/test/unit/getResourceQuery.test.js +++ b/test/unit/getResourceQuery.test.js @@ -1,18 +1,16 @@ const getResourceQuery = require('../../sockets/utils/getResourceQuery'); describe('getResourceQuery', () => { - let previousQuery; - beforeEach(() => { - previousQuery = global.__resourceQuery; + global.__resourceQuery = undefined; }); - afterEach(() => { - global.__resourceQuery = previousQuery; + afterAll(() => { + delete global.__resourceQuery; }); it('should parse __resourceQuery', () => { - global.__resourceQuery = '?sockHost=localhost&sockPort=8080&sockPath=/__socket'; + global.__resourceQuery = '?sockHost=localhost&sockPath=/__socket&sockPort=8080'; expect(getResourceQuery()).toStrictEqual({ sockHost: 'localhost', sockPath: '/__socket', diff --git a/test/unit/injectRefreshEntry.test.js b/test/unit/injectRefreshEntry.test.js new file mode 100644 index 00000000..ed5226ca --- /dev/null +++ b/test/unit/injectRefreshEntry.test.js @@ -0,0 +1,158 @@ +const injectRefreshEntry = require('../../lib/utils/injectRefreshEntry'); + +const ErrorOverlayEntry = require.resolve('../../client/ErrorOverlayEntry'); +const ReactRefreshEntry = require.resolve('../../client/ReactRefreshEntry'); + +const DEFAULT_OPTIONS = { + overlay: { + entry: ErrorOverlayEntry, + }, +}; + +describe('injectRefreshEntry', () => { + it('should add entries to a string', () => { + expect(injectRefreshEntry('test.js', DEFAULT_OPTIONS)).toStrictEqual([ + ReactRefreshEntry, + ErrorOverlayEntry, + 'test.js', + ]); + }); + + it('should add entries to an array', () => { + expect(injectRefreshEntry(['test.js'], DEFAULT_OPTIONS)).toStrictEqual([ + ReactRefreshEntry, + ErrorOverlayEntry, + 'test.js', + ]); + }); + + it('should add entries to an object', () => { + expect( + injectRefreshEntry( + { + main: 'test.js', + vendor: ['react', 'react-dom'], + }, + DEFAULT_OPTIONS + ) + ).toStrictEqual({ + main: [ReactRefreshEntry, ErrorOverlayEntry, 'test.js'], + vendor: [ReactRefreshEntry, ErrorOverlayEntry, 'react', 'react-dom'], + }); + }); + + it.skipIf( + WEBPACK_VERSION !== 5, + 'should add entries to an object using entry description', + () => { + expect( + injectRefreshEntry( + { + main: { + dependOn: 'vendors', + import: 'test.js', + }, + vendor: ['react', 'react-dom'], + }, + DEFAULT_OPTIONS + ) + ).toStrictEqual({ + main: { + dependOn: 'vendors', + import: [ReactRefreshEntry, ErrorOverlayEntry, 'test.js'], + }, + vendor: [ReactRefreshEntry, ErrorOverlayEntry, 'react', 'react-dom'], + }); + } + ); + + it('should add entries to a synchronous function', () => { + const returnedEntry = injectRefreshEntry(() => 'test.js', DEFAULT_OPTIONS); + expect(typeof returnedEntry).toBe('function'); + expect(returnedEntry()).resolves.toStrictEqual([ + ReactRefreshEntry, + ErrorOverlayEntry, + 'test.js', + ]); + }); + + it('should add entries to an asynchronous function', () => { + const returnedEntry = injectRefreshEntry(() => Promise.resolve('test.js'), DEFAULT_OPTIONS); + expect(typeof returnedEntry).toBe('function'); + expect(returnedEntry()).resolves.toStrictEqual([ + ReactRefreshEntry, + ErrorOverlayEntry, + 'test.js', + ]); + }); + + it('should add entries to a function returning an object', () => { + const returnedEntry = injectRefreshEntry( + () => ({ + main: 'test.js', + vendor: ['react', 'react-dom'], + }), + DEFAULT_OPTIONS + ); + expect(typeof returnedEntry).toBe('function'); + expect(returnedEntry()).resolves.toStrictEqual({ + main: [ReactRefreshEntry, ErrorOverlayEntry, 'test.js'], + vendor: [ReactRefreshEntry, ErrorOverlayEntry, 'react', 'react-dom'], + }); + }); + + it('should not append overlay entry when unused', () => { + expect(injectRefreshEntry('test.js', {})).toStrictEqual([ReactRefreshEntry, 'test.js']); + }); + + it('should append legacy WDS entry when required', () => { + expect( + injectRefreshEntry('test.js', { + ...DEFAULT_OPTIONS, + useLegacyWDSSockets: true, + }) + ).toStrictEqual([ + ReactRefreshEntry, + require.resolve('../../client/LegacyWDSSocketEntry'), + ErrorOverlayEntry, + 'test.js', + ]); + }); + + it('should append resource queries to the overlay entry when specified', () => { + expect( + injectRefreshEntry('test.js', { + overlay: { + entry: ErrorOverlayEntry, + sockHost: 'localhost', + sockPath: '/socket', + sockPort: '9000', + }, + }) + ).toStrictEqual([ + ReactRefreshEntry, + `${ErrorOverlayEntry}?sockHost=localhost&sockPath=/socket&sockPort=9000`, + 'test.js', + ]); + }); + + it('should append overlay entry for a string after socket-related entries', () => { + expect(injectRefreshEntry('webpack-dev-server/client', DEFAULT_OPTIONS)).toStrictEqual([ + ReactRefreshEntry, + 'webpack-dev-server/client', + ErrorOverlayEntry, + ]); + }); + + it('should append overlay entry for an array after socket-related entries', () => { + expect( + injectRefreshEntry(['webpack-dev-server/client', 'test.js'], DEFAULT_OPTIONS) + ).toStrictEqual([ReactRefreshEntry, 'webpack-dev-server/client', ErrorOverlayEntry, 'test.js']); + }); + + it('should throw when non-parsable entry is received', () => { + expect(() => injectRefreshEntry(1, DEFAULT_OPTIONS)).toThrowErrorMatchingInlineSnapshot( + '"Failed to parse the Webpack `entry` object!"' + ); + }); +}); diff --git a/test/unit/normalizeOptions.test.js b/test/unit/normalizeOptions.test.js new file mode 100644 index 00000000..3e732cf4 --- /dev/null +++ b/test/unit/normalizeOptions.test.js @@ -0,0 +1,76 @@ +const normalizeOptions = require('../../lib/utils/normalizeOptions'); + +const DEFAULT_OPTIONS = { + exclude: /node_modules/, + include: /\.([jt]sx?|flow)$/, + overlay: { + entry: require.resolve('../../client/ErrorOverlayEntry'), + module: require.resolve('../../overlay'), + sockIntegration: 'wds', + }, +}; + +describe('normalizeOptions', () => { + it('should return default options when an empty object is received', () => { + expect(normalizeOptions({})).toStrictEqual(DEFAULT_OPTIONS); + }); + + it('should return user options', () => { + expect( + normalizeOptions({ + exclude: 'exclude', + forceEnable: true, + include: 'include', + overlay: { + entry: 'entry', + module: 'overlay', + sockHost: 'localhost', + sockIntegration: 'whm', + sockPath: '/socket', + sockPort: 9000, + }, + useLegacyWDSSockets: true, + }) + ).toStrictEqual({ + exclude: 'exclude', + forceEnable: true, + include: 'include', + overlay: { + entry: 'entry', + module: 'overlay', + sockHost: 'localhost', + sockIntegration: 'whm', + sockPath: '/socket', + sockPort: 9000, + }, + useLegacyWDSSockets: true, + }); + }); + + // TODO: Remove when the deprecation warning is removed + it('should emit warning and exclude its value when disableRefreshCheck is used', () => { + jest.spyOn(console, 'warn').mockImplementationOnce(() => undefined); + + expect(normalizeOptions({ disableRefreshCheck: true })).toStrictEqual(DEFAULT_OPTIONS); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenLastCalledWith( + [ + 'The "disableRefreshCheck" option has been deprecated and will not have any effect on how the plugin parses files.', + 'Please remove it from your configuration.', + ].join(' ') + ); + + console.warn.mockRestore(); + }); + + it('should return default for overlay options when it is true', () => { + expect(normalizeOptions({ overlay: true })).toStrictEqual(DEFAULT_OPTIONS); + }); + + it('should return false for overlay options when it is false', () => { + expect(normalizeOptions({ overlay: false })).toStrictEqual({ + ...DEFAULT_OPTIONS, + overlay: false, + }); + }); +}); diff --git a/test/unit/validateOptions.test.js b/test/unit/validateOptions.test.js new file mode 100644 index 00000000..5d8e45f6 --- /dev/null +++ b/test/unit/validateOptions.test.js @@ -0,0 +1,375 @@ +const ReactRefreshPlugin = require('../../lib'); + +describe('validateOptions', () => { + it('should accept "exclude" when it is a RegExp', () => { + expect(() => { + new ReactRefreshPlugin({ exclude: /test/ }); + }).not.toThrow(); + }); + + it('should accept "exclude" when it is an absolute path string', () => { + expect(() => { + new ReactRefreshPlugin({ exclude: '/test' }); + }).not.toThrow(); + }); + + it('should accept "exclude" when it is an array of RegExp or absolute path strings', () => { + expect(() => { + new ReactRefreshPlugin({ exclude: [/test/, '/test'] }); + }).not.toThrow(); + }); + + it('should reject "exclude" when it is a string', () => { + expect(() => { + new ReactRefreshPlugin({ exclude: 'test' }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.exclude: The provided value \\"test\\" is not an absolute path!" + `); + }); + + it('should reject "exclude" when it is an object', () => { + expect(() => { + new ReactRefreshPlugin({ exclude: {} }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.exclude should be one of these: + RegExp | string | [RegExp | string, ...] (should not have fewer than 1 item) + Details: + * options.exclude should be one of these: + RegExp | string + Details: + * options.exclude should be an instance of RegExp. + * options.exclude should be a string. + * options.exclude should be an array: + [RegExp | string, ...] (should not have fewer than 1 item)" + `); + }); + + it('should accept "forceEnable" when it is true', () => { + expect(() => { + new ReactRefreshPlugin({ forceEnable: true }); + }).not.toThrow(); + }); + + it('should accept "forceEnable" when it is false', () => { + expect(() => { + new ReactRefreshPlugin({ forceEnable: false }); + }).not.toThrow(); + }); + + it('should reject "forceEnable" when it is not a boolean', () => { + expect(() => { + new ReactRefreshPlugin({ forceEnable: 1 }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.forceEnable should be a boolean." + `); + }); + + it('should accept "include" when it is a RegExp', () => { + expect(() => { + new ReactRefreshPlugin({ include: /test/ }); + }).not.toThrow(); + }); + + it('should accept "include" when it is an absolute path string', () => { + expect(() => { + new ReactRefreshPlugin({ include: '/test' }); + }).not.toThrow(); + }); + + it('should accept "include" when it is an array of RegExp or absolute path strings', () => { + expect(() => { + new ReactRefreshPlugin({ include: [/test/, '/test'] }); + }).not.toThrow(); + }); + + it('should reject "include" when it is a string', () => { + expect(() => { + new ReactRefreshPlugin({ include: 'test' }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.include: The provided value \\"test\\" is not an absolute path!" + `); + }); + + it('should reject "include" when it is an object', () => { + expect(() => { + new ReactRefreshPlugin({ include: {} }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.include should be one of these: + RegExp | string | [RegExp | string, ...] (should not have fewer than 1 item) + Details: + * options.include should be one of these: + RegExp | string + Details: + * options.include should be an instance of RegExp. + * options.include should be a string. + * options.include should be an array: + [RegExp | string, ...] (should not have fewer than 1 item)" + `); + }); + + it('should accept "overlay" when it is true', () => { + expect(() => { + new ReactRefreshPlugin({ overlay: true }); + }).not.toThrow(); + }); + + it('should accept "overlay" when it is false', () => { + expect(() => { + new ReactRefreshPlugin({ overlay: false }); + }).not.toThrow(); + }); + + it('should accept "overlay" when it is an empty object', () => { + expect(() => { + new ReactRefreshPlugin({ overlay: {} }); + }).not.toThrow(); + }); + + it('should reject "overlay" when it is not a boolean nor an object', () => { + expect(() => { + new ReactRefreshPlugin({ overlay: 'overlay' }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.overlay should be one of these: + boolean | object { entry?, module?, sockIntegration?, sockHost?, sockPath?, sockPort? } + Details: + * options.overlay should be a boolean. + * options.overlay should be an object: + object { entry?, module?, sockIntegration?, sockHost?, sockPath?, sockPort? }" + `); + }); + + it('should accept "overlay.entry" when it is an absolute path string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { entry: '/test' }, + }); + }).not.toThrow(); + }); + + it('should reject "overlay.entry" when it is a string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { entry: 'test' }, + }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.overlay.entry: The provided value \\"test\\" is not an absolute path!" + `); + }); + + it('should reject "overlay.entry" when it is not a string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { entry: true }, + }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.overlay.entry should be a string." + `); + }); + + it('should accept "overlay.module" when it is an absolute path string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { module: '/test' }, + }); + }).not.toThrow(); + }); + + it('should reject "overlay.module" when it is a string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { module: 'test' }, + }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.overlay.module: The provided value \\"test\\" is not an absolute path!" + `); + }); + + it('should reject "overlay.module" when it is not a string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { module: true }, + }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.overlay.module should be a string." + `); + }); + + it('should accept "overlay.sockIntegration" when it is "wds"', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { sockIntegration: 'wds' }, + }); + }).not.toThrow(); + }); + + it('should accept "overlay.sockIntegration" when it is "whm"', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { sockIntegration: 'whm' }, + }); + }).not.toThrow(); + }); + + it('should accept "overlay.sockIntegration" when it is "wps"', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { sockIntegration: 'wps' }, + }); + }).not.toThrow(); + }); + + it('should accept "overlay.sockIntegration" when it is an absolute path string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { sockIntegration: '/test' }, + }); + }).not.toThrow(); + }); + + it('should reject "overlay.sockIntegration" when it is a string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { sockIntegration: 'test' }, + }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.overlay.sockIntegration: The provided value \\"test\\" is not an absolute path!" + `); + }); + + it('should reject "overlay.sockIntegration" when it is not a string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { sockIntegration: true }, + }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.overlay should be one of these: + boolean | object { entry?, module?, sockIntegration?, sockHost?, sockPath?, sockPort? } + Details: + * options.overlay.sockIntegration should be one of these: + \\"wds\\" | \\"whm\\" | \\"wps\\" | string + Details: + * options.overlay.sockIntegration should be one of these: + \\"wds\\" | \\"whm\\" | \\"wps\\" + * options.overlay.sockIntegration should be a string." + `); + }); + + it('should accept "overlay.sockHost" when it is a string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { sockHost: 'test' }, + }); + }).not.toThrow(); + }); + + it('should reject "overlay.sockHost" when it is not a string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { sockHost: true }, + }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.overlay.sockHost should be a string." + `); + }); + + it('should accept "overlay.sockPath" when it is a string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { sockPath: 'test' }, + }); + }).not.toThrow(); + }); + + it('should reject "overlay.sockPath" when it is not a string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { sockPath: true }, + }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.overlay.sockPath should be a string." + `); + }); + + it('should accept "overlay.sockPort" when it is 0', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { sockPort: 0 }, + }); + }).not.toThrow(); + }); + + it('should accept "overlay.sockPort" when it is a positive number', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { sockPort: 1 }, + }); + }).not.toThrow(); + }); + + it('should reject "overlay.sockPort" when it is a negative number', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { sockPort: -1 }, + }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.overlay.sockPort should be >= 0." + `); + }); + + it('should reject "overlay.sockPort" when it is not a number', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { sockPort: '1' }, + }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.overlay.sockPort should be a number (should be >= 0)." + `); + }); + + it('should accept "useLegacyWDSSockets" when it is true', () => { + expect(() => { + new ReactRefreshPlugin({ useLegacyWDSSockets: true }); + }).not.toThrow(); + }); + + it('should accept "useLegacyWDSSockets" when it is false', () => { + expect(() => { + new ReactRefreshPlugin({ useLegacyWDSSockets: false }); + }).not.toThrow(); + }); + + it('should reject "useLegacyWDSSockets" when it is not a boolean', () => { + expect(() => { + new ReactRefreshPlugin({ useLegacyWDSSockets: 1 }); + }).toThrowErrorMatchingInlineSnapshot(` + "Invalid options object. React Refresh Plugin has been initialized using an options object that does not match the API schema. + - options.useLegacyWDSSockets should be a boolean." + `); + }); + + it('should reject any unknown options', () => { + expect(() => { + new ReactRefreshPlugin({ unknown: true }); + }).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 { exclude?, forceEnable?, include?, overlay?, useLegacyWDSSockets? }" + `); + }); +});