packages/bundles/override/RefreshUtils.js (166 lines of code) (raw):
/* global __webpack_require__ */
let Refresh = require('react-refresh/runtime');
/**
* Extracts exports from a webpack module object.
* @param {string} moduleId A Webpack module ID.
* @returns {*} An exports object from the module.
*/
function getModuleExports(moduleId) {
if (typeof moduleId === 'undefined') {
// `moduleId` is unavailable, which indicates that this module is not in the cache,
// which means we won't be able to capture any exports,
// and thus they cannot be refreshed safely.
// These are likely runtime or dynamically generated modules.
return {};
}
// eslint-disable-next-line
let maybeModule = __webpack_require__.c[moduleId];
if (typeof maybeModule === 'undefined') {
// `moduleId` is available but the module in cache is unavailable,
// which indicates the module is somehow corrupted (e.g. broken Webpacak `module` globals).
// We will warn the user (as this is likely a mistake) and assume they cannot be refreshed.
console.warn(`[React Refresh] Failed to get exports for module: ${moduleId}.`);
return {};
}
let exportsOrPromise = maybeModule.exports;
if (typeof Promise !== 'undefined' && exportsOrPromise instanceof Promise) {
return exportsOrPromise.then((exports) => {
return exports;
});
}
return exportsOrPromise;
}
/**
* 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).
* @param {*} moduleExports A Webpack module exports object.
* @returns {string[]} A React refresh boundary signature array.
*/
function getReactRefreshBoundarySignature(moduleExports) {
let signature = [];
signature.push(Refresh.getFamilyByType(moduleExports));
if (moduleExports == null || typeof moduleExports !== 'object') {
// Exit if we can't iterate over exports.
return signature;
}
for (let key in moduleExports) {
if (key === '__esModule') {
continue;
}
signature.push(key);
signature.push(Refresh.getFamilyByType(moduleExports[key]));
}
return signature;
}
/**
* Creates a helper that performs a delayed React refresh.
* @returns {function(function(): void): void} A debounced React refresh function.
*/
function createDebounceUpdate() {
/**
* A cached setTimeout handler.
* @type {number | undefined}
*/
let refreshTimeout;
/**
* Performs react refresh on a delay and clears the error overlay.
* @param {function(): void} callback
* @returns {void}
*/
function enqueueUpdate(callback) {
if (typeof refreshTimeout === 'undefined') {
refreshTimeout = setTimeout(() => {
refreshTimeout = undefined;
Refresh.performReactRefresh();
callback();
}, 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 {*} moduleExports A Webpack module exports object.
* @returns {boolean} Whether the exports are React component like.
*/
function isSafeExport(key) {
return (
// This is the ES Module indicator flag.
key === '__esModule' ||
// Specify keys which is safe export by page routes.
key === 'pageConfig' ||
key === 'dataLoader' ||
key === 'serverDataLoader' ||
key === 'staticDataLoader'
);
}
function isReactRefreshBoundary(moduleExports) {
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 (let key in moduleExports) {
hasExports = true;
if (isSafeExport(key)) {
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
let 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 {*} moduleExports A Webpack module exports object.
* @param {string} moduleId A Webpack module ID.
* @returns {void}
*/
function registerExportsForReactRefresh(moduleExports, moduleId) {
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 (let key in moduleExports) {
// Skip registering the ES Module indicator
if (key === '__esModule') {
continue;
}
let exportValue = moduleExports[key];
if (Refresh.isLikelyComponentType(exportValue)) {
let 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).
* @param {*} prevExports The current Webpack module exports object.
* @param {*} nextExports The next Webpack module exports object.
* @returns {boolean} Whether the React refresh boundary should be invalidated.
*/
function shouldInvalidateReactRefreshBoundary(prevExports, nextExports) {
let prevSignature = getReactRefreshBoundarySignature(prevExports);
let nextSignature = getReactRefreshBoundarySignature(nextExports);
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;
}
let enqueueUpdate = createDebounceUpdate();
function executeRuntime(moduleExports, moduleId, webpackHot, refreshOverlay, isTest) {
registerExportsForReactRefresh(moduleExports, moduleId);
if (webpackHot) {
let isHotUpdate = !!webpackHot.data;
let prevExports;
if (isHotUpdate) {
prevExports = webpackHot.data.prevExports;
}
if (isReactRefreshBoundary(moduleExports)) {
webpackHot.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}
*/
(data) => {
// We have to mutate the data object to get data registered and cached
data.prevExports = moduleExports;
},
);
webpackHot.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 refreshOverlay !== 'undefined' && refreshOverlay) {
refreshOverlay.handleRuntimeError(error);
}
if (typeof isTest !== 'undefined' && isTest) {
if (window.onHotAcceptError) {
window.onHotAcceptError(error.message);
}
}
// eslint-disable-next-line
__webpack_require__.c[moduleId].hot.accept(hotErrorHandler);
},
);
if (isHotUpdate) {
if (
isReactRefreshBoundary(prevExports) &&
shouldInvalidateReactRefreshBoundary(prevExports, moduleExports)
) {
webpackHot.invalidate();
} else {
enqueueUpdate(
/**
* A function to dismiss the error overlay after performing React refresh.
* @returns {void}
*/
() => {
if (typeof refreshOverlay !== 'undefined' && refreshOverlay) {
refreshOverlay.clearRuntimeErrors();
}
},
);
}
}
} else {
if (isHotUpdate && typeof prevExports !== 'undefined') {
webpackHot.invalidate();
}
}
}
}
module.exports = Object.freeze({
enqueueUpdate: enqueueUpdate,
executeRuntime: executeRuntime,
getModuleExports: getModuleExports,
isReactRefreshBoundary: isReactRefreshBoundary,
shouldInvalidateReactRefreshBoundary: shouldInvalidateReactRefreshBoundary,
registerExportsForReactRefresh: registerExportsForReactRefresh,
});