UNPKG

12.4 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5"use strict";
6
7const { SyncBailHook } = require("tapable");
8const { RawSource } = require("webpack-sources");
9const Template = require("./Template");
10const ModuleHotAcceptDependency = require("./dependencies/ModuleHotAcceptDependency");
11const ModuleHotDeclineDependency = require("./dependencies/ModuleHotDeclineDependency");
12const ConstDependency = require("./dependencies/ConstDependency");
13const NullFactory = require("./NullFactory");
14const ParserHelpers = require("./ParserHelpers");
15
16module.exports = class HotModuleReplacementPlugin {
17 constructor(options) {
18 this.options = options || {};
19 this.multiStep = this.options.multiStep;
20 this.fullBuildTimeout = this.options.fullBuildTimeout || 200;
21 this.requestTimeout = this.options.requestTimeout || 10000;
22 }
23
24 apply(compiler) {
25 const multiStep = this.multiStep;
26 const fullBuildTimeout = this.fullBuildTimeout;
27 const requestTimeout = this.requestTimeout;
28 const hotUpdateChunkFilename =
29 compiler.options.output.hotUpdateChunkFilename;
30 const hotUpdateMainFilename = compiler.options.output.hotUpdateMainFilename;
31 compiler.hooks.additionalPass.tapAsync(
32 "HotModuleReplacementPlugin",
33 callback => {
34 if (multiStep) return setTimeout(callback, fullBuildTimeout);
35 return callback();
36 }
37 );
38
39 const addParserPlugins = (parser, parserOptions) => {
40 parser.hooks.expression
41 .for("__webpack_hash__")
42 .tap(
43 "HotModuleReplacementPlugin",
44 ParserHelpers.toConstantDependencyWithWebpackRequire(
45 parser,
46 "__webpack_require__.h()"
47 )
48 );
49 parser.hooks.evaluateTypeof
50 .for("__webpack_hash__")
51 .tap(
52 "HotModuleReplacementPlugin",
53 ParserHelpers.evaluateToString("string")
54 );
55 parser.hooks.evaluateIdentifier.for("module.hot").tap(
56 {
57 name: "HotModuleReplacementPlugin",
58 before: "NodeStuffPlugin"
59 },
60 expr => {
61 return ParserHelpers.evaluateToIdentifier(
62 "module.hot",
63 !!parser.state.compilation.hotUpdateChunkTemplate
64 )(expr);
65 }
66 );
67 // TODO webpack 5: refactor this, no custom hooks
68 if (!parser.hooks.hotAcceptCallback) {
69 parser.hooks.hotAcceptCallback = new SyncBailHook([
70 "expression",
71 "requests"
72 ]);
73 }
74 if (!parser.hooks.hotAcceptWithoutCallback) {
75 parser.hooks.hotAcceptWithoutCallback = new SyncBailHook([
76 "expression",
77 "requests"
78 ]);
79 }
80 parser.hooks.call
81 .for("module.hot.accept")
82 .tap("HotModuleReplacementPlugin", expr => {
83 if (!parser.state.compilation.hotUpdateChunkTemplate) {
84 return false;
85 }
86 if (expr.arguments.length >= 1) {
87 const arg = parser.evaluateExpression(expr.arguments[0]);
88 let params = [];
89 let requests = [];
90 if (arg.isString()) {
91 params = [arg];
92 } else if (arg.isArray()) {
93 params = arg.items.filter(param => param.isString());
94 }
95 if (params.length > 0) {
96 params.forEach((param, idx) => {
97 const request = param.string;
98 const dep = new ModuleHotAcceptDependency(request, param.range);
99 dep.optional = true;
100 dep.loc = Object.create(expr.loc);
101 dep.loc.index = idx;
102 parser.state.module.addDependency(dep);
103 requests.push(request);
104 });
105 if (expr.arguments.length > 1) {
106 parser.hooks.hotAcceptCallback.call(
107 expr.arguments[1],
108 requests
109 );
110 parser.walkExpression(expr.arguments[1]); // other args are ignored
111 return true;
112 } else {
113 parser.hooks.hotAcceptWithoutCallback.call(expr, requests);
114 return true;
115 }
116 }
117 }
118 });
119 parser.hooks.call
120 .for("module.hot.decline")
121 .tap("HotModuleReplacementPlugin", expr => {
122 if (!parser.state.compilation.hotUpdateChunkTemplate) {
123 return false;
124 }
125 if (expr.arguments.length === 1) {
126 const arg = parser.evaluateExpression(expr.arguments[0]);
127 let params = [];
128 if (arg.isString()) {
129 params = [arg];
130 } else if (arg.isArray()) {
131 params = arg.items.filter(param => param.isString());
132 }
133 params.forEach((param, idx) => {
134 const dep = new ModuleHotDeclineDependency(
135 param.string,
136 param.range
137 );
138 dep.optional = true;
139 dep.loc = Object.create(expr.loc);
140 dep.loc.index = idx;
141 parser.state.module.addDependency(dep);
142 });
143 }
144 });
145 parser.hooks.expression
146 .for("module.hot")
147 .tap("HotModuleReplacementPlugin", ParserHelpers.skipTraversal);
148 };
149
150 compiler.hooks.compilation.tap(
151 "HotModuleReplacementPlugin",
152 (compilation, { normalModuleFactory }) => {
153 const hotUpdateChunkTemplate = compilation.hotUpdateChunkTemplate;
154 if (!hotUpdateChunkTemplate) return;
155
156 compilation.dependencyFactories.set(ConstDependency, new NullFactory());
157 compilation.dependencyTemplates.set(
158 ConstDependency,
159 new ConstDependency.Template()
160 );
161
162 compilation.dependencyFactories.set(
163 ModuleHotAcceptDependency,
164 normalModuleFactory
165 );
166 compilation.dependencyTemplates.set(
167 ModuleHotAcceptDependency,
168 new ModuleHotAcceptDependency.Template()
169 );
170
171 compilation.dependencyFactories.set(
172 ModuleHotDeclineDependency,
173 normalModuleFactory
174 );
175 compilation.dependencyTemplates.set(
176 ModuleHotDeclineDependency,
177 new ModuleHotDeclineDependency.Template()
178 );
179
180 compilation.hooks.record.tap(
181 "HotModuleReplacementPlugin",
182 (compilation, records) => {
183 if (records.hash === compilation.hash) return;
184 records.hash = compilation.hash;
185 records.moduleHashs = {};
186 for (const module of compilation.modules) {
187 const identifier = module.identifier();
188 records.moduleHashs[identifier] = module.hash;
189 }
190 records.chunkHashs = {};
191 for (const chunk of compilation.chunks) {
192 records.chunkHashs[chunk.id] = chunk.hash;
193 }
194 records.chunkModuleIds = {};
195 for (const chunk of compilation.chunks) {
196 records.chunkModuleIds[chunk.id] = Array.from(
197 chunk.modulesIterable,
198 m => m.id
199 );
200 }
201 }
202 );
203 let initialPass = false;
204 let recompilation = false;
205 compilation.hooks.afterHash.tap("HotModuleReplacementPlugin", () => {
206 let records = compilation.records;
207 if (!records) {
208 initialPass = true;
209 return;
210 }
211 if (!records.hash) initialPass = true;
212 const preHash = records.preHash || "x";
213 const prepreHash = records.prepreHash || "x";
214 if (preHash === compilation.hash) {
215 recompilation = true;
216 compilation.modifyHash(prepreHash);
217 return;
218 }
219 records.prepreHash = records.hash || "x";
220 records.preHash = compilation.hash;
221 compilation.modifyHash(records.prepreHash);
222 });
223 compilation.hooks.shouldGenerateChunkAssets.tap(
224 "HotModuleReplacementPlugin",
225 () => {
226 if (multiStep && !recompilation && !initialPass) return false;
227 }
228 );
229 compilation.hooks.needAdditionalPass.tap(
230 "HotModuleReplacementPlugin",
231 () => {
232 if (multiStep && !recompilation && !initialPass) return true;
233 }
234 );
235 compilation.hooks.additionalChunkAssets.tap(
236 "HotModuleReplacementPlugin",
237 () => {
238 const records = compilation.records;
239 if (records.hash === compilation.hash) return;
240 if (
241 !records.moduleHashs ||
242 !records.chunkHashs ||
243 !records.chunkModuleIds
244 )
245 return;
246 for (const module of compilation.modules) {
247 const identifier = module.identifier();
248 let hash = module.hash;
249 module.hotUpdate = records.moduleHashs[identifier] !== hash;
250 }
251 const hotUpdateMainContent = {
252 h: compilation.hash,
253 c: {}
254 };
255 for (const key of Object.keys(records.chunkHashs)) {
256 const chunkId = isNaN(+key) ? key : +key;
257 const currentChunk = compilation.chunks.find(
258 chunk => `${chunk.id}` === key
259 );
260 if (currentChunk) {
261 const newModules = currentChunk
262 .getModules()
263 .filter(module => module.hotUpdate);
264 const allModules = new Set();
265 for (const module of currentChunk.modulesIterable) {
266 allModules.add(module.id);
267 }
268 const removedModules = records.chunkModuleIds[chunkId].filter(
269 id => !allModules.has(id)
270 );
271 if (newModules.length > 0 || removedModules.length > 0) {
272 const source = hotUpdateChunkTemplate.render(
273 chunkId,
274 newModules,
275 removedModules,
276 compilation.hash,
277 compilation.moduleTemplates.javascript,
278 compilation.dependencyTemplates
279 );
280 const filename = compilation.getPath(hotUpdateChunkFilename, {
281 hash: records.hash,
282 chunk: currentChunk
283 });
284 compilation.additionalChunkAssets.push(filename);
285 compilation.assets[filename] = source;
286 hotUpdateMainContent.c[chunkId] = true;
287 currentChunk.files.push(filename);
288 compilation.hooks.chunkAsset.call(currentChunk, filename);
289 }
290 } else {
291 hotUpdateMainContent.c[chunkId] = false;
292 }
293 }
294 const source = new RawSource(JSON.stringify(hotUpdateMainContent));
295 const filename = compilation.getPath(hotUpdateMainFilename, {
296 hash: records.hash
297 });
298 compilation.assets[filename] = source;
299 }
300 );
301
302 const mainTemplate = compilation.mainTemplate;
303
304 mainTemplate.hooks.hash.tap("HotModuleReplacementPlugin", hash => {
305 hash.update("HotMainTemplateDecorator");
306 });
307
308 mainTemplate.hooks.moduleRequire.tap(
309 "HotModuleReplacementPlugin",
310 (_, chunk, hash, varModuleId) => {
311 return `hotCreateRequire(${varModuleId})`;
312 }
313 );
314
315 mainTemplate.hooks.requireExtensions.tap(
316 "HotModuleReplacementPlugin",
317 source => {
318 const buf = [source];
319 buf.push("");
320 buf.push("// __webpack_hash__");
321 buf.push(
322 mainTemplate.requireFn +
323 ".h = function() { return hotCurrentHash; };"
324 );
325 return Template.asString(buf);
326 }
327 );
328
329 const needChunkLoadingCode = chunk => {
330 for (const chunkGroup of chunk.groupsIterable) {
331 if (chunkGroup.chunks.length > 1) return true;
332 if (chunkGroup.getNumberOfChildren() > 0) return true;
333 }
334 return false;
335 };
336
337 mainTemplate.hooks.bootstrap.tap(
338 "HotModuleReplacementPlugin",
339 (source, chunk, hash) => {
340 source = mainTemplate.hooks.hotBootstrap.call(source, chunk, hash);
341 return Template.asString([
342 source,
343 "",
344 hotInitCode
345 .replace(/\$require\$/g, mainTemplate.requireFn)
346 .replace(/\$hash\$/g, JSON.stringify(hash))
347 .replace(/\$requestTimeout\$/g, requestTimeout)
348 .replace(
349 /\/\*foreachInstalledChunks\*\//g,
350 needChunkLoadingCode(chunk)
351 ? "for(var chunkId in installedChunks)"
352 : `var chunkId = ${JSON.stringify(chunk.id)};`
353 )
354 ]);
355 }
356 );
357
358 mainTemplate.hooks.globalHash.tap(
359 "HotModuleReplacementPlugin",
360 () => true
361 );
362
363 mainTemplate.hooks.currentHash.tap(
364 "HotModuleReplacementPlugin",
365 (_, length) => {
366 if (isFinite(length)) {
367 return `hotCurrentHash.substr(0, ${length})`;
368 } else {
369 return "hotCurrentHash";
370 }
371 }
372 );
373
374 mainTemplate.hooks.moduleObj.tap(
375 "HotModuleReplacementPlugin",
376 (source, chunk, hash, varModuleId) => {
377 return Template.asString([
378 `${source},`,
379 `hot: hotCreateModule(${varModuleId}),`,
380 "parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),",
381 "children: []"
382 ]);
383 }
384 );
385
386 // TODO add HMR support for javascript/esm
387 normalModuleFactory.hooks.parser
388 .for("javascript/auto")
389 .tap("HotModuleReplacementPlugin", addParserPlugins);
390 normalModuleFactory.hooks.parser
391 .for("javascript/dynamic")
392 .tap("HotModuleReplacementPlugin", addParserPlugins);
393
394 compilation.hooks.normalModuleLoader.tap(
395 "HotModuleReplacementPlugin",
396 context => {
397 context.hot = true;
398 }
399 );
400 }
401 );
402 }
403};
404
405const hotInitCode = Template.getFunctionContent(
406 require("./HotModuleReplacement.runtime")
407);