|
|
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 { |
|
|
|
|
|
|
|
|
|
|
|
constructor(options = {}) { |
|
|
validateOptions(schema, options, { |
|
|
name: 'React Refresh Plugin', |
|
|
baseDataPath: 'options', |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.options = normalizeOptions(options); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
apply(compiler) { |
|
|
|
|
|
if ( |
|
|
|
|
|
|
|
|
(compiler.options.mode !== 'development' || |
|
|
|
|
|
|
|
|
(process.env.NODE_ENV && process.env.NODE_ENV === 'production')) && |
|
|
!this.options.forceEnable |
|
|
) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const webpackVersion = getWebpackVersion(compiler); |
|
|
const logger = compiler.getInfrastructureLogger(this.constructor.name); |
|
|
|
|
|
|
|
|
|
|
|
const webpack = compiler.webpack || require('webpack'); |
|
|
const { |
|
|
DefinePlugin, |
|
|
EntryDependency, |
|
|
EntryPlugin, |
|
|
ModuleFilenameHelpers, |
|
|
NormalModule, |
|
|
ProvidePlugin, |
|
|
RuntimeGlobals, |
|
|
Template, |
|
|
} = webpack; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const addEntries = getAdditionalEntries({ |
|
|
devServer: compiler.options.devServer, |
|
|
options: this.options, |
|
|
}); |
|
|
if (EntryPlugin) { |
|
|
|
|
|
|
|
|
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) => { |
|
|
|
|
|
|
|
|
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 }); |
|
|
} |
|
|
} |
|
|
} |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addEntries.overlayEntries.forEach((entry, idx, arr) => { |
|
|
compiler.hooks.finishMake.tapPromise( |
|
|
{ name: this.constructor.name, stage: Number.MIN_SAFE_INTEGER + (arr.length - idx - 1) }, |
|
|
(compilation) => { |
|
|
|
|
|
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 (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); |
|
|
} |
|
|
|
|
|
|
|
|
const refreshGlobal = getRefreshGlobalScope(RuntimeGlobals || {}); |
|
|
|
|
|
const definedModules = { |
|
|
|
|
|
$RefreshReg$: `${refreshGlobal}.register`, |
|
|
$RefreshSig$: `${refreshGlobal}.signature`, |
|
|
'typeof $RefreshReg$': 'function', |
|
|
'typeof $RefreshSig$': 'function', |
|
|
|
|
|
|
|
|
__react_refresh_library__: JSON.stringify( |
|
|
Template.toIdentifier( |
|
|
this.options.library || |
|
|
compiler.options.output.uniqueName || |
|
|
compiler.options.output.library |
|
|
) |
|
|
), |
|
|
}; |
|
|
|
|
|
const providedModules = { |
|
|
__react_refresh_utils__: require.resolve('./runtime/RefreshUtils'), |
|
|
}; |
|
|
|
|
|
if (this.options.overlay === false) { |
|
|
|
|
|
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 }) => { |
|
|
|
|
|
if (compilation.compiler !== compiler) { |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
switch (webpackVersion) { |
|
|
case 4: { |
|
|
const outputOptions = compilation.mainTemplate.outputOptions; |
|
|
compilation.mainTemplate.hooks.require.tap( |
|
|
this.constructor.name, |
|
|
|
|
|
(source, chunk, hash) => { |
|
|
|
|
|
|
|
|
let filename = outputOptions.filename; |
|
|
if (typeof filename === 'function') { |
|
|
|
|
|
|
|
|
|
|
|
filename = filename({ |
|
|
contentHashType: 'javascript', |
|
|
chunk, |
|
|
hash, |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!filename || !filename.includes('.js')) { |
|
|
return source; |
|
|
} |
|
|
|
|
|
|
|
|
const lines = source.split('\n'); |
|
|
|
|
|
const moduleInitializationLineNumber = lines.findIndex((line) => |
|
|
line.includes('modules[moduleId].call(') |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
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, |
|
|
|
|
|
(source) => { |
|
|
return Template.asString([source, '', getRefreshGlobal(Template)]); |
|
|
} |
|
|
); |
|
|
|
|
|
normalModuleFactory.hooks.afterResolve.tap( |
|
|
this.constructor.name, |
|
|
|
|
|
(data) => { |
|
|
return injectRefreshLoader(data, { |
|
|
match, |
|
|
options: { const: false, esModule: false }, |
|
|
}); |
|
|
} |
|
|
); |
|
|
|
|
|
compilation.hooks.normalModuleLoader.tap( |
|
|
|
|
|
{ name: this.constructor.name, stage: Number.POSITIVE_INFINITY }, |
|
|
|
|
|
|
|
|
(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: { |
|
|
|
|
|
compilation.dependencyFactories.set(EntryDependency, normalModuleFactory); |
|
|
|
|
|
const ReactRefreshRuntimeModule = makeRefreshRuntimeModule(webpack); |
|
|
compilation.hooks.additionalTreeRuntimeRequirements.tap( |
|
|
this.constructor.name, |
|
|
|
|
|
(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, |
|
|
|
|
|
(resolveData) => { |
|
|
injectRefreshLoader(resolveData.createData, { |
|
|
match, |
|
|
options: { |
|
|
const: compilation.runtimeTemplate.supportsConst(), |
|
|
esModule: this.options.esModule, |
|
|
}, |
|
|
}); |
|
|
} |
|
|
); |
|
|
|
|
|
NormalModule.getCompilationHooks(compilation).loader.tap( |
|
|
|
|
|
{ name: this.constructor.name, stage: Infinity }, |
|
|
|
|
|
|
|
|
(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: { |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
module.exports.ReactRefreshPlugin = ReactRefreshPlugin; |
|
|
module.exports = ReactRefreshPlugin; |
|
|
|