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}*/ 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} */ 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;