mirror of https://github.com/webpack/webpack.git
				
				
				
			
		
			
				
	
	
		
			407 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			407 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
/*
 | 
						|
	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 = {};
 | 
						|
						for (const module of compilation.modules) {
 | 
						|
							const identifier = module.identifier();
 | 
						|
							const hash = createHash(compilation.outputOptions.hashFunction);
 | 
						|
							module.updateHash(hash);
 | 
						|
							records.moduleHashs[identifier] = hash.digest("hex");
 | 
						|
						}
 | 
						|
						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 = createHash(compilation.outputOptions.hashFunction);
 | 
						|
							module.updateHash(hash);
 | 
						|
							hash = hash.digest("hex");
 | 
						|
							module.hotUpdate = records.moduleHashs[identifier] !== hash;
 | 
						|
						}
 | 
						|
						const hotUpdateMainContent = {
 | 
						|
							h: compilation.hash,
 | 
						|
							c: {}
 | 
						|
						};
 | 
						|
						for (let chunkId of Object.keys(records.chunkHashs)) {
 | 
						|
							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.hooks.chunkAsset.call(
 | 
						|
										"HotModuleReplacementPlugin",
 | 
						|
										currentChunk,
 | 
						|
										filename
 | 
						|
									);
 | 
						|
								}
 | 
						|
							} else {
 | 
						|
								hotUpdateMainContent.c[chunkId] = false;
 | 
						|
							}
 | 
						|
						}
 | 
						|
						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(
 | 
						|
						{
 | 
						|
							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
 | 
						|
										);
 | 
						|
									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")
 | 
						|
);
 |