Spaces:
Sleeping
Sleeping
const { validate: validateOptions } = require('schema-utils'); | |
const { getRefreshGlobalScope, getWebpackVersion } = require('./globals'); | |
const { | |
getAdditionalEntries, | |
getIntegrationEntry, | |
getRefreshGlobal, | |
getSocketIntegration, | |
injectRefreshEntry, | |
injectRefreshLoader, | |
makeRefreshRuntimeModule, | |
normalizeOptions, | |
} = require('./utils'); | |
const schema = require('./options.json'); | |
class ReactRefreshPlugin { | |
/** | |
* @param {import('./types').ReactRefreshPluginOptions} [options] Options for react-refresh-plugin. | |
*/ | |
constructor(options = {}) { | |
validateOptions(schema, options, { | |
name: 'React Refresh Plugin', | |
baseDataPath: 'options', | |
}); | |
/** | |
* @readonly | |
* @type {import('./types').NormalizedPluginOptions} | |
*/ | |
this.options = normalizeOptions(options); | |
} | |
/** | |
* Applies the plugin. | |
* @param {import('webpack').Compiler} compiler A webpack compiler object. | |
* @returns {void} | |
*/ | |
apply(compiler) { | |
// Skip processing in non-development mode, but allow manual force-enabling | |
if ( | |
// Webpack do not set process.env.NODE_ENV, so we need to check for mode. | |
// Ref: https://github.com/webpack/webpack/issues/7074 | |
(compiler.options.mode !== 'development' || | |
// We also check for production process.env.NODE_ENV, | |
// in case it was set and mode is non-development (e.g. 'none') | |
(process.env.NODE_ENV && process.env.NODE_ENV === 'production')) && | |
!this.options.forceEnable | |
) { | |
return; | |
} | |
const webpackVersion = getWebpackVersion(compiler); | |
const logger = compiler.getInfrastructureLogger(this.constructor.name); | |
// Get Webpack imports from compiler instance (if available) - | |
// this allow mono-repos to use different versions of Webpack without conflicts. | |
const webpack = compiler.webpack || require('webpack'); | |
const { | |
DefinePlugin, | |
EntryDependency, | |
EntryPlugin, | |
ModuleFilenameHelpers, | |
NormalModule, | |
ProvidePlugin, | |
RuntimeGlobals, | |
Template, | |
} = webpack; | |
// Inject react-refresh context to all Webpack entry points. | |
// This should create `EntryDependency` objects when available, | |
// and fallback to patching the `entry` object for legacy workflows. | |
const addEntries = getAdditionalEntries({ | |
devServer: compiler.options.devServer, | |
options: this.options, | |
}); | |
if (EntryPlugin) { | |
// Prepended entries does not care about injection order, | |
// so we can utilise EntryPlugin for simpler logic. | |
addEntries.prependEntries.forEach((entry) => { | |
new EntryPlugin(compiler.context, entry, { name: undefined }).apply(compiler); | |
}); | |
const integrationEntry = getIntegrationEntry(this.options.overlay.sockIntegration); | |
const socketEntryData = []; | |
compiler.hooks.make.tap( | |
{ name: this.constructor.name, stage: Number.POSITIVE_INFINITY }, | |
(compilation) => { | |
// Exhaustively search all entries for `integrationEntry`. | |
// If found, mark those entries and the index of `integrationEntry`. | |
for (const [name, entryData] of compilation.entries.entries()) { | |
const index = entryData.dependencies.findIndex( | |
(dep) => dep.request && dep.request.includes(integrationEntry) | |
); | |
if (index !== -1) { | |
socketEntryData.push({ name, index }); | |
} | |
} | |
} | |
); | |
// Overlay entries need to be injected AFTER integration's entry, | |
// so we will loop through everything in `finishMake` instead of `make`. | |
// This ensures we can traverse all entry points and inject stuff with the correct order. | |
addEntries.overlayEntries.forEach((entry, idx, arr) => { | |
compiler.hooks.finishMake.tapPromise( | |
{ name: this.constructor.name, stage: Number.MIN_SAFE_INTEGER + (arr.length - idx - 1) }, | |
(compilation) => { | |
// Only hook into the current compiler | |
if (compilation.compiler !== compiler) { | |
return Promise.resolve(); | |
} | |
const injectData = socketEntryData.length ? socketEntryData : [{ name: undefined }]; | |
return Promise.all( | |
injectData.map(({ name, index }) => { | |
return new Promise((resolve, reject) => { | |
const options = { name }; | |
const dep = EntryPlugin.createDependency(entry, options); | |
compilation.addEntry(compiler.context, dep, options, (err) => { | |
if (err) return reject(err); | |
// If the entry is not a global one, | |
// and we have registered the index for integration entry, | |
// we will reorder all entry dependencies to our desired order. | |
// That is, to have additional entries DIRECTLY behind integration entry. | |
if (name && typeof index !== 'undefined') { | |
const entryData = compilation.entries.get(name); | |
entryData.dependencies.splice( | |
index + 1, | |
0, | |
entryData.dependencies.splice(entryData.dependencies.length - 1, 1)[0] | |
); | |
} | |
resolve(); | |
}); | |
}); | |
}) | |
).then(() => {}); | |
} | |
); | |
}); | |
} else { | |
compiler.options.entry = injectRefreshEntry(compiler.options.entry, addEntries); | |
} | |
// Inject necessary modules and variables to bundle's global scope | |
const refreshGlobal = getRefreshGlobalScope(RuntimeGlobals || {}); | |
/** @type {Record<string, string | boolean>}*/ | |
const definedModules = { | |
// Mapping of react-refresh globals to Webpack runtime globals | |
$RefreshReg$: `${refreshGlobal}.register`, | |
$RefreshSig$: `${refreshGlobal}.signature`, | |
'typeof $RefreshReg$': 'function', | |
'typeof $RefreshSig$': 'function', | |
// Library mode | |
__react_refresh_library__: JSON.stringify( | |
Template.toIdentifier( | |
this.options.library || | |
compiler.options.output.uniqueName || | |
compiler.options.output.library | |
) | |
), | |
}; | |
/** @type {Record<string, string>} */ | |
const providedModules = { | |
__react_refresh_utils__: require.resolve('./runtime/RefreshUtils'), | |
}; | |
if (this.options.overlay === false) { | |
// Stub errorOverlay module so their calls can be erased | |
definedModules.__react_refresh_error_overlay__ = false; | |
definedModules.__react_refresh_polyfill_url__ = false; | |
definedModules.__react_refresh_socket__ = false; | |
} else { | |
definedModules.__react_refresh_polyfill_url__ = this.options.overlay.useURLPolyfill || false; | |
if (this.options.overlay.module) { | |
providedModules.__react_refresh_error_overlay__ = require.resolve( | |
this.options.overlay.module | |
); | |
} | |
if (this.options.overlay.sockIntegration) { | |
providedModules.__react_refresh_socket__ = getSocketIntegration( | |
this.options.overlay.sockIntegration | |
); | |
} | |
} | |
new DefinePlugin(definedModules).apply(compiler); | |
new ProvidePlugin(providedModules).apply(compiler); | |
const match = ModuleFilenameHelpers.matchObject.bind(undefined, this.options); | |
let loggedHotWarning = false; | |
compiler.hooks.compilation.tap( | |
this.constructor.name, | |
(compilation, { normalModuleFactory }) => { | |
// Only hook into the current compiler | |
if (compilation.compiler !== compiler) { | |
return; | |
} | |
// Tap into version-specific compilation hooks | |
switch (webpackVersion) { | |
case 4: { | |
const outputOptions = compilation.mainTemplate.outputOptions; | |
compilation.mainTemplate.hooks.require.tap( | |
this.constructor.name, | |
// Constructs the module template for react-refresh | |
(source, chunk, hash) => { | |
// Check for the output filename | |
// This is to ensure we are processing a JS-related chunk | |
let filename = 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({ | |
contentHashType: 'javascript', | |
chunk, | |
hash, | |
}); | |
} | |
// 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.includes('modules[moduleId].call(') | |
); | |
// Unable to find call to module execution - | |
// this happens if the current module does not call MainTemplate. | |
// In this case, we will return the original source and won't mess with it. | |
if (moduleInitializationLineNumber === -1) { | |
return source; | |
} | |
const moduleInterceptor = Template.asString([ | |
`${refreshGlobal}.setup(moduleId);`, | |
'try {', | |
Template.indent(lines[moduleInitializationLineNumber]), | |
'} finally {', | |
Template.indent(`${refreshGlobal}.cleanup(moduleId);`), | |
'}', | |
]); | |
return Template.asString([ | |
...lines.slice(0, moduleInitializationLineNumber), | |
'', | |
outputOptions.strictModuleExceptionHandling | |
? Template.indent(moduleInterceptor) | |
: moduleInterceptor, | |
'', | |
...lines.slice(moduleInitializationLineNumber + 1, lines.length), | |
]); | |
} | |
); | |
compilation.mainTemplate.hooks.requireExtensions.tap( | |
this.constructor.name, | |
// Setup react-refresh globals as extensions to Webpack's require function | |
(source) => { | |
return Template.asString([source, '', getRefreshGlobal(Template)]); | |
} | |
); | |
normalModuleFactory.hooks.afterResolve.tap( | |
this.constructor.name, | |
// Add react-refresh loader to process files that matches specified criteria | |
(data) => { | |
return injectRefreshLoader(data, { | |
match, | |
options: { const: false, esModule: false }, | |
}); | |
} | |
); | |
compilation.hooks.normalModuleLoader.tap( | |
// `Number.POSITIVE_INFINITY` ensures this check will run only after all other taps | |
{ name: this.constructor.name, stage: Number.POSITIVE_INFINITY }, | |
// Check for existence of the HMR runtime - | |
// it is the foundation to this plugin working correctly | |
(context) => { | |
if (!context.hot && !loggedHotWarning) { | |
logger.warn( | |
[ | |
'Hot Module Replacement (HMR) is not enabled!', | |
'React Refresh requires HMR to function properly.', | |
].join(' ') | |
); | |
loggedHotWarning = true; | |
} | |
} | |
); | |
break; | |
} | |
case 5: { | |
// Set factory for EntryDependency which is used to initialise the module | |
compilation.dependencyFactories.set(EntryDependency, normalModuleFactory); | |
const ReactRefreshRuntimeModule = makeRefreshRuntimeModule(webpack); | |
compilation.hooks.additionalTreeRuntimeRequirements.tap( | |
this.constructor.name, | |
// Setup react-refresh globals with a Webpack runtime module | |
(chunk, runtimeRequirements) => { | |
runtimeRequirements.add(RuntimeGlobals.interceptModuleExecution); | |
runtimeRequirements.add(RuntimeGlobals.moduleCache); | |
runtimeRequirements.add(refreshGlobal); | |
compilation.addRuntimeModule(chunk, new ReactRefreshRuntimeModule()); | |
} | |
); | |
normalModuleFactory.hooks.afterResolve.tap( | |
this.constructor.name, | |
// Add react-refresh loader to process files that matches specified criteria | |
(resolveData) => { | |
injectRefreshLoader(resolveData.createData, { | |
match, | |
options: { | |
const: compilation.runtimeTemplate.supportsConst(), | |
esModule: this.options.esModule, | |
}, | |
}); | |
} | |
); | |
NormalModule.getCompilationHooks(compilation).loader.tap( | |
// `Infinity` ensures this check will run only after all other taps | |
{ name: this.constructor.name, stage: Infinity }, | |
// Check for existence of the HMR runtime - | |
// it is the foundation to this plugin working correctly | |
(context) => { | |
if (!context.hot && !loggedHotWarning) { | |
logger.warn( | |
[ | |
'Hot Module Replacement (HMR) is not enabled!', | |
'React Refresh requires HMR to function properly.', | |
].join(' ') | |
); | |
loggedHotWarning = true; | |
} | |
} | |
); | |
break; | |
} | |
default: { | |
// Do nothing - this should be an impossible case | |
} | |
} | |
} | |
); | |
} | |
} | |
module.exports.ReactRefreshPlugin = ReactRefreshPlugin; | |
module.exports = ReactRefreshPlugin; | |