UNPKG

12.7 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 {
281 path: filename,
282 info: assetInfo
283 } = compilation.getPathWithInfo(hotUpdateChunkFilename, {
284 hash: records.hash,
285 chunk: currentChunk
286 });
287 compilation.additionalChunkAssets.push(filename);
288 compilation.emitAsset(
289 filename,
290 source,
291 Object.assign({ hotModuleReplacement: true }, assetInfo)
292 );
293 hotUpdateMainContent.c[chunkId] = true;
294 currentChunk.files.push(filename);
295 compilation.hooks.chunkAsset.call(currentChunk, filename);
296 }
297 } else {
298 hotUpdateMainContent.c[chunkId] = false;
299 }
300 }
301 const source = new RawSource(JSON.stringify(hotUpdateMainContent));
302 const {
303 path: filename,
304 info: assetInfo
305 } = compilation.getPathWithInfo(hotUpdateMainFilename, {
306 hash: records.hash
307 });
308 compilation.emitAsset(
309 filename,
310 source,
311 Object.assign({ hotModuleReplacement: true }, assetInfo)
312 );
313 }
314 );
315
316 const mainTemplate = compilation.mainTemplate;
317
318 mainTemplate.hooks.hash.tap("HotModuleReplacementPlugin", hash => {
319 hash.update("HotMainTemplateDecorator");
320 });
321
322 mainTemplate.hooks.moduleRequire.tap(
323 "HotModuleReplacementPlugin",
324 (_, chunk, hash, varModuleId) => {
325 return `hotCreateRequire(${varModuleId})`;
326 }
327 );
328
329 mainTemplate.hooks.requireExtensions.tap(
330 "HotModuleReplacementPlugin",
331 source => {
332 const buf = [source];
333 buf.push("");
334 buf.push("// __webpack_hash__");
335 buf.push(
336 mainTemplate.requireFn +
337 ".h = function() { return hotCurrentHash; };"
338 );
339 return Template.asString(buf);
340 }
341 );
342
343 const needChunkLoadingCode = chunk => {
344 for (const chunkGroup of chunk.groupsIterable) {
345 if (chunkGroup.chunks.length > 1) return true;
346 if (chunkGroup.getNumberOfChildren() > 0) return true;
347 }
348 return false;
349 };
350
351 mainTemplate.hooks.bootstrap.tap(
352 "HotModuleReplacementPlugin",
353 (source, chunk, hash) => {
354 source = mainTemplate.hooks.hotBootstrap.call(source, chunk, hash);
355 return Template.asString([
356 source,
357 "",
358 hotInitCode
359 .replace(/\$require\$/g, mainTemplate.requireFn)
360 .replace(/\$hash\$/g, JSON.stringify(hash))
361 .replace(/\$requestTimeout\$/g, requestTimeout)
362 .replace(
363 /\/\*foreachInstalledChunks\*\//g,
364 needChunkLoadingCode(chunk)
365 ? "for(var chunkId in installedChunks)"
366 : `var chunkId = ${JSON.stringify(chunk.id)};`
367 )
368 ]);
369 }
370 );
371
372 mainTemplate.hooks.globalHash.tap(
373 "HotModuleReplacementPlugin",
374 () => true
375 );
376
377 mainTemplate.hooks.currentHash.tap(
378 "HotModuleReplacementPlugin",
379 (_, length) => {
380 if (isFinite(length)) {
381 return `hotCurrentHash.substr(0, ${length})`;
382 } else {
383 return "hotCurrentHash";
384 }
385 }
386 );
387
388 mainTemplate.hooks.moduleObj.tap(
389 "HotModuleReplacementPlugin",
390 (source, chunk, hash, varModuleId) => {
391 return Template.asString([
392 `${source},`,
393 `hot: hotCreateModule(${varModuleId}),`,
394 "parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),",
395 "children: []"
396 ]);
397 }
398 );
399
400 // TODO add HMR support for javascript/esm
401 normalModuleFactory.hooks.parser
402 .for("javascript/auto")
403 .tap("HotModuleReplacementPlugin", addParserPlugins);
404 normalModuleFactory.hooks.parser
405 .for("javascript/dynamic")
406 .tap("HotModuleReplacementPlugin", addParserPlugins);
407
408 compilation.hooks.normalModuleLoader.tap(
409 "HotModuleReplacementPlugin",
410 context => {
411 context.hot = true;
412 }
413 );
414 }
415 );
416 }
417};
418
419const hotInitCode = Template.getFunctionContent(
420 require("./HotModuleReplacement.runtime")
421);