UNPKG

react-refresh-webpack-plugin

Version:

An **EXPERIMENTAL** Webpack plugin to enable "Fast Refresh" (also previously known as _Hot Reloading_) for React components.

226 lines (194 loc) 7 kB
const Refresh = require('react-refresh/runtime'); const ErrorOverlay = require('../overlay'); /** * Extracts exports from a webpack module object. * @param {*} module A Webpack module object. * @returns {*} An exports object from the module. */ function getModuleExports(module) { return module.exports || module.__proto__.exports; } /** * Calculates the signature of a React refresh boundary. * If this signature changes, it's unsafe to accept the boundary. * * This implementation is based on the one in [Metro](https://github.com/facebook/metro/blob/907d6af22ac6ebe58572be418e9253a90665ecbd/packages/metro/src/lib/polyfills/require.js#L795-L816). */ function getReactRefreshBoundarySignature(moduleExports) { const signature = []; signature.push(Refresh.getFamilyByType(moduleExports)); if (moduleExports == null || typeof moduleExports !== 'object') { // Exit if we can't iterate over exports. return signature; } for (const key in moduleExports) { if (key === '__esModule') { continue; } signature.push(key); signature.push(Refresh.getFamilyByType(moduleExports[key])); } return signature; } /** * Creates conditional full refresh dispose handler for Webpack hot. * @param {*} module A Webpack module object. * @returns {hotDisposeCallback} A webpack hot dispose callback. */ function createHotDisposeCallback(module) { /** * 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) { if (Refresh.hasUnrecoverableErrors()) { window.location.reload(); } // We have to mutate the data object to get data registered and cached data.module = module; } return hotDisposeCallback; } /** * Creates self-recovering an error handler for webpack hot. * @param {string} moduleId A unique ID for a Webpack module. * @returns {selfAcceptingHotErrorHandler} A self-accepting webpack hot error handler. */ function createHotErrorHandler(moduleId) { /** * An error handler to show a module evaluation error with an error overlay. * @param {Error} error An error occurred during evaluation of a module. * @returns {void} */ function hotErrorHandler(error) { ErrorOverlay.handleRuntimeError(error); } /** * An error handler to allow self-recovering behaviours. * @param {Error} error An error occurred during evaluation of a module. * @returns {void} */ function selfAcceptingHotErrorHandler(error) { hotErrorHandler(error); require.cache[moduleId].hot.accept(hotErrorHandler); } return selfAcceptingHotErrorHandler; } /** * Creates a helper that performs a delayed React refresh. * @returns {enqueueUpdate} A debounced React refresh function. */ function createDebounceUpdate() { /** * A cached setTimeout handler. * @type {number | void} */ let refreshTimeout = undefined; /** * Performs react refresh on a delay and clears the error overlay. * @returns {void} */ function enqueueUpdate() { if (refreshTimeout === undefined) { refreshTimeout = setTimeout(function() { refreshTimeout = undefined; Refresh.performReactRefresh(); ErrorOverlay.clearRuntimeErrors(); }, 30); } } return enqueueUpdate; } /** * Checks if all exports are likely a React component. * * This implementation is based on the one in [Metro](https://github.com/facebook/metro/blob/febdba2383113c88296c61e28e4ef6a7f4939fda/packages/metro/src/lib/polyfills/require.js#L748-L774). * @param {*} module A Webpack module object. * @returns {boolean} Whether the exports are React component like. */ function isReactRefreshBoundary(module) { const moduleExports = getModuleExports(module); if (Refresh.isLikelyComponentType(moduleExports)) { return true; } if (moduleExports === undefined || moduleExports === null || typeof moduleExports !== 'object') { // Exit if we can't iterate over exports. return false; } let hasExports = false; let areAllExportsComponents = true; for (const key in moduleExports) { hasExports = true; // This is the ES Module indicator flag set by Webpack if (key === '__esModule') { continue; } // We can (and have to) safely execute getters here, // as Webpack manually assigns harmony exports to getters, // without any side-effects attached. // Ref: https://github.com/webpack/webpack/blob/b93048643fe74de2a6931755911da1212df55897/lib/MainTemplate.js#L281 const exportValue = moduleExports[key]; if (!Refresh.isLikelyComponentType(exportValue)) { areAllExportsComponents = false; } } return hasExports && areAllExportsComponents; } /** * Checks if exports are likely a React component and registers them. * * This implementation is based on the one in [Metro](https://github.com/facebook/metro/blob/febdba2383113c88296c61e28e4ef6a7f4939fda/packages/metro/src/lib/polyfills/require.js#L818-L835). * @param {*} module A Webpack module object. * @returns {void} */ function registerExportsForReactRefresh(module) { const moduleExports = getModuleExports(module); const moduleId = module.id; if (Refresh.isLikelyComponentType(moduleExports)) { // Register module.exports if it is likely a component Refresh.register(moduleExports, moduleId + ' %exports%'); } if (moduleExports === undefined || moduleExports === null || typeof moduleExports !== 'object') { // Exit if we can't iterate over the exports. return; } for (const key in moduleExports) { // Skip registering the Webpack ES Module indicator if (key === '__esModule') { continue; } const exportValue = moduleExports[key]; if (Refresh.isLikelyComponentType(exportValue)) { const typeID = moduleId + ' %exports% ' + key; Refresh.register(exportValue, typeID); } } } /** * Compares previous and next module objects to check for mutated boundaries. * * This implementation is based on the one in [Metro](https://github.com/facebook/metro/blob/907d6af22ac6ebe58572be418e9253a90665ecbd/packages/metro/src/lib/polyfills/require.js#L776-L792). */ function shouldInvalidateReactRefreshBoundary(prevModule, nextModule) { const prevSignature = getReactRefreshBoundarySignature(getModuleExports(prevModule)); const nextSignature = getReactRefreshBoundarySignature(getModuleExports(nextModule)); if (prevSignature.length !== nextSignature.length) { return true; } for (let i = 0; i < nextSignature.length; i += 1) { if (prevSignature[i] !== nextSignature[i]) { return true; } } return false; } module.exports = Object.freeze({ createHotDisposeCallback, createHotErrorHandler, enqueueUpdate: createDebounceUpdate(), isReactRefreshBoundary, shouldInvalidateReactRefreshBoundary, registerExportsForReactRefresh, });