From 71ed347381b6f24711f1f7e0e2f65d78bb732bd8 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Thu, 25 Jun 2020 00:56:02 +0800 Subject: [PATCH 01/20] test: move sandbox files and rename createSandbox to getSandbox --- test/conformance/ReactRefreshRequire.test.js | 34 +++++++++---------- test/{ => helpers}/sandbox/configs.js | 2 +- .../sandbox/fixtures/hmr-notifier.js} | 0 test/{ => helpers}/sandbox/index.js | 6 ++-- test/{ => helpers}/sandbox/spawn.js | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) rename test/{ => helpers}/sandbox/configs.js (95%) rename test/{sandbox/runtime/hot-notifier.js => helpers/sandbox/fixtures/hmr-notifier.js} (100%) rename test/{ => helpers}/sandbox/index.js (97%) rename test/{ => helpers}/sandbox/spawn.js (98%) 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/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..7ad1a2ec 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 97% rename from test/sandbox/index.js rename to test/helpers/sandbox/index.js index 5366c615..a2c988d9 100644 --- a/test/sandbox/index.js +++ b/test/helpers/sandbox/index.js @@ -51,7 +51,7 @@ const sleep = (ms) => { * @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 @@ -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', From 416716d540f5ca38a75d00f3f772fbcb231b3293 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Thu, 25 Jun 2020 00:57:14 +0800 Subject: [PATCH 02/20] test: add compilation setup to test the loader --- .../compilation/fixtures/source-map-loader.js | 4 + test/helpers/compilation/index.js | 158 ++++++++++++++++++ test/helpers/compilation/normalizeErrors.js | 18 ++ 3 files changed, 180 insertions(+) create mode 100644 test/helpers/compilation/fixtures/source-map-loader.js create mode 100644 test/helpers/compilation/index.js create mode 100644 test/helpers/compilation/normalizeErrors.js 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..9ea7ec9b --- /dev/null +++ b/test/helpers/compilation/index.js @@ -0,0 +1,158 @@ +const path = require('path'); +const { createFsFromVolume, Volume } = require('memfs'); +const webpack = require('webpack'); +const normalizeErrors = require('./normalizeErrors'); + +/** @type {Set>} */ +const cleanupHandlers = new Set(); +afterEach(async () => { + await Promise.all([...cleanupHandlers].map((callback) => callback())); +}); + +const BUNDLE_FILENAME = 'main'; +const CONTEXT_PATH = path.join(__dirname, '../../loader/fixtures'); +const OUTPUT_PATH = path.join(__dirname, 'dist'); + +/** + * Gets a Webpack compiler instance to test loader operations. + * @param {string} fixtureFile + * @param {Object} [options] + * @param {boolean} [options.devtool] + * @param {*} [options.prevSourceMap] + * @returns {import('webpack').Compiler} + */ +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()], + optimization: { + runtimeChunk: 'single', + splitChunks: { + chunks: 'all', + }, + }, + }); + + if (!compiler.outputFileSystem) { + compiler.outputFileSystem = createFsFromVolume(new Volume()); + if (WEBPACK_VERSION !== 5) { + compiler.outputFileSystem.join = path.join.bind(path); + } + } + + function cleanupCompilation() { + return new Promise((resolve) => { + compiler.close(() => { + cleanupHandlers.delete(cleanupCompilation); + resolve(); + }); + }); + } + + cleanupHandlers.add(cleanupCompilation); + + const compilerOutputFs = compiler.outputFileSystem; + let compilationStats; + + return [ + { + get errors() { + return normalizeErrors(compilationStats.compilation.errors); + }, + get warnings() { + return normalizeErrors(compilationStats.compilation.errors); + }, + get module() { + if (!compilationStats) { + throw new Error('Compilation stats data is not available!'); + } + + 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(); + } + + 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, + }; + }, + async run() { + return new Promise((resolve, reject) => { + compiler.run((error, stats) => { + if (error) { + reject(error); + return; + } + + if (stats.hasErrors()) { + reject(stats.toJson().errors); + return; + } + + compilationStats = stats; + resolve(stats.toJson({ source: true })); + }); + }); + }, + }, + cleanupCompilation, + ]; +} + +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; From 7c8bf697a671c51d9beb008da1e0f048b65861b8 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Thu, 25 Jun 2020 01:00:30 +0800 Subject: [PATCH 03/20] test: add loader tests and fixtures --- test/loader/fixtures/index.cjs.js | 1 + test/loader/fixtures/index.esm.js | 1 + test/loader/loader.test.js | 437 ++++++++++++++++++++++++++++++ 3 files changed, 439 insertions(+) create mode 100644 test/loader/fixtures/index.cjs.js create mode 100644 test/loader/fixtures/index.esm.js create mode 100644 test/loader/loader.test.js 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..33f77686 --- /dev/null +++ b/test/loader/loader.test.js @@ -0,0 +1,437 @@ +const validate = require('sourcemap-validator'); +const getCompilation = require('../helpers/compilation'); + +describe('loader', () => { + it('should work for CommonJS', async () => { + const [compilation] = getCompilation('./index.cjs.js'); + await compilation.run(); + + 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\\",\\"vendors-node_modules_react-refresh_runtime_js\\"]]]);" + `); + + expect(compilation.errors).toStrictEqual([]); + expect(compilation.warnings).toStrictEqual([]); + }); + + it('should work for ES Modules', async () => { + const [compilation] = getCompilation('./index.esm.js'); + await compilation.run(); + + 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\\",\\"vendors-node_modules_react-refresh_runtime_js\\"]]]);" + `); + + expect(compilation.errors).toStrictEqual([]); + expect(compilation.warnings).toStrictEqual([]); + }); + + it('should generate valid source map when the `devtool` option is specified', async () => { + const [compilation] = getCompilation('./index.cjs.js', { devtool: 'source-map' }); + await compilation.run(); + + 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] = getCompilation('./index.cjs.js', { + devtool: 'source-map', + prevSourceMap: undefined, + }); + await compilation.run(); + + 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] = getCompilation('./index.cjs.js', { + devtool: 'source-map', + prevSourceMap: null, + }); + await compilation.run(); + + 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] = 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"], + }), + }); + await compilation.run(); + + 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] = getCompilation('./index.cjs.js', { + devtool: 'source-map', + prevSourceMap: { + version: 3, + sources: ['./index.cjs.js'], + names: [], + mappings: 'AAAA;AACA', + sourcesContent: ["module.exports = 'Test';\n"], + }, + }); + await compilation.run(); + + const { execution, sourceMap } = compilation.module; + expect(() => { + validate(execution, sourceMap); + }).not.toThrow(); + }); +}); From a44d7e06e0b0b9ea506d800c619f19e80ca3df79 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Thu, 25 Jun 2020 02:20:06 +0800 Subject: [PATCH 04/20] test: add packages for loader tests --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index dae9ad9b..e410b800 100644 --- a/package.json +++ b/package.json @@ -74,11 +74,13 @@ "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", "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", From 7ee3f985af59e69d394c48a04f6b32ef512aa00c Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Thu, 25 Jun 2020 02:20:48 +0800 Subject: [PATCH 05/20] test: add skipIf test helper --- jest.config.js | 2 +- test/jest-test-setup.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 test/jest-test-setup.js 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/test/jest-test-setup.js b/test/jest-test-setup.js new file mode 100644 index 00000000..0139c753 --- /dev/null +++ b/test/jest-test-setup.js @@ -0,0 +1,16 @@ +/** + * 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; From 9e2f02de406c6d92c2965d88a617694aba44522c Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Thu, 25 Jun 2020 02:31:03 +0800 Subject: [PATCH 06/20] test: add unit tests for utility functions --- loader/index.js | 6 ++ test/unit/getRefreshGlobal.test.js | 80 ++++++++++++++ test/unit/getResourceQuery.test.js | 10 +- test/unit/injectRefreshEntry.test.js | 154 +++++++++++++++++++++++++++ 4 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 test/unit/getRefreshGlobal.test.js create mode 100644 test/unit/injectRefreshEntry.test.js 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/test/unit/getRefreshGlobal.test.js b/test/unit/getRefreshGlobal.test.js new file mode 100644 index 00000000..f2e7b2cd --- /dev/null +++ b/test/unit/getRefreshGlobal.test.js @@ -0,0 +1,80 @@ +const getRefreshGlobal = require('../../lib/utils/getRefreshGlobal'); + +describe('getRefreshGlobal', () => { + beforeEach(() => { + global.__webpack_require__ = {}; + }); + + afterAll(() => { + delete global.__webpack_require__; + }); + + it('should return 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 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..f74e7751 --- /dev/null +++ b/test/unit/injectRefreshEntry.test.js @@ -0,0 +1,154 @@ +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('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!"' + ); + }); +}); From a892c48459c7699f3835c24c1f97fff5db5f86d5 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Thu, 25 Jun 2020 02:31:36 +0800 Subject: [PATCH 07/20] chore: fix linting in tests --- .eslintrc.json | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index e061df24..2ae0b09d 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/helpers/**/fixtures/*.esm.js"], + "parserOptions": { + "ecmaVersion": 2015, + "sourceType": "module" + }, + "env": { + "commonjs": false, + "es6": true + } } ] } From c19fe98f840b371f8266660a81914e72b8cb4f99 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Thu, 25 Jun 2020 02:47:59 +0800 Subject: [PATCH 08/20] test: use memfs unconditionally for compilation --- test/helpers/compilation/index.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/helpers/compilation/index.js b/test/helpers/compilation/index.js index 9ea7ec9b..819ee575 100644 --- a/test/helpers/compilation/index.js +++ b/test/helpers/compilation/index.js @@ -59,11 +59,9 @@ function getCompilation(fixtureFile, options = {}) { }, }); - if (!compiler.outputFileSystem) { - compiler.outputFileSystem = createFsFromVolume(new Volume()); - if (WEBPACK_VERSION !== 5) { - compiler.outputFileSystem.join = path.join.bind(path); - } + compiler.outputFileSystem = createFsFromVolume(new Volume()); + if (WEBPACK_VERSION !== 5) { + compiler.outputFileSystem.join = path.join.bind(path); } function cleanupCompilation() { From d8a7948bba4594973ef88b991b65c8651ae98a89 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Thu, 25 Jun 2020 17:00:03 +0800 Subject: [PATCH 09/20] docs: fix JSDoc function syntax --- client/LegacyWDSSocketEntry.js | 2 +- client/errorEventHandlers.js | 2 +- lib/index.js | 7 ++++++- lib/utils/getParserHelpers.js | 4 ++-- lib/utils/getRefreshGlobal.js | 4 ++-- lib/utils/normalizeOptions.js | 2 +- overlay/components/RuntimeErrorFooter.js | 6 +++--- sockets/WDSSocket.js | 2 +- sockets/WHMEventSource.js | 2 +- sockets/WPSSocket.js | 2 +- test/helpers/sandbox/index.js | 8 ++++---- 11 files changed, 23 insertions(+), 18 deletions(-) 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/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/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..bcfc5fa1 100644 --- a/lib/utils/normalizeOptions.js +++ b/lib/utils/normalizeOptions.js @@ -16,7 +16,7 @@ const d = (object, property, defaultValue) => { * @template T * @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 {function(T | undefined): Result} fn The handler to resolve the option's value. * @returns {Result} The resolved option value. */ const nestedOption = (value, fn) => { 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/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/helpers/sandbox/index.js b/test/helpers/sandbox/index.js index a2c988d9..d6cd99fd 100644 --- a/test/helpers/sandbox/index.js +++ b/test/helpers/sandbox/index.js @@ -44,10 +44,10 @@ 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 */ From cc5a0f44690f0cace72389706b123936a8698944 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Fri, 26 Jun 2020 04:32:09 +0800 Subject: [PATCH 10/20] test: add describe.skipIf helper --- test/jest-test-setup.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/jest-test-setup.js b/test/jest-test-setup.js index 0139c753..a0ad5165 100644 --- a/test/jest-test-setup.js +++ b/test/jest-test-setup.js @@ -1,10 +1,24 @@ +/** + * 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|*} + * @return {void} */ test.skipIf = (condition, testName, fn, timeout) => { if (condition) { From d48fd1bab04c4b6381a7ae2f54ca1e10cfec628c Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Fri, 26 Jun 2020 04:32:45 +0800 Subject: [PATCH 11/20] test: cleanup getCompilation --- test/helpers/compilation/index.js | 168 +++++++++++++++--------------- test/helpers/sandbox/index.js | 6 +- 2 files changed, 86 insertions(+), 88 deletions(-) diff --git a/test/helpers/compilation/index.js b/test/helpers/compilation/index.js index 819ee575..0b6aeec7 100644 --- a/test/helpers/compilation/index.js +++ b/test/helpers/compilation/index.js @@ -3,25 +3,33 @@ const { createFsFromVolume, Volume } = require('memfs'); const webpack = require('webpack'); const normalizeErrors = require('./normalizeErrors'); -/** @type {Set>} */ -const cleanupHandlers = new Set(); -afterEach(async () => { - await Promise.all([...cleanupHandlers].map((callback) => callback())); -}); - 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 {import('webpack').Compiler} + * @returns {CompilationSession} */ -function getCompilation(fixtureFile, options = {}) { +async function getCompilation(fixtureFile, options = {}) { const compiler = webpack({ mode: 'development', context: CONTEXT_PATH, @@ -55,6 +63,7 @@ function getCompilation(fixtureFile, options = {}) { runtimeChunk: 'single', splitChunks: { chunks: 'all', + name: (module, chunks, cacheGroupKey) => cacheGroupKey, }, }, }); @@ -64,93 +73,82 @@ function getCompilation(fixtureFile, options = {}) { compiler.outputFileSystem.join = path.join.bind(path); } - function cleanupCompilation() { - return new Promise((resolve) => { - compiler.close(() => { - cleanupHandlers.delete(cleanupCompilation); - resolve(); - }); - }); - } - - cleanupHandlers.add(cleanupCompilation); - + /** @type {import('memfs').IFs} */ const compilerOutputFs = compiler.outputFileSystem; + /** @type {import('webpack').Stats | undefined} */ let compilationStats; - return [ - { - get errors() { - return normalizeErrors(compilationStats.compilation.errors); - }, - get warnings() { - return normalizeErrors(compilationStats.compilation.errors); - }, - get module() { - if (!compilationStats) { - throw new Error('Compilation stats data is not available!'); - } + await new Promise((resolve, reject) => { + compiler.run((error, stats) => { + if (error) { + reject(error); + return; + } - const parsed = compilationStats - .toJson({ source: true }) - .modules.find(({ name }) => name === fixtureFile); - if (!parsed) { - throw new Error('Fixture module is not found in compilation stats!'); - } + compilationStats = stats; - let execution; - try { - execution = compilerOutputFs - .readFileSync(path.join(OUTPUT_PATH, `${BUNDLE_FILENAME}.js`)) - .toString(); - } catch (error) { - execution = error.toString(); - } + if (WEBPACK_VERSION !== 5) { + resolve(); + } else { + compiler.close(() => { + resolve(); + }); + } + }); + }); - 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 { + /** @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!'); + } - return { - parsed: parsed.source, - execution, - sourceMap, - }; - }, - async run() { - return new Promise((resolve, reject) => { - compiler.run((error, stats) => { - if (error) { - reject(error); - return; - } + let execution; + try { + execution = compilerOutputFs + .readFileSync(path.join(OUTPUT_PATH, `${BUNDLE_FILENAME}.js`)) + .toString(); + } catch (error) { + execution = error.toString(); + } - if (stats.hasErrors()) { - reject(stats.toJson().errors); - return; - } + /** @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(); + } + } - compilationStats = stats; - resolve(stats.toJson({ source: true })); - }); - }); - }, + return { + parsed: parsed.source, + execution, + sourceMap, + }; }, - cleanupCompilation, - ]; + }; } module.exports = getCompilation; diff --git a/test/helpers/sandbox/index.js b/test/helpers/sandbox/index.js index d6cd99fd..6fa17f2a 100644 --- a/test/helpers/sandbox/index.js +++ b/test/helpers/sandbox/index.js @@ -146,15 +146,15 @@ async function getSandbox({ 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; }, From dea92ba8809ab312427686dcc772ecdb71f32dfe Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Fri, 26 Jun 2020 04:33:00 +0800 Subject: [PATCH 12/20] test: update loader tests to check for webpack 4 and 5 --- test/loader/loader.test.js | 1057 ++++++++++++++++++++++++------------ 1 file changed, 708 insertions(+), 349 deletions(-) diff --git a/test/loader/loader.test.js b/test/loader/loader.test.js index 33f77686..1e96284f 100644 --- a/test/loader/loader.test.js +++ b/test/loader/loader.test.js @@ -2,403 +2,764 @@ const validate = require('sourcemap-validator'); const getCompilation = require('../helpers/compilation'); describe('loader', () => { - it('should work for CommonJS', async () => { - const [compilation] = getCompilation('./index.cjs.js'); - await compilation.run(); - - 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); + describe.skipIf(WEBPACK_VERSION === 5, '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(); + } + } } - } - __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); - } - } + },[[\\"./index.cjs.js\\",\\"runtime\\",\\"vendors\\"]]]);" + `); - __webpack_require__.c[module.id].hot.accept(hotErrorHandler); - }); + expect(compilation.errors).toStrictEqual([]); + expect(compilation.warnings).toStrictEqual([]); + }); - 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(); + 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(); + } } - }); - } - } - } 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.cjs.js\\",\\"runtime\\",\\"vendors-node_modules_react-refresh_runtime_js\\"]]]);" - `); + },[[\\"./index.esm.js\\",\\"runtime\\",\\"vendors\\"]]]);" + `); - expect(compilation.errors).toStrictEqual([]); - expect(compilation.warnings).toStrictEqual([]); - }); + expect(compilation.errors).toStrictEqual([]); + expect(compilation.warnings).toStrictEqual([]); + }); - it('should work for ES Modules', async () => { - const [compilation] = getCompilation('./index.esm.js'); - await compilation.run(); - - 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); - } + 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(); + }); + }); - if (isTestMode) { - if (window.onHotAcceptError) { - window.onHotAcceptError(error.message); + describe.skipIf(WEBPACK_VERSION === 4, '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); } - } - __webpack_require__.c[module.id].hot.accept(hotErrorHandler); - }); + if (isTestMode) { + if (window.onHotAcceptError) { + window.onHotAcceptError(error.message); + } + } - if (isHotUpdate) { - if (__react_refresh_utils__.isReactRefreshBoundary(prevExports) && __react_refresh_utils__.shouldInvalidateReactRefreshBoundary(prevExports, currentExports)) { + __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(); - } 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); + /***/ }) + + },[[\\"./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); } - } - __webpack_require__.c[module.id].hot.accept(hotErrorHandler); - }); + if (isTestMode) { + if (window.onHotAcceptError) { + window.onHotAcceptError(error.message); + } + } - if (isHotUpdate) { - if (__react_refresh_utils__.isReactRefreshBoundary(prevExports) && __react_refresh_utils__.shouldInvalidateReactRefreshBoundary(prevExports, currentExports)) { + __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(); - } 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-node_modules_react-refresh_runtime_js\\"]]]);" - `); + },[[\\"./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] = getCompilation('./index.cjs.js', { devtool: 'source-map' }); - await compilation.run(); + expect(compilation.errors).toStrictEqual([]); + expect(compilation.warnings).toStrictEqual([]); + }); - 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 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] = getCompilation('./index.cjs.js', { + const compilation = await getCompilation('./index.cjs.js', { devtool: 'source-map', prevSourceMap: undefined, }); - await compilation.run(); - 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] = getCompilation('./index.cjs.js', { + const compilation = await getCompilation('./index.cjs.js', { devtool: 'source-map', prevSourceMap: null, }); - await compilation.run(); - 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] = getCompilation('./index.cjs.js', { + const compilation = await getCompilation('./index.cjs.js', { devtool: 'source-map', prevSourceMap: JSON.stringify({ version: 3, @@ -408,16 +769,15 @@ describe('loader', () => { sourcesContent: ["module.exports = 'Test';\n"], }), }); - await compilation.run(); - 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] = getCompilation('./index.cjs.js', { + const compilation = await getCompilation('./index.cjs.js', { devtool: 'source-map', prevSourceMap: { version: 3, @@ -427,9 +787,8 @@ describe('loader', () => { sourcesContent: ["module.exports = 'Test';\n"], }, }); - await compilation.run(); - const { execution, sourceMap } = compilation.module; + expect(() => { validate(execution, sourceMap); }).not.toThrow(); From c1bc129064d46e7e5951b2b7973fdf11da552e46 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Sat, 27 Jun 2020 02:03:47 +0800 Subject: [PATCH 13/20] chore: fix linting for fixtures --- .eslintrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index 2ae0b09d..dbfebfc4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -47,7 +47,7 @@ } }, { - "files": ["test/helpers/**/fixtures/*.esm.js"], + "files": ["test/**/fixtures/*.esm.js"], "parserOptions": { "ecmaVersion": 2015, "sourceType": "module" From a9e2db0e219dfd9aeaa92439d723d0095b635a88 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Sun, 28 Jun 2020 01:45:32 +0800 Subject: [PATCH 14/20] refactor: cleanup undefined checks --- lib/runtime/RefreshUtils.js | 4 ++-- lib/utils/normalizeOptions.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) 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/normalizeOptions.js b/lib/utils/normalizeOptions.js index bcfc5fa1..13084e7d 100644 --- a/lib/utils/normalizeOptions.js +++ b/lib/utils/normalizeOptions.js @@ -8,7 +8,7 @@ * @returns {T[Property]} */ const d = (object, property, defaultValue) => { - return object[property] === undefined ? defaultValue : object[property]; + return typeof object[property] === 'undefined' ? defaultValue : object[property]; }; /** @@ -20,7 +20,7 @@ const d = (object, property, defaultValue) => { * @returns {Result} The resolved option value. */ const nestedOption = (value, fn) => { - return fn(value === undefined ? {} : value); + return fn(typeof value === 'undefined' ? {} : value); }; /** @@ -29,7 +29,7 @@ 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 if (typeof options.disableRefreshCheck !== 'undefined') { console.warn( [ From 89723208f74b98496f123e69585850bb08c1cc5d Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Sun, 28 Jun 2020 03:14:25 +0800 Subject: [PATCH 15/20] test: add tests for normalizeOptions --- test/unit/normalizeOptions.test.js | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 test/unit/normalizeOptions.test.js 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, + }); + }); +}); From c8102175a03a4f689b58cf32882f9e26db9de726 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Sun, 28 Jun 2020 03:14:51 +0800 Subject: [PATCH 16/20] refactor: update impl of normalizeOptions to not add unwanted keys --- lib/utils/normalizeOptions.js | 78 +++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/lib/utils/normalizeOptions.js b/lib/utils/normalizeOptions.js index 13084e7d..381b6ccd 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 typeof 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(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(typeof 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 for the `disableRefreshCheck` option + // 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, 'sockHost'); + d(overlay, 'sockIntegration', defaults.sockIntegration); + 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; From d942b8067e677dd6da3a00837313c091f22c163a Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Mon, 29 Jun 2020 02:33:54 +0800 Subject: [PATCH 17/20] test: add tests for options schema validation --- test/unit/validateOptions.test.js | 334 ++++++++++++++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 test/unit/validateOptions.test.js diff --git a/test/unit/validateOptions.test.js b/test/unit/validateOptions.test.js new file mode 100644 index 00000000..4d5d8949 --- /dev/null +++ b/test/unit/validateOptions.test.js @@ -0,0 +1,334 @@ +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 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 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 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? }" + `); + }); +}); From eb379bb325f561c89eae4fa85219534bf6bc51dd Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Mon, 29 Jun 2020 02:34:58 +0800 Subject: [PATCH 18/20] chore: add patch to handle jest-snapshot bug #10217 --- package.json | 3 ++- patches/jest-snapshot+26.1.0.patch | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 patches/jest-snapshot+26.1.0.patch diff --git a/package.json b/package.json index e410b800..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 .", @@ -76,6 +76,7 @@ "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", 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 From 23f22081827ebbbd84c64a4962534321d4c64f0a Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Mon, 29 Jun 2020 03:05:51 +0800 Subject: [PATCH 19/20] test: update tests and add cases for consistency --- lib/utils/normalizeOptions.js | 2 +- test/helpers/compilation/index.js | 9 +++++-- test/helpers/sandbox/configs.js | 2 +- test/loader/loader.test.js | 12 ++++----- test/unit/getRefreshGlobal.test.js | 26 ++++++++++-------- test/unit/injectRefreshEntry.test.js | 40 +++++++++++++++------------- test/unit/validateOptions.test.js | 26 +++++++++++++++++- 7 files changed, 77 insertions(+), 40 deletions(-) diff --git a/lib/utils/normalizeOptions.js b/lib/utils/normalizeOptions.js index 381b6ccd..136aaa01 100644 --- a/lib/utils/normalizeOptions.js +++ b/lib/utils/normalizeOptions.js @@ -68,8 +68,8 @@ const normalizeOptions = (options) => { d(overlay, 'entry', defaults.entry); d(overlay, 'module', defaults.module); - d(overlay, 'sockHost'); d(overlay, 'sockIntegration', defaults.sockIntegration); + d(overlay, 'sockHost'); d(overlay, 'sockPath'); d(overlay, 'sockPort'); diff --git a/test/helpers/compilation/index.js b/test/helpers/compilation/index.js index 0b6aeec7..99977a15 100644 --- a/test/helpers/compilation/index.js +++ b/test/helpers/compilation/index.js @@ -4,7 +4,7 @@ const webpack = require('webpack'); const normalizeErrors = require('./normalizeErrors'); const BUNDLE_FILENAME = 'main'; -const CONTEXT_PATH = path.join(__dirname, '../../loader/fixtures'); +const CONTEXT_PATH = path.join(__dirname, '../..', 'loader/fixtures'); const OUTPUT_PATH = path.join(__dirname, 'dist'); /** @@ -49,7 +49,7 @@ async function getCompilation(fixtureFile, options = {}) { 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'), + loader: path.join(__dirname, 'fixtures/source-map-loader.js'), options: { sourceMap: options.prevSourceMap, }, @@ -59,6 +59,9 @@ async function getCompilation(fixtureFile, options = {}) { ], }, 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: { @@ -68,6 +71,7 @@ async function getCompilation(fixtureFile, options = {}) { }, }); + // 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); @@ -90,6 +94,7 @@ async function getCompilation(fixtureFile, options = {}) { if (WEBPACK_VERSION !== 5) { resolve(); } else { + // The compiler have to be explicitly closed in Webpack 5 compiler.close(() => { resolve(); }); diff --git a/test/helpers/sandbox/configs.js b/test/helpers/sandbox/configs.js index 7ad1a2ec..ba0d0dd2 100644 --- a/test/helpers/sandbox/configs.js +++ b/test/helpers/sandbox/configs.js @@ -37,7 +37,7 @@ module.exports = { devtool: false, entry: { '${BUNDLE_FILENAME}': [ - '${path.join(__dirname, './fixtures/hmr-notifier.js')}', + '${path.join(__dirname, 'fixtures/hmr-notifier.js')}', './index.js', ], }, diff --git a/test/loader/loader.test.js b/test/loader/loader.test.js index 1e96284f..6211ef53 100644 --- a/test/loader/loader.test.js +++ b/test/loader/loader.test.js @@ -2,7 +2,7 @@ const validate = require('sourcemap-validator'); const getCompilation = require('../helpers/compilation'); describe('loader', () => { - describe.skipIf(WEBPACK_VERSION === 5, 'on Webpack 4', () => { + 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; @@ -341,7 +341,7 @@ describe('loader', () => { expect(compilation.warnings).toStrictEqual([]); }); - it('should generate valid source map when the `devtool` option is specified', async () => { + 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; @@ -366,7 +366,7 @@ describe('loader', () => { }); }); - describe.skipIf(WEBPACK_VERSION === 4, 'on Webpack 5', () => { + 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; @@ -709,7 +709,7 @@ describe('loader', () => { expect(compilation.warnings).toStrictEqual([]); }); - it('should generate valid source map when the `devtool` option is specified', async () => { + 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; @@ -734,7 +734,7 @@ describe('loader', () => { }); }); - it('should generate valid source map when `undefined` source map is provided', async () => { + 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, @@ -746,7 +746,7 @@ describe('loader', () => { }).not.toThrow(); }); - it('should generate valid source map when `null` source map is provided', async () => { + 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, diff --git a/test/unit/getRefreshGlobal.test.js b/test/unit/getRefreshGlobal.test.js index f2e7b2cd..3ceb3f3d 100644 --- a/test/unit/getRefreshGlobal.test.js +++ b/test/unit/getRefreshGlobal.test.js @@ -9,7 +9,7 @@ describe('getRefreshGlobal', () => { delete global.__webpack_require__; }); - it('should return template without providing runtime template', () => { + it('should return refresh global template without providing runtime template', () => { const refreshGlobal = getRefreshGlobal(); expect(refreshGlobal).toMatchInlineSnapshot(` "__webpack_require__.$Refresh$ = { @@ -42,12 +42,15 @@ describe('getRefreshGlobal', () => { }).not.toThrow(); }); - it.skipIf(WEBPACK_VERSION !== 5, 'should return template with provided runtime template', () => { - const RuntimeTemplate = require('webpack/lib/RuntimeTemplate'); - const refreshGlobal = getRefreshGlobal( - new RuntimeTemplate({ ecmaVersion: 6 }, { shorten: (item) => item }) - ); - expect(refreshGlobal).toMatchInlineSnapshot(` + 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, @@ -73,8 +76,9 @@ describe('getRefreshGlobal', () => { signature: () => (type) => type };" `); - expect(() => { - eval(refreshGlobal); - }).not.toThrow(); - }); + expect(() => { + eval(refreshGlobal); + }).not.toThrow(); + } + ); }); diff --git a/test/unit/injectRefreshEntry.test.js b/test/unit/injectRefreshEntry.test.js index f74e7751..ed5226ca 100644 --- a/test/unit/injectRefreshEntry.test.js +++ b/test/unit/injectRefreshEntry.test.js @@ -41,26 +41,30 @@ describe('injectRefreshEntry', () => { }); }); - it('should add entries to an object using entry description', () => { - expect( - injectRefreshEntry( - { - main: { - dependOn: 'vendors', - import: 'test.js', + 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'], }, - vendor: ['react', 'react-dom'], + DEFAULT_OPTIONS + ) + ).toStrictEqual({ + main: { + dependOn: 'vendors', + import: [ReactRefreshEntry, ErrorOverlayEntry, 'test.js'], }, - DEFAULT_OPTIONS - ) - ).toStrictEqual({ - main: { - dependOn: 'vendors', - import: [ReactRefreshEntry, ErrorOverlayEntry, 'test.js'], - }, - vendor: [ReactRefreshEntry, ErrorOverlayEntry, 'react', 'react-dom'], - }); - }); + vendor: [ReactRefreshEntry, ErrorOverlayEntry, 'react', 'react-dom'], + }); + } + ); it('should add entries to a synchronous function', () => { const returnedEntry = injectRefreshEntry(() => 'test.js', DEFAULT_OPTIONS); diff --git a/test/unit/validateOptions.test.js b/test/unit/validateOptions.test.js index 4d5d8949..aff79edb 100644 --- a/test/unit/validateOptions.test.js +++ b/test/unit/validateOptions.test.js @@ -163,6 +163,14 @@ describe('validateOptions', () => { `); }); + it('should reject "overlay.entry" when it is not a string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { entry: true }, + }); + }).toThrowErrorMatchingInlineSnapshot(); + }); + it('should accept "overlay.module" when it is an absolute path string', () => { expect(() => { new ReactRefreshPlugin({ @@ -182,6 +190,14 @@ describe('validateOptions', () => { `); }); + it('should reject "overlay.module" when it is not a string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { module: true }, + }); + }).toThrowErrorMatchingInlineSnapshot(); + }); + it('should accept "overlay.sockIntegration" when it is "wds"', () => { expect(() => { new ReactRefreshPlugin({ @@ -193,7 +209,7 @@ describe('validateOptions', () => { it('should accept "overlay.sockIntegration" when it is "whm"', () => { expect(() => { new ReactRefreshPlugin({ - overlay: { sockIntegration: '/whm' }, + overlay: { sockIntegration: 'whm' }, }); }).not.toThrow(); }); @@ -225,6 +241,14 @@ describe('validateOptions', () => { `); }); + it('should reject "overlay.sockIntegration" when it is not a string', () => { + expect(() => { + new ReactRefreshPlugin({ + overlay: { sockIntegration: true }, + }); + }).toThrowErrorMatchingInlineSnapshot(); + }); + it('should accept "overlay.sockHost" when it is a string', () => { expect(() => { new ReactRefreshPlugin({ From 9b448f01ca07893a3633ce749a8cdb7dc5424251 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Mon, 29 Jun 2020 04:34:22 +0800 Subject: [PATCH 20/20] test: update inline snapshots for validateOptions tests --- test/unit/validateOptions.test.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/test/unit/validateOptions.test.js b/test/unit/validateOptions.test.js index aff79edb..5d8e45f6 100644 --- a/test/unit/validateOptions.test.js +++ b/test/unit/validateOptions.test.js @@ -168,7 +168,10 @@ describe('validateOptions', () => { new ReactRefreshPlugin({ overlay: { entry: true }, }); - }).toThrowErrorMatchingInlineSnapshot(); + }).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', () => { @@ -195,7 +198,10 @@ describe('validateOptions', () => { new ReactRefreshPlugin({ overlay: { module: true }, }); - }).toThrowErrorMatchingInlineSnapshot(); + }).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"', () => { @@ -246,7 +252,18 @@ describe('validateOptions', () => { new ReactRefreshPlugin({ overlay: { sockIntegration: true }, }); - }).toThrowErrorMatchingInlineSnapshot(); + }).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', () => {