/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const Template = require("./Template"); const ModuleHotAcceptDependency = require("./dependencies/ModuleHotAcceptDependency"); const ModuleHotDeclineDependency = require("./dependencies/ModuleHotDeclineDependency"); const RawSource = require("webpack-sources").RawSource; const ConstDependency = require("./dependencies/ConstDependency"); const NullFactory = require("./NullFactory"); const ParserHelpers = require("./ParserHelpers"); const createHash = require("./util/createHash"); const SyncBailHook = require("tapable").SyncBailHook; 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(); }); compiler.hooks.compilation.tap("HotModuleReplacementPlugin", (compilation, { normalModuleFactory }) => { 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 = {}; compilation.modules.forEach(module => { const identifier = module.identifier(); const hash = createHash(compilation.outputOptions.hashFunction); module.updateHash(hash); records.moduleHashs[identifier] = hash.digest("hex"); }); records.chunkHashs = {}; compilation.chunks.forEach(chunk => { records.chunkHashs[chunk.id] = chunk.hash; }); records.chunkModuleIds = {}; compilation.chunks.forEach(chunk => { records.chunkModuleIds[chunk.id] = chunk.mapModules(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; compilation.modules.forEach(module => { const identifier = module.identifier(); let hash = createHash(compilation.outputOptions.hashFunction); module.updateHash(hash); hash = hash.digest("hex"); module.hotUpdate = records.moduleHashs[identifier] !== hash; }); const hotUpdateMainContent = { h: compilation.hash, c: {}, }; Object.keys(records.chunkHashs).forEach(chunkId => { chunkId = isNaN(+chunkId) ? chunkId : +chunkId; const currentChunk = compilation.chunks.find(chunk => chunk.id === chunkId); 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 filename = compilation.getPath(hotUpdateChunkFilename, { hash: records.hash, chunk: currentChunk }); compilation.additionalChunkAssets.push(filename); compilation.assets[filename] = source; hotUpdateMainContent.c[chunkId] = true; currentChunk.files.push(filename); compilation.applyPlugins("chunk-asset", currentChunk, filename); } } else { hotUpdateMainContent.c[chunkId] = false; } }, compilation); const source = new RawSource(JSON.stringify(hotUpdateMainContent)); const filename = compilation.getPath(hotUpdateMainFilename, { hash: records.hash }); compilation.assets[filename] = source; }); 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: []" ]); }); const handler = (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("HotModuleReplacementPlugin", 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); else parser.hooks.hotAcceptWithoutCallback.call(expr, requests); } } }); 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); }; // TODO add HMR support for javascript/esm normalModuleFactory.hooks.parser.for("javascript/auto").tap("HotModuleReplacementPlugin", handler); normalModuleFactory.hooks.parser.for("javascript/dynamic").tap("HotModuleReplacementPlugin", handler); compilation.hooks.normalModuleLoader.tap("HotModuleReplacementPlugin", context => { context.hot = true; }); }); } }; const hotInitCode = Template.getFunctionContent(require("./HotModuleReplacement.runtime.js"));