| /* |
| MIT License http://www.opensource.org/licenses/mit-license.php |
| Author Tobias Koppers @sokra |
| */ |
| "use strict"; |
| |
| const { SyncBailHook } = require("tapable"); |
| const { RawSource } = require("webpack-sources"); |
| const Template = require("./Template"); |
| const ModuleHotAcceptDependency = require("./dependencies/ModuleHotAcceptDependency"); |
| const ModuleHotDeclineDependency = require("./dependencies/ModuleHotDeclineDependency"); |
| const ConstDependency = require("./dependencies/ConstDependency"); |
| const NullFactory = require("./NullFactory"); |
| const ParserHelpers = require("./ParserHelpers"); |
| |
| module.exports = class HotModuleReplacementPlugin { |
| constructor(options) { |
| this.options = options || {}; |
| this.multiStep = this.options.multiStep; |
| this.fullBuildTimeout = this.options.fullBuildTimeout || 200; |
| this.requestTimeout = this.options.requestTimeout || 10000; |
| } |
| |
| apply(compiler) { |
| const multiStep = this.multiStep; |
| const fullBuildTimeout = this.fullBuildTimeout; |
| const requestTimeout = this.requestTimeout; |
| const hotUpdateChunkFilename = |
| compiler.options.output.hotUpdateChunkFilename; |
| const hotUpdateMainFilename = compiler.options.output.hotUpdateMainFilename; |
| compiler.hooks.additionalPass.tapAsync( |
| "HotModuleReplacementPlugin", |
| callback => { |
| if (multiStep) return setTimeout(callback, fullBuildTimeout); |
| return callback(); |
| } |
| ); |
| |
| const addParserPlugins = (parser, parserOptions) => { |
| parser.hooks.expression |
| .for("__webpack_hash__") |
| .tap( |
| "HotModuleReplacementPlugin", |
| ParserHelpers.toConstantDependencyWithWebpackRequire( |
| parser, |
| "__webpack_require__.h()" |
| ) |
| ); |
| parser.hooks.evaluateTypeof |
| .for("__webpack_hash__") |
| .tap( |
| "HotModuleReplacementPlugin", |
| ParserHelpers.evaluateToString("string") |
| ); |
| parser.hooks.evaluateIdentifier.for("module.hot").tap( |
| { |
| name: "HotModuleReplacementPlugin", |
| before: "NodeStuffPlugin" |
| }, |
| expr => { |
| return ParserHelpers.evaluateToIdentifier( |
| "module.hot", |
| !!parser.state.compilation.hotUpdateChunkTemplate |
| )(expr); |
| } |
| ); |
| // TODO webpack 5: refactor this, no custom hooks |
| if (!parser.hooks.hotAcceptCallback) { |
| parser.hooks.hotAcceptCallback = new SyncBailHook([ |
| "expression", |
| "requests" |
| ]); |
| } |
| if (!parser.hooks.hotAcceptWithoutCallback) { |
| parser.hooks.hotAcceptWithoutCallback = new SyncBailHook([ |
| "expression", |
| "requests" |
| ]); |
| } |
| parser.hooks.call |
| .for("module.hot.accept") |
| .tap("HotModuleReplacementPlugin", expr => { |
| if (!parser.state.compilation.hotUpdateChunkTemplate) { |
| return false; |
| } |
| if (expr.arguments.length >= 1) { |
| const arg = parser.evaluateExpression(expr.arguments[0]); |
| let params = []; |
| let requests = []; |
| if (arg.isString()) { |
| params = [arg]; |
| } else if (arg.isArray()) { |
| params = arg.items.filter(param => param.isString()); |
| } |
| if (params.length > 0) { |
| params.forEach((param, idx) => { |
| const request = param.string; |
| const dep = new ModuleHotAcceptDependency(request, param.range); |
| dep.optional = true; |
| dep.loc = Object.create(expr.loc); |
| dep.loc.index = idx; |
| parser.state.module.addDependency(dep); |
| requests.push(request); |
| }); |
| if (expr.arguments.length > 1) { |
| parser.hooks.hotAcceptCallback.call( |
| expr.arguments[1], |
| requests |
| ); |
| parser.walkExpression(expr.arguments[1]); // other args are ignored |
| return true; |
| } else { |
| parser.hooks.hotAcceptWithoutCallback.call(expr, requests); |
| return true; |
| } |
| } |
| } |
| }); |
| parser.hooks.call |
| .for("module.hot.decline") |
| .tap("HotModuleReplacementPlugin", expr => { |
| if (!parser.state.compilation.hotUpdateChunkTemplate) { |
| return false; |
| } |
| if (expr.arguments.length === 1) { |
| const arg = parser.evaluateExpression(expr.arguments[0]); |
| let params = []; |
| if (arg.isString()) { |
| params = [arg]; |
| } else if (arg.isArray()) { |
| params = arg.items.filter(param => param.isString()); |
| } |
| params.forEach((param, idx) => { |
| const dep = new ModuleHotDeclineDependency( |
| param.string, |
| param.range |
| ); |
| dep.optional = true; |
| dep.loc = Object.create(expr.loc); |
| dep.loc.index = idx; |
| parser.state.module.addDependency(dep); |
| }); |
| } |
| }); |
| parser.hooks.expression |
| .for("module.hot") |
| .tap("HotModuleReplacementPlugin", ParserHelpers.skipTraversal); |
| }; |
| |
| compiler.hooks.compilation.tap( |
| "HotModuleReplacementPlugin", |
| (compilation, { normalModuleFactory }) => { |
| // This applies the HMR plugin only to the targeted compiler |
| // It should not affect child compilations |
| if (compilation.compiler !== compiler) return; |
| |
| const hotUpdateChunkTemplate = compilation.hotUpdateChunkTemplate; |
| if (!hotUpdateChunkTemplate) return; |
| |
| compilation.dependencyFactories.set(ConstDependency, new NullFactory()); |
| compilation.dependencyTemplates.set( |
| ConstDependency, |
| new ConstDependency.Template() |
| ); |
| |
| compilation.dependencyFactories.set( |
| ModuleHotAcceptDependency, |
| normalModuleFactory |
| ); |
| compilation.dependencyTemplates.set( |
| ModuleHotAcceptDependency, |
| new ModuleHotAcceptDependency.Template() |
| ); |
| |
| compilation.dependencyFactories.set( |
| ModuleHotDeclineDependency, |
| normalModuleFactory |
| ); |
| compilation.dependencyTemplates.set( |
| ModuleHotDeclineDependency, |
| new ModuleHotDeclineDependency.Template() |
| ); |
| |
| compilation.hooks.record.tap( |
| "HotModuleReplacementPlugin", |
| (compilation, records) => { |
| if (records.hash === compilation.hash) return; |
| records.hash = compilation.hash; |
| records.moduleHashs = {}; |
| for (const module of compilation.modules) { |
| const identifier = module.identifier(); |
| records.moduleHashs[identifier] = module.hash; |
| } |
| records.chunkHashs = {}; |
| for (const chunk of compilation.chunks) { |
| records.chunkHashs[chunk.id] = chunk.hash; |
| } |
| records.chunkModuleIds = {}; |
| for (const chunk of compilation.chunks) { |
| records.chunkModuleIds[chunk.id] = Array.from( |
| chunk.modulesIterable, |
| m => m.id |
| ); |
| } |
| } |
| ); |
| let initialPass = false; |
| let recompilation = false; |
| compilation.hooks.afterHash.tap("HotModuleReplacementPlugin", () => { |
| let records = compilation.records; |
| if (!records) { |
| initialPass = true; |
| return; |
| } |
| if (!records.hash) initialPass = true; |
| const preHash = records.preHash || "x"; |
| const prepreHash = records.prepreHash || "x"; |
| if (preHash === compilation.hash) { |
| recompilation = true; |
| compilation.modifyHash(prepreHash); |
| return; |
| } |
| records.prepreHash = records.hash || "x"; |
| records.preHash = compilation.hash; |
| compilation.modifyHash(records.prepreHash); |
| }); |
| compilation.hooks.shouldGenerateChunkAssets.tap( |
| "HotModuleReplacementPlugin", |
| () => { |
| if (multiStep && !recompilation && !initialPass) return false; |
| } |
| ); |
| compilation.hooks.needAdditionalPass.tap( |
| "HotModuleReplacementPlugin", |
| () => { |
| if (multiStep && !recompilation && !initialPass) return true; |
| } |
| ); |
| compilation.hooks.additionalChunkAssets.tap( |
| "HotModuleReplacementPlugin", |
| () => { |
| const records = compilation.records; |
| if (records.hash === compilation.hash) return; |
| if ( |
| !records.moduleHashs || |
| !records.chunkHashs || |
| !records.chunkModuleIds |
| ) |
| return; |
| for (const module of compilation.modules) { |
| const identifier = module.identifier(); |
| let hash = module.hash; |
| module.hotUpdate = records.moduleHashs[identifier] !== hash; |
| } |
| const hotUpdateMainContent = { |
| h: compilation.hash, |
| c: {} |
| }; |
| for (const key of Object.keys(records.chunkHashs)) { |
| const chunkId = isNaN(+key) ? key : +key; |
| const currentChunk = compilation.chunks.find( |
| chunk => `${chunk.id}` === key |
| ); |
| if (currentChunk) { |
| const newModules = currentChunk |
| .getModules() |
| .filter(module => module.hotUpdate); |
| const allModules = new Set(); |
| for (const module of currentChunk.modulesIterable) { |
| allModules.add(module.id); |
| } |
| const removedModules = records.chunkModuleIds[chunkId].filter( |
| id => !allModules.has(id) |
| ); |
| if (newModules.length > 0 || removedModules.length > 0) { |
| const source = hotUpdateChunkTemplate.render( |
| chunkId, |
| newModules, |
| removedModules, |
| compilation.hash, |
| compilation.moduleTemplates.javascript, |
| compilation.dependencyTemplates |
| ); |
| const { |
| path: filename, |
| info: assetInfo |
| } = compilation.getPathWithInfo(hotUpdateChunkFilename, { |
| hash: records.hash, |
| chunk: currentChunk |
| }); |
| compilation.additionalChunkAssets.push(filename); |
| compilation.emitAsset( |
| filename, |
| source, |
| Object.assign({ hotModuleReplacement: true }, assetInfo) |
| ); |
| hotUpdateMainContent.c[chunkId] = true; |
| currentChunk.files.push(filename); |
| compilation.hooks.chunkAsset.call(currentChunk, filename); |
| } |
| } else { |
| hotUpdateMainContent.c[chunkId] = false; |
| } |
| } |
| const source = new RawSource(JSON.stringify(hotUpdateMainContent)); |
| const { |
| path: filename, |
| info: assetInfo |
| } = compilation.getPathWithInfo(hotUpdateMainFilename, { |
| hash: records.hash |
| }); |
| compilation.emitAsset( |
| filename, |
| source, |
| Object.assign({ hotModuleReplacement: true }, assetInfo) |
| ); |
| } |
| ); |
| |
| const mainTemplate = compilation.mainTemplate; |
| |
| mainTemplate.hooks.hash.tap("HotModuleReplacementPlugin", hash => { |
| hash.update("HotMainTemplateDecorator"); |
| }); |
| |
| mainTemplate.hooks.moduleRequire.tap( |
| "HotModuleReplacementPlugin", |
| (_, chunk, hash, varModuleId) => { |
| return `hotCreateRequire(${varModuleId})`; |
| } |
| ); |
| |
| mainTemplate.hooks.requireExtensions.tap( |
| "HotModuleReplacementPlugin", |
| source => { |
| const buf = [source]; |
| buf.push(""); |
| buf.push("// __webpack_hash__"); |
| buf.push( |
| mainTemplate.requireFn + |
| ".h = function() { return hotCurrentHash; };" |
| ); |
| return Template.asString(buf); |
| } |
| ); |
| |
| const needChunkLoadingCode = chunk => { |
| for (const chunkGroup of chunk.groupsIterable) { |
| if (chunkGroup.chunks.length > 1) return true; |
| if (chunkGroup.getNumberOfChildren() > 0) return true; |
| } |
| return false; |
| }; |
| |
| mainTemplate.hooks.bootstrap.tap( |
| "HotModuleReplacementPlugin", |
| (source, chunk, hash) => { |
| source = mainTemplate.hooks.hotBootstrap.call(source, chunk, hash); |
| return Template.asString([ |
| source, |
| "", |
| hotInitCode |
| .replace(/\$require\$/g, mainTemplate.requireFn) |
| .replace(/\$hash\$/g, JSON.stringify(hash)) |
| .replace(/\$requestTimeout\$/g, requestTimeout) |
| .replace( |
| /\/\*foreachInstalledChunks\*\//g, |
| needChunkLoadingCode(chunk) |
| ? "for(var chunkId in installedChunks)" |
| : `var chunkId = ${JSON.stringify(chunk.id)};` |
| ) |
| ]); |
| } |
| ); |
| |
| mainTemplate.hooks.globalHash.tap( |
| "HotModuleReplacementPlugin", |
| () => true |
| ); |
| |
| mainTemplate.hooks.currentHash.tap( |
| "HotModuleReplacementPlugin", |
| (_, length) => { |
| if (isFinite(length)) { |
| return `hotCurrentHash.substr(0, ${length})`; |
| } else { |
| return "hotCurrentHash"; |
| } |
| } |
| ); |
| |
| mainTemplate.hooks.moduleObj.tap( |
| "HotModuleReplacementPlugin", |
| (source, chunk, hash, varModuleId) => { |
| return Template.asString([ |
| `${source},`, |
| `hot: hotCreateModule(${varModuleId}),`, |
| "parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),", |
| "children: []" |
| ]); |
| } |
| ); |
| |
| // TODO add HMR support for javascript/esm |
| normalModuleFactory.hooks.parser |
| .for("javascript/auto") |
| .tap("HotModuleReplacementPlugin", addParserPlugins); |
| normalModuleFactory.hooks.parser |
| .for("javascript/dynamic") |
| .tap("HotModuleReplacementPlugin", addParserPlugins); |
| |
| compilation.hooks.normalModuleLoader.tap( |
| "HotModuleReplacementPlugin", |
| context => { |
| context.hot = true; |
| } |
| ); |
| } |
| ); |
| } |
| }; |
| |
| const hotInitCode = Template.getFunctionContent( |
| require("./HotModuleReplacement.runtime") |
| ); |