From 85b98d351c603ef5c661bff5abb1db6e4841cfd0 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Fri, 29 May 2020 16:18:30 +0800 Subject: [PATCH 01/16] chore: add package for loader source map generation --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 65ba6eeb..f08a1920 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ ], "scripts": { "pretest": "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 .", "lint:fix": "yarn lint --fix", @@ -47,7 +48,8 @@ "html-entities": "^1.2.1", "lodash.debounce": "^4.0.8", "native-url": "^0.2.6", - "schema-utils": "^2.6.5" + "schema-utils": "^2.6.5", + "source-map": "^0.7.3" }, "devDependencies": { "@babel/core": "^7.9.6", From bf2f11fb770db7a36795462ec67dcf809a48ca27 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Fri, 29 May 2020 16:33:23 +0800 Subject: [PATCH 02/16] feat: attach react-refresh globals to webpack require --- src/helpers/createRefreshTemplate.js | 60 ---------- src/helpers/index.js | 2 - src/index.js | 172 ++++++++++++++++++++------- 3 files changed, 128 insertions(+), 106 deletions(-) delete mode 100644 src/helpers/createRefreshTemplate.js diff --git a/src/helpers/createRefreshTemplate.js b/src/helpers/createRefreshTemplate.js deleted file mode 100644 index afd25628..00000000 --- a/src/helpers/createRefreshTemplate.js +++ /dev/null @@ -1,60 +0,0 @@ -const { Template } = require('webpack'); - -/** - * Code to run before each module, sets up react-refresh. - * - * `module.i` is injected by Webpack and should always exist. - * - * [Ref](https://github.com/webpack/webpack/blob/master/lib/MainTemplate.js#L233) - */ -const beforeModule = ` -let cleanup = function NoOp() {}; - -const check = function(it) { - return it && it.Math == Math && it; -}; - -const safeThis = - check(typeof globalThis == 'object' && globalThis) || - check(typeof window == 'object' && window) || - check(typeof self == 'object' && self) || - check(typeof global == 'object' && global) || - Function('return this')(); - -if (safeThis && safeThis.$RefreshSetup$) { - cleanup = safeThis.$RefreshSetup$(module.i); -} - -try { -`; - -/** Code to run after each module, sets up react-refresh */ -const afterModule = ` -} finally { - cleanup(); -} -`; - -/** - * Creates a module wrapped by a refresh template. - * @param {string} source The source code of a module. - * @returns {string} A refresh-wrapped module. - */ -function createRefreshTemplate(source) { - const lines = source.split('\n'); - - // Webpack generates this line whenever the mainTemplate is called - const moduleInitializationLineNumber = lines.findIndex((line) => - line.startsWith('modules[moduleId].call') - ); - - return Template.asString([ - ...lines.slice(0, moduleInitializationLineNumber), - beforeModule, - Template.indent(lines[moduleInitializationLineNumber]), - afterModule, - ...lines.slice(moduleInitializationLineNumber + 1, lines.length), - ]); -} - -module.exports = createRefreshTemplate; diff --git a/src/helpers/index.js b/src/helpers/index.js index 254704aa..63a0007b 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -1,10 +1,8 @@ -const createRefreshTemplate = require('./createRefreshTemplate'); const getSocketIntegration = require('./getSocketIntegration'); const injectRefreshEntry = require('./injectRefreshEntry'); const normalizeOptions = require('./normalizeOptions'); module.exports = { - createRefreshTemplate, getSocketIntegration, injectRefreshEntry, normalizeOptions, diff --git a/src/index.js b/src/index.js index ea0bee71..744d79d0 100644 --- a/src/index.js +++ b/src/index.js @@ -1,15 +1,28 @@ const path = require('path'); const validateOptions = require('schema-utils'); -const webpack = require('webpack'); -const { - createRefreshTemplate, - getSocketIntegration, - injectRefreshEntry, - normalizeOptions, -} = require('./helpers'); +const { DefinePlugin, ModuleFilenameHelpers, ProvidePlugin, Template } = require('webpack'); +const ConstDependency = require('webpack/lib/dependencies/ConstDependency'); +const NullFactory = require('webpack/lib/NullFactory'); +const ParserHelpers = require('webpack/lib/ParserHelpers'); +const { getSocketIntegration, injectRefreshEntry, normalizeOptions } = require('./helpers'); const { errorOverlay, initSocket, refreshUtils } = require('./runtime/globals'); const schema = require('./options.json'); +// Mapping of react-refresh globals to Webpack require extensions +const PARSER_REPLACEMENTS = { + $RefreshRuntime$: '__webpack_require__.$Refresh$.runtime', + $RefreshReg$: '__webpack_require__.$Refresh$.register', + $RefreshSig$: '__webpack_require__.$Refresh$.signature', + $RefreshCleanup$: '__webpack_require__.$Refresh$.cleanup', +}; + +const PARSER_REPLACEMENT_TYPES = { + $RefreshRuntime$: 'object', + $RefreshReg$: 'function', + $RefreshSig$: 'function', + $RefreshCleanup$: 'function', +}; + class ReactRefreshPlugin { /** * @param {import('./types').ReactRefreshPluginOptions} [options] Options for react-refresh-plugin. @@ -49,14 +62,14 @@ class ReactRefreshPlugin { // Inject react-refresh context to all Webpack entry points compiler.options.entry = injectRefreshEntry(compiler.options.entry, this.options); - // Inject necessary modules to Webpack's global scope + // Inject necessary modules to bundle's global scope let providedModules = { [refreshUtils]: require.resolve('./runtime/refreshUtils'), }; if (this.options.overlay === false) { // Stub errorOverlay module so calls to it will be erased - const definePlugin = new webpack.DefinePlugin({ [errorOverlay]: false }); + const definePlugin = new DefinePlugin({ [errorOverlay]: false }); definePlugin.apply(compiler); } else { providedModules = { @@ -66,7 +79,7 @@ class ReactRefreshPlugin { }; } - const providePlugin = new webpack.ProvidePlugin(providedModules); + const providePlugin = new ProvidePlugin(providedModules); providePlugin.apply(compiler); compiler.hooks.beforeRun.tap(this.constructor.name, (compiler) => { @@ -86,7 +99,7 @@ class ReactRefreshPlugin { } }); - const matchObject = webpack.ModuleFilenameHelpers.matchObject.bind(undefined, this.options); + const matchObject = ModuleFilenameHelpers.matchObject.bind(undefined, this.options); compiler.hooks.normalModuleFactory.tap(this.constructor.name, (nmf) => { nmf.hooks.afterResolve.tap(this.constructor.name, (data) => { // Inject refresh loader to all JavaScript-like files @@ -108,43 +121,114 @@ class ReactRefreshPlugin { }); }); - compiler.hooks.compilation.tap(this.constructor.name, (compilation) => { - compilation.mainTemplate.hooks.require.tap( - this.constructor.name, - // Constructs the correct module template for react-refresh - (source, chunk, hash) => { - const mainTemplate = compilation.mainTemplate; - - // Check for the output filename - // This is to ensure we are processing a JS-related chunk - let filename = mainTemplate.outputOptions.filename; - if (typeof filename === 'function') { - // Only usage of the `chunk` property is documented by Webpack. - // However, some internal Webpack plugins uses other properties, - // so we also pass them through to be on the safe side. - filename = filename({ - chunk, - hash, - // TODO: Figure out whether we need to stub the following properties, probably no - contentHashType: 'javascript', - hashWithLength: (length) => mainTemplate.renderCurrentHashCode(hash, length), - noChunkHash: mainTemplate.useChunkHash(chunk), - }); + compiler.hooks.compilation.tap( + this.constructor.name, + (compilation, { normalModuleFactory }) => { + compilation.dependencyFactories.set(ConstDependency, new NullFactory()); + compilation.dependencyTemplates.set(ConstDependency, new ConstDependency.Template()); + + compilation.mainTemplate.hooks.require.tap( + this.constructor.name, + // Constructs the module template for react-refresh + (source, chunk, hash) => { + const mainTemplate = compilation.mainTemplate; + + // Check for the output filename + // This is to ensure we are processing a JS-related chunk + let filename = mainTemplate.outputOptions.filename; + if (typeof filename === 'function') { + // Only usage of the `chunk` property is documented by Webpack. + // However, some internal Webpack plugins uses other properties, + // so we also pass them through to be on the safe side. + filename = filename({ + chunk, + hash, + // TODO: Figure out whether we need to stub the following properties, probably no + contentHashType: 'javascript', + hashWithLength: (length) => mainTemplate.renderCurrentHashCode(hash, length), + noChunkHash: mainTemplate.useChunkHash(chunk), + }); + } + + // Check whether the current compilation is outputting to JS, + // since other plugins can trigger compilations for other file types too. + // If we apply the transform to them, their compilation will break fatally. + // One prominent example of this is the HTMLWebpackPlugin. + // If filename is falsy, something is terribly wrong and there's nothing we can do. + if (!filename || !filename.includes('.js')) { + return source; + } + + // Split template source code into lines for easier processing + const lines = source.split('\n'); + // Webpack generates this line when the MainTemplate is called + const moduleInitializationLineNumber = lines.findIndex((line) => + line.startsWith('modules[moduleId].call') + ); + + return Template.asString([ + ...lines.slice(0, moduleInitializationLineNumber), + '', + 'try {', + Template.indent(lines[moduleInitializationLineNumber]), + '} finally {', + Template.indent(['__webpack_require__.$Refresh$.cleanup();']), + '}', + '', + ...lines.slice(moduleInitializationLineNumber + 1, lines.length), + ]); } + ); - // Check whether the current compilation is outputting to JS, - // since other plugins can trigger compilations for other file types too. - // If we apply the transform to them, their compilation will break fatally. - // One prominent example of this is the HTMLWebpackPlugin. - // If filename is falsy, something is terribly wrong and there's nothing we can do. - if (!filename || !filename.includes('.js')) { - return source; + compilation.mainTemplate.hooks.requireExtensions.tap( + this.constructor.name, + // Setup react-refresh globals as extensions to Webpack's require function + (source) => { + return Template.asString([ + source, + '', + '__webpack_require__.$Refresh$ = {};', + '__webpack_require__.$Refresh$.runtime = {};', + '__webpack_require__.$Refresh$.cleanup = function() {};', + '__webpack_require__.$Refresh$.register = function() {};', + '__webpack_require__.$Refresh$.signature = function() {', + Template.indent(['return function(type) { return type; };']), + '};', + ]); } + ); - return createRefreshTemplate(source); - } - ); - }); + // Transform global calls into require extensions calls + const parserHandler = (parser) => { + Object.entries(PARSER_REPLACEMENTS).forEach(([key, replacement]) => { + parser.hooks.expression + .for(key) + .tap( + this.constructor.name, + ParserHelpers.toConstantDependencyWithWebpackRequire(parser, replacement) + ); + if (PARSER_REPLACEMENT_TYPES[key]) { + parser.hooks.evaluateTypeof + .for(key) + .tap( + this.constructor.name, + ParserHelpers.evaluateToString(PARSER_REPLACEMENT_TYPES[key]) + ); + } + }); + }; + + normalModuleFactory.hooks.parser + .for('javascript/auto') + .tap(this.constructor.name, parserHandler); + normalModuleFactory.hooks.parser + .for('javascript/dynamic') + .tap(this.constructor.name, parserHandler); + normalModuleFactory.hooks.parser + .for('javascript/esm') + .tap(this.constructor.name, parserHandler); + } + ); } } From f586ef6f80cf3b2b551837af6291530bc5a4c0b6 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Fri, 29 May 2020 16:34:28 +0800 Subject: [PATCH 03/16] feat: implement module setup code with parser-transformed globals --- src/loader/RefreshSetup.runtime.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/loader/RefreshSetup.runtime.js diff --git a/src/loader/RefreshSetup.runtime.js b/src/loader/RefreshSetup.runtime.js new file mode 100644 index 00000000..4bdf4899 --- /dev/null +++ b/src/loader/RefreshSetup.runtime.js @@ -0,0 +1,30 @@ +/* global $RefreshCleanup$, $RefreshReg$, $RefreshRuntime$, $RefreshSig$ */ +/* eslint-disable no-global-assign */ + +/** + * Code prepended to each JS-like module to setup react-refresh globals. + * + * All globals are injected via Webpack parser hooks. + * + * The function declaration syntax below is needed for `Template.getFunctionContent` to parse this. + */ +module.exports = function () { + $RefreshRuntime$ = require('react-refresh/runtime'); + + const __react_refresh_prev_reg__ = $RefreshReg$; + const __react_refresh_prev_sig__ = $RefreshSig$; + const __react_refresh_prev_cleanup__ = $RefreshCleanup$; + + $RefreshReg$ = function (type, id) { + const typeId = module.i + ' ' + id; + $RefreshRuntime$.register(type, typeId); + }; + + $RefreshSig$ = $RefreshRuntime$.createSignatureFunctionForTransform; + + $RefreshCleanup$ = function () { + $RefreshReg$ = __react_refresh_prev_reg__; + $RefreshSig$ = __react_refresh_prev_sig__; + $RefreshCleanup$ = __react_refresh_prev_cleanup__; + }; +}; From ecafbeaad4b09ef06d3c6b08a1a4eb34557ae058 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Fri, 29 May 2020 16:36:41 +0800 Subject: [PATCH 04/16] feat: update loader for prepended runtime and proper source map handling --- ...uleRuntime.js => RefreshModule.runtime.js} | 2 +- src/loader/index.js | 41 ++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) rename src/loader/{RefreshModuleRuntime.js => RefreshModule.runtime.js} (95%) diff --git a/src/loader/RefreshModuleRuntime.js b/src/loader/RefreshModule.runtime.js similarity index 95% rename from src/loader/RefreshModuleRuntime.js rename to src/loader/RefreshModule.runtime.js index b05e90e6..a1abf80c 100644 --- a/src/loader/RefreshModuleRuntime.js +++ b/src/loader/RefreshModule.runtime.js @@ -1,7 +1,7 @@ /* global $RefreshUtils$ */ /** - * Code injected to each JS-like module for react-refresh capabilities. + * Code appended to each JS-like module for react-refresh capabilities. * * `$RefreshUtils$` is replaced to the actual utils during source parsing by `webpack.ProvidePlugin`. * diff --git a/src/loader/index.js b/src/loader/index.js index a750fa00..3ae02f3f 100644 --- a/src/loader/index.js +++ b/src/loader/index.js @@ -1,7 +1,12 @@ +const { SourceMapConsumer, SourceNode } = require('source-map'); const { Template } = require('webpack'); const { refreshUtils } = require('../runtime/globals'); -const RefreshModuleRuntime = Template.getFunctionContent(require('./RefreshModuleRuntime')) +const RefreshSetupRuntime = Template.getFunctionContent(require('./RefreshSetup.runtime')) + .trim() + .replace(/^ {2}/gm, ''); + +const RefreshModuleRuntime = Template.getFunctionContent(require('./RefreshModule.runtime')) .trim() .replace(/^ {2}/gm, '') .replace(/\$RefreshUtils\$/g, refreshUtils); @@ -13,11 +18,37 @@ const RefreshModuleRuntime = Template.getFunctionContent(require('./RefreshModul * @this {import('webpack').loader.LoaderContext} * @param {string} source The original module source code. * @param {import('source-map').RawSourceMap} [inputSourceMap] The source map of the module. + * @param {*} [meta] The loader metadata passed in. * @returns {void} */ -function RefreshHotLoader(source, inputSourceMap) { - // Use callback to allow source maps to pass through - this.callback(null, source + '\n\n' + RefreshModuleRuntime, inputSourceMap); +function ReactRefreshLoader(source, inputSourceMap, meta) { + const callback = this.async(); + + async function _loader(source, inputSourceMap) { + if (this.sourceMap) { + const node = SourceNode.fromStringWithSourceMap( + source, + await new SourceMapConsumer(inputSourceMap) + ); + + node.prepend([RefreshSetupRuntime, '\n\n']); + node.add(['\n\n', RefreshModuleRuntime]); + + const { code, map } = node.toStringWithSourceMap(); + return [code, map.toJSON()]; + } else { + return [[RefreshSetupRuntime, source, RefreshModuleRuntime].join('\n\n'), inputSourceMap]; + } + } + + _loader.call(this, source, inputSourceMap).then( + ([code, map]) => { + callback(null, code, map, meta); + }, + (error) => { + callback(error); + } + ); } -module.exports = RefreshHotLoader; +module.exports = ReactRefreshLoader; From 3598751d44b4023abac3102242781559e3301aea Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Fri, 29 May 2020 16:37:21 +0800 Subject: [PATCH 05/16] chore: implement globalThis shim --- src/runtime/safeThis.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/runtime/safeThis.js diff --git a/src/runtime/safeThis.js b/src/runtime/safeThis.js new file mode 100644 index 00000000..41de9d7f --- /dev/null +++ b/src/runtime/safeThis.js @@ -0,0 +1,12 @@ +/* global globalThis */ + +const check = function (it) { + return it && it.Math == Math && it; +}; + +module.exports = + check(typeof globalThis == 'object' && globalThis) || + check(typeof window == 'object' && window) || + check(typeof self == 'object' && self) || + check(typeof global == 'object' && global) || + Function('return this')(); From f4fc1cb67d88e14e3c6b58ec05c8975f7a5b0199 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Fri, 29 May 2020 16:39:49 +0800 Subject: [PATCH 06/16] feat: remove globals from refresh entry; inject hook only on first run --- src/runtime/ReactRefreshEntry.js | 54 ++++++-------------------------- 1 file changed, 10 insertions(+), 44 deletions(-) diff --git a/src/runtime/ReactRefreshEntry.js b/src/runtime/ReactRefreshEntry.js index afd51c93..169c7e41 100644 --- a/src/runtime/ReactRefreshEntry.js +++ b/src/runtime/ReactRefreshEntry.js @@ -1,48 +1,14 @@ -if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined') { - const Refresh = require('react-refresh/runtime'); - - // Inject refresh runtime into global - Refresh.injectIntoGlobalHook(window); - - // Setup placeholder functions - window.$RefreshReg$ = function () {}; - window.$RefreshSig$ = function () { - return function (type) { - return type; - }; - }; +const safeThis = require('./safeThis'); - /** - * Setup module refresh. - * @param {number} moduleId An ID of a module. - * @returns {function(): void} A function to restore handlers to their previous state. - */ - window.$RefreshSetup$ = function setupModuleRefresh(moduleId) { - // Capture previous refresh state - const prevRefreshReg = window.$RefreshReg$; - const prevRefreshSig = window.$RefreshSig$; - - /** - * Registers a refresh to react-refresh. - * @param {string} [type] A valid type of a module. - * @param {number} [id] An ID of a module. - * @returns {void} - */ - window.$RefreshReg$ = function (type, id) { - const typeId = moduleId + ' ' + id; - Refresh.register(type, typeId); - }; +if (process.env.NODE_ENV !== 'production' && typeof safeThis !== 'undefined') { + const Refresh = require('react-refresh/runtime'); - /** - * Creates a module signature function from react-refresh. - * @returns {function(type: string): string} A created signature function. - */ - window.$RefreshSig$ = Refresh.createSignatureFunctionForTransform; + // Only inject the runtime if it hasn't been injected + if (!safeThis.__reactRefreshInjected) { + // Inject refresh runtime into global + Refresh.injectIntoGlobalHook(safeThis); - // Restore to previous refresh functions after initialization - return function cleanup() { - window.$RefreshReg$ = prevRefreshReg; - window.$RefreshSig$ = prevRefreshSig; - }; - }; + // Mark the runtime as injected to prevent double-injection + safeThis.__reactRefreshInjected = true; + } } From a75e25215095c8359d3d65d7a1f36c32b7b804d8 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Fri, 29 May 2020 16:40:02 +0800 Subject: [PATCH 07/16] refactor: use safeThis legacy wds sockets --- src/runtime/LegacyWDSSocketEntry.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/runtime/LegacyWDSSocketEntry.js b/src/runtime/LegacyWDSSocketEntry.js index b61e5fcc..7137fe9e 100644 --- a/src/runtime/LegacyWDSSocketEntry.js +++ b/src/runtime/LegacyWDSSocketEntry.js @@ -1,4 +1,5 @@ const SockJS = require('sockjs-client/dist/sockjs'); +const safeThis = require('./safeThis'); /** * A SockJS client adapted for use with webpack-dev-server. @@ -27,4 +28,4 @@ SockJSClient.prototype.onMessage = function onMessage(fn) { }; }; -window.__webpack_dev_server_client__ = SockJSClient; +safeThis.__webpack_dev_server_client__ = SockJSClient; From 6f0fa556984e838053e2b1054fb4860112b6a485 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Fri, 29 May 2020 16:44:53 +0800 Subject: [PATCH 08/16] refactor: remove centralized globals for define/provide plugin --- src/index.js | 9 ++++----- src/loader/index.js | 3 +-- src/runtime/globals.js | 5 ----- 3 files changed, 5 insertions(+), 12 deletions(-) delete mode 100644 src/runtime/globals.js diff --git a/src/index.js b/src/index.js index 744d79d0..8a98eedb 100644 --- a/src/index.js +++ b/src/index.js @@ -5,7 +5,6 @@ const ConstDependency = require('webpack/lib/dependencies/ConstDependency'); const NullFactory = require('webpack/lib/NullFactory'); const ParserHelpers = require('webpack/lib/ParserHelpers'); const { getSocketIntegration, injectRefreshEntry, normalizeOptions } = require('./helpers'); -const { errorOverlay, initSocket, refreshUtils } = require('./runtime/globals'); const schema = require('./options.json'); // Mapping of react-refresh globals to Webpack require extensions @@ -64,18 +63,18 @@ class ReactRefreshPlugin { // Inject necessary modules to bundle's global scope let providedModules = { - [refreshUtils]: require.resolve('./runtime/refreshUtils'), + __react_refresh_utils__: require.resolve('./runtime/refreshUtils'), }; if (this.options.overlay === false) { // Stub errorOverlay module so calls to it will be erased - const definePlugin = new DefinePlugin({ [errorOverlay]: false }); + const definePlugin = new DefinePlugin({ __react_refresh_error_overlay__: false }); definePlugin.apply(compiler); } else { providedModules = { ...providedModules, - [errorOverlay]: require.resolve(this.options.overlay.module), - [initSocket]: getSocketIntegration(this.options.overlay.sockIntegration), + __react_refresh_error_overlay__: require.resolve(this.options.overlay.module), + __react_refresh_init_socket__: getSocketIntegration(this.options.overlay.sockIntegration), }; } diff --git a/src/loader/index.js b/src/loader/index.js index 3ae02f3f..0f8d2110 100644 --- a/src/loader/index.js +++ b/src/loader/index.js @@ -1,6 +1,5 @@ const { SourceMapConsumer, SourceNode } = require('source-map'); const { Template } = require('webpack'); -const { refreshUtils } = require('../runtime/globals'); const RefreshSetupRuntime = Template.getFunctionContent(require('./RefreshSetup.runtime')) .trim() @@ -9,7 +8,7 @@ const RefreshSetupRuntime = Template.getFunctionContent(require('./RefreshSetup. const RefreshModuleRuntime = Template.getFunctionContent(require('./RefreshModule.runtime')) .trim() .replace(/^ {2}/gm, '') - .replace(/\$RefreshUtils\$/g, refreshUtils); + .replace(/\$RefreshUtils\$/g, '__react_refresh_utils__'); /** * A simple Webpack loader to inject react-refresh HMR code into modules. diff --git a/src/runtime/globals.js b/src/runtime/globals.js deleted file mode 100644 index f4020c25..00000000 --- a/src/runtime/globals.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - errorOverlay: '__react_refresh_error_overlay__', - initSocket: '__react_refresh_init_socket__', - refreshUtils: '__react_refresh_utils__', -}; From 6c08bba2452a9e7a5d09a71923b12ae9817e6880 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Sat, 30 May 2020 01:39:50 +0800 Subject: [PATCH 09/16] refactor: remove placeholder symbols in refresh module runtime --- src/loader/RefreshModule.runtime.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/loader/RefreshModule.runtime.js b/src/loader/RefreshModule.runtime.js index a1abf80c..fb7c2f29 100644 --- a/src/loader/RefreshModule.runtime.js +++ b/src/loader/RefreshModule.runtime.js @@ -1,4 +1,4 @@ -/* global $RefreshUtils$ */ +/* global __react_refresh_utils__ */ /** * Code appended to each JS-like module for react-refresh capabilities. @@ -11,29 +11,29 @@ * [Reference for HMR Error Recovery](https://github.com/webpack/webpack/issues/418#issuecomment-490296365) */ module.exports = function () { - const currentExports = $RefreshUtils$.getModuleExports(module); - $RefreshUtils$.registerExportsForReactRefresh(currentExports, module.id); + const currentExports = __react_refresh_utils__.getModuleExports(module); + __react_refresh_utils__.registerExportsForReactRefresh(currentExports, module.id); if (module.hot) { const isHotUpdate = !!module.hot.data; const prevExports = isHotUpdate ? module.hot.data.prevExports : null; - if ($RefreshUtils$.isReactRefreshBoundary(currentExports)) { - module.hot.dispose($RefreshUtils$.createHotDisposeCallback(currentExports)); - module.hot.accept($RefreshUtils$.createHotErrorHandler(module.id)); + if (__react_refresh_utils__.isReactRefreshBoundary(currentExports)) { + module.hot.dispose(__react_refresh_utils__.createHotDisposeCallback(currentExports)); + module.hot.accept(__react_refresh_utils__.createHotErrorHandler(module.id)); if (isHotUpdate) { if ( - $RefreshUtils$.isReactRefreshBoundary(prevExports) && - $RefreshUtils$.shouldInvalidateReactRefreshBoundary(prevExports, currentExports) + __react_refresh_utils__.isReactRefreshBoundary(prevExports) && + __react_refresh_utils__.shouldInvalidateReactRefreshBoundary(prevExports, currentExports) ) { module.hot.invalidate(); } else { - $RefreshUtils$.enqueueUpdate(); + __react_refresh_utils__.enqueueUpdate(); } } } else { - if (isHotUpdate && $RefreshUtils$.isReactRefreshBoundary(prevExports)) { + if (isHotUpdate && __react_refresh_utils__.isReactRefreshBoundary(prevExports)) { module.hot.invalidate(); } } From a28ddb3ff98d0f31a2fc8ad51fae0f018c3391a6 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Sat, 30 May 2020 01:41:13 +0800 Subject: [PATCH 10/16] refactor: use closure to eliminate globals in refresh setup runtime --- src/index.js | 41 +++++++++++++++++++++++++++--- src/loader/RefreshSetup.runtime.js | 22 +++------------- src/loader/index.js | 19 ++++++++------ 3 files changed, 51 insertions(+), 31 deletions(-) diff --git a/src/index.js b/src/index.js index 8a98eedb..5a94f0b9 100644 --- a/src/index.js +++ b/src/index.js @@ -10,16 +10,18 @@ const schema = require('./options.json'); // Mapping of react-refresh globals to Webpack require extensions const PARSER_REPLACEMENTS = { $RefreshRuntime$: '__webpack_require__.$Refresh$.runtime', + $RefreshSetup$: '__webpack_require__.$Refresh$.setup', + $RefreshCleanup$: '__webpack_require__.$Refresh$.cleanup', $RefreshReg$: '__webpack_require__.$Refresh$.register', $RefreshSig$: '__webpack_require__.$Refresh$.signature', - $RefreshCleanup$: '__webpack_require__.$Refresh$.cleanup', }; const PARSER_REPLACEMENT_TYPES = { $RefreshRuntime$: 'object', + $RefreshSetup$: 'function', + $RefreshCleanup$: 'function', $RefreshReg$: 'function', $RefreshSig$: 'function', - $RefreshCleanup$: 'function', }; class ReactRefreshPlugin { @@ -168,10 +170,40 @@ class ReactRefreshPlugin { return Template.asString([ ...lines.slice(0, moduleInitializationLineNumber), '', + '__webpack_require__.$Refresh$.setup = function() {', + Template.indent([ + 'const Refresh = __webpack_require__.$Refresh$;', + '', + 'const prevSetup = Refresh.setup;', + 'const prevCleanup = Refresh.cleanup;', + 'const prevReg = Refresh.register;', + 'const prevSig = Refresh.signature;', + '', + 'Refresh.register = function register(type, id) {', + Template.indent([ + 'const typeId = moduleId + " " + id;', + 'Refresh.runtime.register(type, typeId);', + ]), + '};', + '', + 'Refresh.signature = Refresh.runtime.createSignatureFunctionForTransform;', + '', + 'Refresh.cleanup = function cleanup() {', + Template.indent([ + 'Refresh.register = prevReg;', + 'Refresh.signature = prevSig;', + 'Refresh.cleanup = prevCleanup;', + ]), + '};', + '', + 'Refresh.setup = prevSetup;', + ]), + '};', + '', 'try {', Template.indent(lines[moduleInitializationLineNumber]), '} finally {', - Template.indent(['__webpack_require__.$Refresh$.cleanup();']), + Template.indent('__webpack_require__.$Refresh$.cleanup();'), '}', '', ...lines.slice(moduleInitializationLineNumber + 1, lines.length), @@ -188,10 +220,11 @@ class ReactRefreshPlugin { '', '__webpack_require__.$Refresh$ = {};', '__webpack_require__.$Refresh$.runtime = {};', + '__webpack_require__.$Refresh$.setup = function() {};', '__webpack_require__.$Refresh$.cleanup = function() {};', '__webpack_require__.$Refresh$.register = function() {};', '__webpack_require__.$Refresh$.signature = function() {', - Template.indent(['return function(type) { return type; };']), + Template.indent('return function(type) { return type; };'), '};', ]); } diff --git a/src/loader/RefreshSetup.runtime.js b/src/loader/RefreshSetup.runtime.js index 4bdf4899..5f3ba431 100644 --- a/src/loader/RefreshSetup.runtime.js +++ b/src/loader/RefreshSetup.runtime.js @@ -1,5 +1,5 @@ -/* global $RefreshCleanup$, $RefreshReg$, $RefreshRuntime$, $RefreshSig$ */ -/* eslint-disable no-global-assign */ +/* eslint-disable no-global-assign, no-unused-vars */ +/* global $RefreshRuntime$, $RefreshSetup$ */ /** * Code prepended to each JS-like module to setup react-refresh globals. @@ -10,21 +10,5 @@ */ module.exports = function () { $RefreshRuntime$ = require('react-refresh/runtime'); - - const __react_refresh_prev_reg__ = $RefreshReg$; - const __react_refresh_prev_sig__ = $RefreshSig$; - const __react_refresh_prev_cleanup__ = $RefreshCleanup$; - - $RefreshReg$ = function (type, id) { - const typeId = module.i + ' ' + id; - $RefreshRuntime$.register(type, typeId); - }; - - $RefreshSig$ = $RefreshRuntime$.createSignatureFunctionForTransform; - - $RefreshCleanup$ = function () { - $RefreshReg$ = __react_refresh_prev_reg__; - $RefreshSig$ = __react_refresh_prev_sig__; - $RefreshCleanup$ = __react_refresh_prev_cleanup__; - }; + $RefreshSetup$(); }; diff --git a/src/loader/index.js b/src/loader/index.js index 0f8d2110..f66512e1 100644 --- a/src/loader/index.js +++ b/src/loader/index.js @@ -1,14 +1,17 @@ const { SourceMapConsumer, SourceNode } = require('source-map'); const { Template } = require('webpack'); -const RefreshSetupRuntime = Template.getFunctionContent(require('./RefreshSetup.runtime')) - .trim() - .replace(/^ {2}/gm, ''); - -const RefreshModuleRuntime = Template.getFunctionContent(require('./RefreshModule.runtime')) - .trim() - .replace(/^ {2}/gm, '') - .replace(/\$RefreshUtils\$/g, '__react_refresh_utils__'); +/** + * Gets a runtime template from provided function. + * @param {function(): void} fn A function containing the runtime template. + * @returns {string} The "sanitized" runtime template. + */ +function getTemplate(fn) { + return Template.getFunctionContent(fn).trim().replace(/^ {2}/gm, ''); +} + +const RefreshSetupRuntime = getTemplate(require('./RefreshSetup.runtime')); +const RefreshModuleRuntime = getTemplate(require('./RefreshModule.runtime')); /** * A simple Webpack loader to inject react-refresh HMR code into modules. From e3436486792d759ec1026e5419b0f022c50ded6b Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Sat, 30 May 2020 01:41:46 +0800 Subject: [PATCH 11/16] refactor: use typeof checks for provide plugin injections --- src/runtime/refreshUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runtime/refreshUtils.js b/src/runtime/refreshUtils.js index 2547b76f..da112217 100644 --- a/src/runtime/refreshUtils.js +++ b/src/runtime/refreshUtils.js @@ -71,7 +71,7 @@ function createHotErrorHandler(moduleId) { * @returns {void} */ function hotErrorHandler(error) { - if (__react_refresh_error_overlay__) { + if (typeof __react_refresh_error_overlay__ !== 'undefined') { __react_refresh_error_overlay__.handleRuntimeError(error); } @@ -115,7 +115,7 @@ function createDebounceUpdate() { refreshTimeout = setTimeout(function () { refreshTimeout = undefined; Refresh.performReactRefresh(); - if (__react_refresh_error_overlay__) { + if (typeof __react_refresh_error_overlay__ !== 'undefined') { __react_refresh_error_overlay__.clearRuntimeErrors(); } }, 30); From 64d0bff560ae0e1d8e1ed2fec843d1da313e8e12 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Sat, 30 May 2020 04:55:41 +0800 Subject: [PATCH 12/16] fix: include moduleId as setup param and interpolate refresh obj --- src/index.js | 56 +++++++++++++++--------------- src/loader/RefreshSetup.runtime.js | 2 +- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/index.js b/src/index.js index 5a94f0b9..d87fab1e 100644 --- a/src/index.js +++ b/src/index.js @@ -7,13 +7,15 @@ const ParserHelpers = require('webpack/lib/ParserHelpers'); const { getSocketIntegration, injectRefreshEntry, normalizeOptions } = require('./helpers'); const schema = require('./options.json'); +const refreshObj = '__webpack_require__.$Refresh$'; + // Mapping of react-refresh globals to Webpack require extensions const PARSER_REPLACEMENTS = { - $RefreshRuntime$: '__webpack_require__.$Refresh$.runtime', - $RefreshSetup$: '__webpack_require__.$Refresh$.setup', - $RefreshCleanup$: '__webpack_require__.$Refresh$.cleanup', - $RefreshReg$: '__webpack_require__.$Refresh$.register', - $RefreshSig$: '__webpack_require__.$Refresh$.signature', + $RefreshRuntime$: `${refreshObj}.runtime`, + $RefreshSetup$: `${refreshObj}.setup`, + $RefreshCleanup$: `${refreshObj}.cleanup`, + $RefreshReg$: `${refreshObj}.register`, + $RefreshSig$: `${refreshObj}.signature`, }; const PARSER_REPLACEMENT_TYPES = { @@ -170,40 +172,38 @@ class ReactRefreshPlugin { return Template.asString([ ...lines.slice(0, moduleInitializationLineNumber), '', - '__webpack_require__.$Refresh$.setup = function() {', + `${refreshObj}.setup = function(currentModuleId) {`, Template.indent([ - 'const Refresh = __webpack_require__.$Refresh$;', - '', - 'const prevSetup = Refresh.setup;', - 'const prevCleanup = Refresh.cleanup;', - 'const prevReg = Refresh.register;', - 'const prevSig = Refresh.signature;', + `const prevSetup = ${refreshObj}.setup;`, + `const prevCleanup = ${refreshObj}.cleanup;`, + `const prevReg = ${refreshObj}.register;`, + `const prevSig = ${refreshObj}.signature;`, '', - 'Refresh.register = function register(type, id) {', + `${refreshObj}.register = function register(type, id) {`, Template.indent([ - 'const typeId = moduleId + " " + id;', - 'Refresh.runtime.register(type, typeId);', + 'const typeId = currentModuleId + " " + id;', + `${refreshObj}.runtime.register(type, typeId);`, ]), '};', '', - 'Refresh.signature = Refresh.runtime.createSignatureFunctionForTransform;', + `${refreshObj}.signature = ${refreshObj}.runtime.createSignatureFunctionForTransform;`, '', - 'Refresh.cleanup = function cleanup() {', + `${refreshObj}.cleanup = function cleanup() {`, Template.indent([ - 'Refresh.register = prevReg;', - 'Refresh.signature = prevSig;', - 'Refresh.cleanup = prevCleanup;', + `${refreshObj}.register = prevReg;`, + `${refreshObj}.signature = prevSig;`, + `${refreshObj}.cleanup = prevCleanup;`, ]), '};', '', - 'Refresh.setup = prevSetup;', + `${refreshObj}.setup = prevSetup;`, ]), '};', '', 'try {', Template.indent(lines[moduleInitializationLineNumber]), '} finally {', - Template.indent('__webpack_require__.$Refresh$.cleanup();'), + Template.indent(`${refreshObj}.cleanup();`), '}', '', ...lines.slice(moduleInitializationLineNumber + 1, lines.length), @@ -218,12 +218,12 @@ class ReactRefreshPlugin { return Template.asString([ source, '', - '__webpack_require__.$Refresh$ = {};', - '__webpack_require__.$Refresh$.runtime = {};', - '__webpack_require__.$Refresh$.setup = function() {};', - '__webpack_require__.$Refresh$.cleanup = function() {};', - '__webpack_require__.$Refresh$.register = function() {};', - '__webpack_require__.$Refresh$.signature = function() {', + `${refreshObj} = {};`, + `${refreshObj}.runtime = {};`, + `${refreshObj}.setup = function() {};`, + `${refreshObj}.cleanup = function() {};`, + `${refreshObj}.register = function() {};`, + `${refreshObj}.signature = function() {`, Template.indent('return function(type) { return type; };'), '};', ]); diff --git a/src/loader/RefreshSetup.runtime.js b/src/loader/RefreshSetup.runtime.js index 5f3ba431..5a102ae4 100644 --- a/src/loader/RefreshSetup.runtime.js +++ b/src/loader/RefreshSetup.runtime.js @@ -10,5 +10,5 @@ */ module.exports = function () { $RefreshRuntime$ = require('react-refresh/runtime'); - $RefreshSetup$(); + $RefreshSetup$(module.id); }; From 6402099b4c6e8da15eef25710f21478eb3a45cc5 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Sun, 7 Jun 2020 14:39:09 +0800 Subject: [PATCH 13/16] chore: add license for globalThis shim --- src/runtime/safeThis.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/runtime/safeThis.js b/src/runtime/safeThis.js index 41de9d7f..db099101 100644 --- a/src/runtime/safeThis.js +++ b/src/runtime/safeThis.js @@ -1,4 +1,11 @@ /* global globalThis */ +/* + This file is copied from `core-js`. + https://github.com/zloirock/core-js/blob/master/packages/core-js/internals/global.js + + MIT License + Author: Denis Pushkarev (@zloirock) +*/ const check = function (it) { return it && it.Math == Math && it; From 9cbd1344ee4ed46e7fc611989783ee8f91699e05 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Sun, 7 Jun 2020 14:39:32 +0800 Subject: [PATCH 14/16] chore: rename error to reason for unhandledRejection in test script --- scripts/test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/test.js b/scripts/test.js index cbd3bcc2..ca94d161 100644 --- a/scripts/test.js +++ b/scripts/test.js @@ -3,8 +3,8 @@ process.env.NODE_ENV = 'test'; // Crash on unhandled rejections instead of failing silently. -process.on('unhandledRejection', (error) => { - throw error; +process.on('unhandledRejection', (reason) => { + throw reason; }); const jest = require('jest'); From 4cc32c459e5bbd8b95a2593e4e2e0a99a6e7ce38 Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Sun, 7 Jun 2020 14:41:16 +0800 Subject: [PATCH 15/16] chore: scope refresh runtime import --- src/runtime/ReactRefreshEntry.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/runtime/ReactRefreshEntry.js b/src/runtime/ReactRefreshEntry.js index 169c7e41..2d6354e4 100644 --- a/src/runtime/ReactRefreshEntry.js +++ b/src/runtime/ReactRefreshEntry.js @@ -1,12 +1,11 @@ const safeThis = require('./safeThis'); if (process.env.NODE_ENV !== 'production' && typeof safeThis !== 'undefined') { - const Refresh = require('react-refresh/runtime'); - // Only inject the runtime if it hasn't been injected if (!safeThis.__reactRefreshInjected) { - // Inject refresh runtime into global - Refresh.injectIntoGlobalHook(safeThis); + const RefreshRuntime = require('react-refresh/runtime'); + // Inject refresh runtime into global scope + RefreshRuntime.injectIntoGlobalHook(safeThis); // Mark the runtime as injected to prevent double-injection safeThis.__reactRefreshInjected = true; From 754e4f926f9351e4d3ed2c93cd2554d590adb8ad Mon Sep 17 00:00:00 2001 From: Michael Mok Date: Sun, 7 Jun 2020 17:01:31 +0800 Subject: [PATCH 16/16] fix: check for object typed __react_refresh_error_overlay__ --- src/runtime/refreshUtils.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/runtime/refreshUtils.js b/src/runtime/refreshUtils.js index da112217..0c0335fa 100644 --- a/src/runtime/refreshUtils.js +++ b/src/runtime/refreshUtils.js @@ -71,11 +71,11 @@ function createHotErrorHandler(moduleId) { * @returns {void} */ function hotErrorHandler(error) { - if (typeof __react_refresh_error_overlay__ !== 'undefined') { + if (typeof __react_refresh_error_overlay__ !== 'undefined' && __react_refresh_error_overlay__) { __react_refresh_error_overlay__.handleRuntimeError(error); } - if (typeof __react_refresh_test__ !== 'undefined') { + if (typeof __react_refresh_test__ !== 'undefined' && __react_refresh_test__) { if (window.onHotAcceptError) { window.onHotAcceptError(error.message); } @@ -115,7 +115,10 @@ function createDebounceUpdate() { refreshTimeout = setTimeout(function () { refreshTimeout = undefined; Refresh.performReactRefresh(); - if (typeof __react_refresh_error_overlay__ !== 'undefined') { + if ( + typeof __react_refresh_error_overlay__ !== 'undefined' && + __react_refresh_error_overlay__ + ) { __react_refresh_error_overlay__.clearRuntimeErrors(); } }, 30);