| [9af201e] | 1 | const { validate: validateOptions } = require('schema-utils');
|
|---|
| 2 | const { getRefreshGlobalScope, getWebpackVersion } = require('./globals');
|
|---|
| 3 | const {
|
|---|
| 4 | getAdditionalEntries,
|
|---|
| 5 | getIntegrationEntry,
|
|---|
| 6 | getRefreshGlobal,
|
|---|
| 7 | getSocketIntegration,
|
|---|
| 8 | injectRefreshEntry,
|
|---|
| 9 | injectRefreshLoader,
|
|---|
| 10 | makeRefreshRuntimeModule,
|
|---|
| 11 | normalizeOptions,
|
|---|
| 12 | } = require('./utils');
|
|---|
| 13 | const schema = require('./options.json');
|
|---|
| 14 |
|
|---|
| 15 | class ReactRefreshPlugin {
|
|---|
| 16 | /**
|
|---|
| 17 | * @param {import('./types').ReactRefreshPluginOptions} [options] Options for react-refresh-plugin.
|
|---|
| 18 | */
|
|---|
| 19 | constructor(options = {}) {
|
|---|
| 20 | validateOptions(schema, options, {
|
|---|
| 21 | name: 'React Refresh Plugin',
|
|---|
| 22 | baseDataPath: 'options',
|
|---|
| 23 | });
|
|---|
| 24 |
|
|---|
| 25 | /**
|
|---|
| 26 | * @readonly
|
|---|
| 27 | * @type {import('./types').NormalizedPluginOptions}
|
|---|
| 28 | */
|
|---|
| 29 | this.options = normalizeOptions(options);
|
|---|
| 30 | }
|
|---|
| 31 |
|
|---|
| 32 | /**
|
|---|
| 33 | * Applies the plugin.
|
|---|
| 34 | * @param {import('webpack').Compiler} compiler A webpack compiler object.
|
|---|
| 35 | * @returns {void}
|
|---|
| 36 | */
|
|---|
| 37 | apply(compiler) {
|
|---|
| 38 | // Skip processing in non-development mode, but allow manual force-enabling
|
|---|
| 39 | if (
|
|---|
| 40 | // Webpack do not set process.env.NODE_ENV, so we need to check for mode.
|
|---|
| 41 | // Ref: https://github.com/webpack/webpack/issues/7074
|
|---|
| 42 | (compiler.options.mode !== 'development' ||
|
|---|
| 43 | // We also check for production process.env.NODE_ENV,
|
|---|
| 44 | // in case it was set and mode is non-development (e.g. 'none')
|
|---|
| 45 | (process.env.NODE_ENV && process.env.NODE_ENV === 'production')) &&
|
|---|
| 46 | !this.options.forceEnable
|
|---|
| 47 | ) {
|
|---|
| 48 | return;
|
|---|
| 49 | }
|
|---|
| 50 |
|
|---|
| 51 | const webpackVersion = getWebpackVersion(compiler);
|
|---|
| 52 | const logger = compiler.getInfrastructureLogger(this.constructor.name);
|
|---|
| 53 |
|
|---|
| 54 | // Get Webpack imports from compiler instance (if available) -
|
|---|
| 55 | // this allow mono-repos to use different versions of Webpack without conflicts.
|
|---|
| 56 | const webpack = compiler.webpack || require('webpack');
|
|---|
| 57 | const {
|
|---|
| 58 | DefinePlugin,
|
|---|
| 59 | EntryDependency,
|
|---|
| 60 | EntryPlugin,
|
|---|
| 61 | ModuleFilenameHelpers,
|
|---|
| 62 | NormalModule,
|
|---|
| 63 | ProvidePlugin,
|
|---|
| 64 | RuntimeGlobals,
|
|---|
| 65 | Template,
|
|---|
| 66 | } = webpack;
|
|---|
| 67 |
|
|---|
| 68 | // Inject react-refresh context to all Webpack entry points.
|
|---|
| 69 | // This should create `EntryDependency` objects when available,
|
|---|
| 70 | // and fallback to patching the `entry` object for legacy workflows.
|
|---|
| 71 | const addEntries = getAdditionalEntries({
|
|---|
| 72 | devServer: compiler.options.devServer,
|
|---|
| 73 | options: this.options,
|
|---|
| 74 | });
|
|---|
| 75 | if (EntryPlugin) {
|
|---|
| 76 | // Prepended entries does not care about injection order,
|
|---|
| 77 | // so we can utilise EntryPlugin for simpler logic.
|
|---|
| 78 | addEntries.prependEntries.forEach((entry) => {
|
|---|
| 79 | new EntryPlugin(compiler.context, entry, { name: undefined }).apply(compiler);
|
|---|
| 80 | });
|
|---|
| 81 |
|
|---|
| 82 | const integrationEntry = getIntegrationEntry(this.options.overlay.sockIntegration);
|
|---|
| 83 | const socketEntryData = [];
|
|---|
| 84 | compiler.hooks.make.tap(
|
|---|
| 85 | { name: this.constructor.name, stage: Number.POSITIVE_INFINITY },
|
|---|
| 86 | (compilation) => {
|
|---|
| 87 | // Exhaustively search all entries for `integrationEntry`.
|
|---|
| 88 | // If found, mark those entries and the index of `integrationEntry`.
|
|---|
| 89 | for (const [name, entryData] of compilation.entries.entries()) {
|
|---|
| 90 | const index = entryData.dependencies.findIndex(
|
|---|
| 91 | (dep) => dep.request && dep.request.includes(integrationEntry)
|
|---|
| 92 | );
|
|---|
| 93 | if (index !== -1) {
|
|---|
| 94 | socketEntryData.push({ name, index });
|
|---|
| 95 | }
|
|---|
| 96 | }
|
|---|
| 97 | }
|
|---|
| 98 | );
|
|---|
| 99 |
|
|---|
| 100 | // Overlay entries need to be injected AFTER integration's entry,
|
|---|
| 101 | // so we will loop through everything in `finishMake` instead of `make`.
|
|---|
| 102 | // This ensures we can traverse all entry points and inject stuff with the correct order.
|
|---|
| 103 | addEntries.overlayEntries.forEach((entry, idx, arr) => {
|
|---|
| 104 | compiler.hooks.finishMake.tapPromise(
|
|---|
| 105 | { name: this.constructor.name, stage: Number.MIN_SAFE_INTEGER + (arr.length - idx - 1) },
|
|---|
| 106 | (compilation) => {
|
|---|
| 107 | // Only hook into the current compiler
|
|---|
| 108 | if (compilation.compiler !== compiler) {
|
|---|
| 109 | return Promise.resolve();
|
|---|
| 110 | }
|
|---|
| 111 |
|
|---|
| 112 | const injectData = socketEntryData.length ? socketEntryData : [{ name: undefined }];
|
|---|
| 113 | return Promise.all(
|
|---|
| 114 | injectData.map(({ name, index }) => {
|
|---|
| 115 | return new Promise((resolve, reject) => {
|
|---|
| 116 | const options = { name };
|
|---|
| 117 | const dep = EntryPlugin.createDependency(entry, options);
|
|---|
| 118 | compilation.addEntry(compiler.context, dep, options, (err) => {
|
|---|
| 119 | if (err) return reject(err);
|
|---|
| 120 |
|
|---|
| 121 | // If the entry is not a global one,
|
|---|
| 122 | // and we have registered the index for integration entry,
|
|---|
| 123 | // we will reorder all entry dependencies to our desired order.
|
|---|
| 124 | // That is, to have additional entries DIRECTLY behind integration entry.
|
|---|
| 125 | if (name && typeof index !== 'undefined') {
|
|---|
| 126 | const entryData = compilation.entries.get(name);
|
|---|
| 127 | entryData.dependencies.splice(
|
|---|
| 128 | index + 1,
|
|---|
| 129 | 0,
|
|---|
| 130 | entryData.dependencies.splice(entryData.dependencies.length - 1, 1)[0]
|
|---|
| 131 | );
|
|---|
| 132 | }
|
|---|
| 133 |
|
|---|
| 134 | resolve();
|
|---|
| 135 | });
|
|---|
| 136 | });
|
|---|
| 137 | })
|
|---|
| 138 | ).then(() => {});
|
|---|
| 139 | }
|
|---|
| 140 | );
|
|---|
| 141 | });
|
|---|
| 142 | } else {
|
|---|
| 143 | compiler.options.entry = injectRefreshEntry(compiler.options.entry, addEntries);
|
|---|
| 144 | }
|
|---|
| 145 |
|
|---|
| 146 | // Inject necessary modules and variables to bundle's global scope
|
|---|
| 147 | const refreshGlobal = getRefreshGlobalScope(RuntimeGlobals || {});
|
|---|
| 148 | /** @type {Record<string, string | boolean>}*/
|
|---|
| 149 | const definedModules = {
|
|---|
| 150 | // Mapping of react-refresh globals to Webpack runtime globals
|
|---|
| 151 | $RefreshReg$: `${refreshGlobal}.register`,
|
|---|
| 152 | $RefreshSig$: `${refreshGlobal}.signature`,
|
|---|
| 153 | 'typeof $RefreshReg$': 'function',
|
|---|
| 154 | 'typeof $RefreshSig$': 'function',
|
|---|
| 155 |
|
|---|
| 156 | // Library mode
|
|---|
| 157 | __react_refresh_library__: JSON.stringify(
|
|---|
| 158 | Template.toIdentifier(
|
|---|
| 159 | this.options.library ||
|
|---|
| 160 | compiler.options.output.uniqueName ||
|
|---|
| 161 | compiler.options.output.library
|
|---|
| 162 | )
|
|---|
| 163 | ),
|
|---|
| 164 | };
|
|---|
| 165 | /** @type {Record<string, string>} */
|
|---|
| 166 | const providedModules = {
|
|---|
| 167 | __react_refresh_utils__: require.resolve('./runtime/RefreshUtils'),
|
|---|
| 168 | };
|
|---|
| 169 |
|
|---|
| 170 | if (this.options.overlay === false) {
|
|---|
| 171 | // Stub errorOverlay module so their calls can be erased
|
|---|
| 172 | definedModules.__react_refresh_error_overlay__ = false;
|
|---|
| 173 | definedModules.__react_refresh_polyfill_url__ = false;
|
|---|
| 174 | definedModules.__react_refresh_socket__ = false;
|
|---|
| 175 | } else {
|
|---|
| 176 | definedModules.__react_refresh_polyfill_url__ = this.options.overlay.useURLPolyfill || false;
|
|---|
| 177 |
|
|---|
| 178 | if (this.options.overlay.module) {
|
|---|
| 179 | providedModules.__react_refresh_error_overlay__ = require.resolve(
|
|---|
| 180 | this.options.overlay.module
|
|---|
| 181 | );
|
|---|
| 182 | }
|
|---|
| 183 | if (this.options.overlay.sockIntegration) {
|
|---|
| 184 | providedModules.__react_refresh_socket__ = getSocketIntegration(
|
|---|
| 185 | this.options.overlay.sockIntegration
|
|---|
| 186 | );
|
|---|
| 187 | }
|
|---|
| 188 | }
|
|---|
| 189 |
|
|---|
| 190 | new DefinePlugin(definedModules).apply(compiler);
|
|---|
| 191 | new ProvidePlugin(providedModules).apply(compiler);
|
|---|
| 192 |
|
|---|
| 193 | const match = ModuleFilenameHelpers.matchObject.bind(undefined, this.options);
|
|---|
| 194 | let loggedHotWarning = false;
|
|---|
| 195 | compiler.hooks.compilation.tap(
|
|---|
| 196 | this.constructor.name,
|
|---|
| 197 | (compilation, { normalModuleFactory }) => {
|
|---|
| 198 | // Only hook into the current compiler
|
|---|
| 199 | if (compilation.compiler !== compiler) {
|
|---|
| 200 | return;
|
|---|
| 201 | }
|
|---|
| 202 |
|
|---|
| 203 | // Tap into version-specific compilation hooks
|
|---|
| 204 | switch (webpackVersion) {
|
|---|
| 205 | case 4: {
|
|---|
| 206 | const outputOptions = compilation.mainTemplate.outputOptions;
|
|---|
| 207 | compilation.mainTemplate.hooks.require.tap(
|
|---|
| 208 | this.constructor.name,
|
|---|
| 209 | // Constructs the module template for react-refresh
|
|---|
| 210 | (source, chunk, hash) => {
|
|---|
| 211 | // Check for the output filename
|
|---|
| 212 | // This is to ensure we are processing a JS-related chunk
|
|---|
| 213 | let filename = outputOptions.filename;
|
|---|
| 214 | if (typeof filename === 'function') {
|
|---|
| 215 | // Only usage of the `chunk` property is documented by Webpack.
|
|---|
| 216 | // However, some internal Webpack plugins uses other properties,
|
|---|
| 217 | // so we also pass them through to be on the safe side.
|
|---|
| 218 | filename = filename({
|
|---|
| 219 | contentHashType: 'javascript',
|
|---|
| 220 | chunk,
|
|---|
| 221 | hash,
|
|---|
| 222 | });
|
|---|
| 223 | }
|
|---|
| 224 |
|
|---|
| 225 | // Check whether the current compilation is outputting to JS,
|
|---|
| 226 | // since other plugins can trigger compilations for other file types too.
|
|---|
| 227 | // If we apply the transform to them, their compilation will break fatally.
|
|---|
| 228 | // One prominent example of this is the HTMLWebpackPlugin.
|
|---|
| 229 | // If filename is falsy, something is terribly wrong and there's nothing we can do.
|
|---|
| 230 | if (!filename || !filename.includes('.js')) {
|
|---|
| 231 | return source;
|
|---|
| 232 | }
|
|---|
| 233 |
|
|---|
| 234 | // Split template source code into lines for easier processing
|
|---|
| 235 | const lines = source.split('\n');
|
|---|
| 236 | // Webpack generates this line when the MainTemplate is called
|
|---|
| 237 | const moduleInitializationLineNumber = lines.findIndex((line) =>
|
|---|
| 238 | line.includes('modules[moduleId].call(')
|
|---|
| 239 | );
|
|---|
| 240 | // Unable to find call to module execution -
|
|---|
| 241 | // this happens if the current module does not call MainTemplate.
|
|---|
| 242 | // In this case, we will return the original source and won't mess with it.
|
|---|
| 243 | if (moduleInitializationLineNumber === -1) {
|
|---|
| 244 | return source;
|
|---|
| 245 | }
|
|---|
| 246 |
|
|---|
| 247 | const moduleInterceptor = Template.asString([
|
|---|
| 248 | `${refreshGlobal}.setup(moduleId);`,
|
|---|
| 249 | 'try {',
|
|---|
| 250 | Template.indent(lines[moduleInitializationLineNumber]),
|
|---|
| 251 | '} finally {',
|
|---|
| 252 | Template.indent(`${refreshGlobal}.cleanup(moduleId);`),
|
|---|
| 253 | '}',
|
|---|
| 254 | ]);
|
|---|
| 255 |
|
|---|
| 256 | return Template.asString([
|
|---|
| 257 | ...lines.slice(0, moduleInitializationLineNumber),
|
|---|
| 258 | '',
|
|---|
| 259 | outputOptions.strictModuleExceptionHandling
|
|---|
| 260 | ? Template.indent(moduleInterceptor)
|
|---|
| 261 | : moduleInterceptor,
|
|---|
| 262 | '',
|
|---|
| 263 | ...lines.slice(moduleInitializationLineNumber + 1, lines.length),
|
|---|
| 264 | ]);
|
|---|
| 265 | }
|
|---|
| 266 | );
|
|---|
| 267 |
|
|---|
| 268 | compilation.mainTemplate.hooks.requireExtensions.tap(
|
|---|
| 269 | this.constructor.name,
|
|---|
| 270 | // Setup react-refresh globals as extensions to Webpack's require function
|
|---|
| 271 | (source) => {
|
|---|
| 272 | return Template.asString([source, '', getRefreshGlobal(Template)]);
|
|---|
| 273 | }
|
|---|
| 274 | );
|
|---|
| 275 |
|
|---|
| 276 | normalModuleFactory.hooks.afterResolve.tap(
|
|---|
| 277 | this.constructor.name,
|
|---|
| 278 | // Add react-refresh loader to process files that matches specified criteria
|
|---|
| 279 | (data) => {
|
|---|
| 280 | return injectRefreshLoader(data, {
|
|---|
| 281 | match,
|
|---|
| 282 | options: { const: false, esModule: false },
|
|---|
| 283 | });
|
|---|
| 284 | }
|
|---|
| 285 | );
|
|---|
| 286 |
|
|---|
| 287 | compilation.hooks.normalModuleLoader.tap(
|
|---|
| 288 | // `Number.POSITIVE_INFINITY` ensures this check will run only after all other taps
|
|---|
| 289 | { name: this.constructor.name, stage: Number.POSITIVE_INFINITY },
|
|---|
| 290 | // Check for existence of the HMR runtime -
|
|---|
| 291 | // it is the foundation to this plugin working correctly
|
|---|
| 292 | (context) => {
|
|---|
| 293 | if (!context.hot && !loggedHotWarning) {
|
|---|
| 294 | logger.warn(
|
|---|
| 295 | [
|
|---|
| 296 | 'Hot Module Replacement (HMR) is not enabled!',
|
|---|
| 297 | 'React Refresh requires HMR to function properly.',
|
|---|
| 298 | ].join(' ')
|
|---|
| 299 | );
|
|---|
| 300 | loggedHotWarning = true;
|
|---|
| 301 | }
|
|---|
| 302 | }
|
|---|
| 303 | );
|
|---|
| 304 |
|
|---|
| 305 | break;
|
|---|
| 306 | }
|
|---|
| 307 | case 5: {
|
|---|
| 308 | // Set factory for EntryDependency which is used to initialise the module
|
|---|
| 309 | compilation.dependencyFactories.set(EntryDependency, normalModuleFactory);
|
|---|
| 310 |
|
|---|
| 311 | const ReactRefreshRuntimeModule = makeRefreshRuntimeModule(webpack);
|
|---|
| 312 | compilation.hooks.additionalTreeRuntimeRequirements.tap(
|
|---|
| 313 | this.constructor.name,
|
|---|
| 314 | // Setup react-refresh globals with a Webpack runtime module
|
|---|
| 315 | (chunk, runtimeRequirements) => {
|
|---|
| 316 | runtimeRequirements.add(RuntimeGlobals.interceptModuleExecution);
|
|---|
| 317 | runtimeRequirements.add(RuntimeGlobals.moduleCache);
|
|---|
| 318 | runtimeRequirements.add(refreshGlobal);
|
|---|
| 319 | compilation.addRuntimeModule(chunk, new ReactRefreshRuntimeModule());
|
|---|
| 320 | }
|
|---|
| 321 | );
|
|---|
| 322 |
|
|---|
| 323 | normalModuleFactory.hooks.afterResolve.tap(
|
|---|
| 324 | this.constructor.name,
|
|---|
| 325 | // Add react-refresh loader to process files that matches specified criteria
|
|---|
| 326 | (resolveData) => {
|
|---|
| 327 | injectRefreshLoader(resolveData.createData, {
|
|---|
| 328 | match,
|
|---|
| 329 | options: {
|
|---|
| 330 | const: compilation.runtimeTemplate.supportsConst(),
|
|---|
| 331 | esModule: this.options.esModule,
|
|---|
| 332 | },
|
|---|
| 333 | });
|
|---|
| 334 | }
|
|---|
| 335 | );
|
|---|
| 336 |
|
|---|
| 337 | NormalModule.getCompilationHooks(compilation).loader.tap(
|
|---|
| 338 | // `Infinity` ensures this check will run only after all other taps
|
|---|
| 339 | { name: this.constructor.name, stage: Infinity },
|
|---|
| 340 | // Check for existence of the HMR runtime -
|
|---|
| 341 | // it is the foundation to this plugin working correctly
|
|---|
| 342 | (context) => {
|
|---|
| 343 | if (!context.hot && !loggedHotWarning) {
|
|---|
| 344 | logger.warn(
|
|---|
| 345 | [
|
|---|
| 346 | 'Hot Module Replacement (HMR) is not enabled!',
|
|---|
| 347 | 'React Refresh requires HMR to function properly.',
|
|---|
| 348 | ].join(' ')
|
|---|
| 349 | );
|
|---|
| 350 | loggedHotWarning = true;
|
|---|
| 351 | }
|
|---|
| 352 | }
|
|---|
| 353 | );
|
|---|
| 354 |
|
|---|
| 355 | break;
|
|---|
| 356 | }
|
|---|
| 357 | default: {
|
|---|
| 358 | // Do nothing - this should be an impossible case
|
|---|
| 359 | }
|
|---|
| 360 | }
|
|---|
| 361 | }
|
|---|
| 362 | );
|
|---|
| 363 | }
|
|---|
| 364 | }
|
|---|
| 365 |
|
|---|
| 366 | module.exports.ReactRefreshPlugin = ReactRefreshPlugin;
|
|---|
| 367 | module.exports = ReactRefreshPlugin;
|
|---|