UNPKG

webpack

Version:

Packs CommonJs/AMD modules for the browser. Allows to split your codebase into multiple bundles, which can be loaded on demand. Support loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.

426 lines (403 loc) 12.9 kB
/* 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") );