UNPKG

18.7 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const { SyncBailHook } = require("tapable");
9const { RawSource } = require("webpack-sources");
10const ChunkGraph = require("./ChunkGraph");
11const Compilation = require("./Compilation");
12const HotUpdateChunk = require("./HotUpdateChunk");
13const NormalModule = require("./NormalModule");
14const RuntimeGlobals = require("./RuntimeGlobals");
15const ConstDependency = require("./dependencies/ConstDependency");
16const ImportMetaHotAcceptDependency = require("./dependencies/ImportMetaHotAcceptDependency");
17const ImportMetaHotDeclineDependency = require("./dependencies/ImportMetaHotDeclineDependency");
18const ModuleHotAcceptDependency = require("./dependencies/ModuleHotAcceptDependency");
19const ModuleHotDeclineDependency = require("./dependencies/ModuleHotDeclineDependency");
20const HotModuleReplacementRuntimeModule = require("./hmr/HotModuleReplacementRuntimeModule");
21const JavascriptParser = require("./javascript/JavascriptParser");
22const {
23 evaluateToIdentifier
24} = require("./javascript/JavascriptParserHelpers");
25const { find } = require("./util/SetHelpers");
26const TupleSet = require("./util/TupleSet");
27const { compareModulesById } = require("./util/comparators");
28const { getRuntimeKey, keyToRuntime } = require("./util/runtime");
29
30/** @typedef {import("./Chunk")} Chunk */
31/** @typedef {import("./Compilation").AssetInfo} AssetInfo */
32/** @typedef {import("./Compiler")} Compiler */
33/** @typedef {import("./Module")} Module */
34
35/**
36 * @typedef {Object} HMRJavascriptParserHooks
37 * @property {SyncBailHook<[TODO, string[]], void>} hotAcceptCallback
38 * @property {SyncBailHook<[TODO, string[]], void>} hotAcceptWithoutCallback
39 */
40
41/** @type {WeakMap<JavascriptParser, HMRJavascriptParserHooks>} */
42const parserHooksMap = new WeakMap();
43
44class HotModuleReplacementPlugin {
45 /**
46 * @param {JavascriptParser} parser the parser
47 * @returns {HMRJavascriptParserHooks} the attached hooks
48 */
49 static getParserHooks(parser) {
50 if (!(parser instanceof JavascriptParser)) {
51 throw new TypeError(
52 "The 'parser' argument must be an instance of JavascriptParser"
53 );
54 }
55 let hooks = parserHooksMap.get(parser);
56 if (hooks === undefined) {
57 hooks = {
58 hotAcceptCallback: new SyncBailHook(["expression", "requests"]),
59 hotAcceptWithoutCallback: new SyncBailHook(["expression", "requests"])
60 };
61 parserHooksMap.set(parser, hooks);
62 }
63 return hooks;
64 }
65
66 constructor(options) {
67 this.options = options || {};
68 }
69
70 /**
71 * Apply the plugin
72 * @param {Compiler} compiler the compiler instance
73 * @returns {void}
74 */
75 apply(compiler) {
76 const runtimeRequirements = [RuntimeGlobals.module];
77
78 const createAcceptHandler = (parser, ParamDependency) => {
79 const {
80 hotAcceptCallback,
81 hotAcceptWithoutCallback
82 } = HotModuleReplacementPlugin.getParserHooks(parser);
83
84 return expr => {
85 const module = parser.state.module;
86 const dep = new ConstDependency(
87 `${module.moduleArgument}.hot.accept`,
88 expr.callee.range,
89 runtimeRequirements
90 );
91 dep.loc = expr.loc;
92 module.addPresentationalDependency(dep);
93 module.buildInfo.moduleConcatenationBailout = "Hot Module Replacement";
94 if (expr.arguments.length >= 1) {
95 const arg = parser.evaluateExpression(expr.arguments[0]);
96 let params = [];
97 let requests = [];
98 if (arg.isString()) {
99 params = [arg];
100 } else if (arg.isArray()) {
101 params = arg.items.filter(param => param.isString());
102 }
103 if (params.length > 0) {
104 params.forEach((param, idx) => {
105 const request = param.string;
106 const dep = new ParamDependency(request, param.range);
107 dep.optional = true;
108 dep.loc = Object.create(expr.loc);
109 dep.loc.index = idx;
110 module.addDependency(dep);
111 requests.push(request);
112 });
113 if (expr.arguments.length > 1) {
114 hotAcceptCallback.call(expr.arguments[1], requests);
115 parser.walkExpression(expr.arguments[1]); // other args are ignored
116 return true;
117 } else {
118 hotAcceptWithoutCallback.call(expr, requests);
119 return true;
120 }
121 }
122 }
123 parser.walkExpressions(expr.arguments);
124 return true;
125 };
126 };
127
128 const createDeclineHandler = (parser, ParamDependency) => expr => {
129 const module = parser.state.module;
130 const dep = new ConstDependency(
131 `${module.moduleArgument}.hot.decline`,
132 expr.callee.range,
133 runtimeRequirements
134 );
135 dep.loc = expr.loc;
136 module.addPresentationalDependency(dep);
137 module.buildInfo.moduleConcatenationBailout = "Hot Module Replacement";
138 if (expr.arguments.length === 1) {
139 const arg = parser.evaluateExpression(expr.arguments[0]);
140 let params = [];
141 if (arg.isString()) {
142 params = [arg];
143 } else if (arg.isArray()) {
144 params = arg.items.filter(param => param.isString());
145 }
146 params.forEach((param, idx) => {
147 const dep = new ParamDependency(param.string, param.range);
148 dep.optional = true;
149 dep.loc = Object.create(expr.loc);
150 dep.loc.index = idx;
151 module.addDependency(dep);
152 });
153 }
154 return true;
155 };
156
157 const createHMRExpressionHandler = parser => expr => {
158 const module = parser.state.module;
159 const dep = new ConstDependency(
160 `${module.moduleArgument}.hot`,
161 expr.range,
162 runtimeRequirements
163 );
164 dep.loc = expr.loc;
165 module.addPresentationalDependency(dep);
166 module.buildInfo.moduleConcatenationBailout = "Hot Module Replacement";
167 return true;
168 };
169
170 const applyModuleHot = parser => {
171 parser.hooks.evaluateIdentifier.for("module.hot").tap(
172 {
173 name: "HotModuleReplacementPlugin",
174 before: "NodeStuffPlugin"
175 },
176 expr => {
177 return evaluateToIdentifier(
178 "module.hot",
179 "module",
180 () => ["hot"],
181 true
182 )(expr);
183 }
184 );
185 parser.hooks.call
186 .for("module.hot.accept")
187 .tap(
188 "HotModuleReplacementPlugin",
189 createAcceptHandler(parser, ModuleHotAcceptDependency)
190 );
191 parser.hooks.call
192 .for("module.hot.decline")
193 .tap(
194 "HotModuleReplacementPlugin",
195 createDeclineHandler(parser, ModuleHotDeclineDependency)
196 );
197 parser.hooks.expression
198 .for("module.hot")
199 .tap("HotModuleReplacementPlugin", createHMRExpressionHandler(parser));
200 };
201
202 const applyImportMetaHot = parser => {
203 parser.hooks.evaluateIdentifier
204 .for("import.meta.webpackHot")
205 .tap("HotModuleReplacementPlugin", expr => {
206 return evaluateToIdentifier(
207 "import.meta.webpackHot",
208 "import.meta",
209 () => ["webpackHot"],
210 true
211 )(expr);
212 });
213 parser.hooks.call
214 .for("import.meta.webpackHot.accept")
215 .tap(
216 "HotModuleReplacementPlugin",
217 createAcceptHandler(parser, ImportMetaHotAcceptDependency)
218 );
219 parser.hooks.call
220 .for("import.meta.webpackHot.decline")
221 .tap(
222 "HotModuleReplacementPlugin",
223 createDeclineHandler(parser, ImportMetaHotDeclineDependency)
224 );
225 parser.hooks.expression
226 .for("import.meta.webpackHot")
227 .tap("HotModuleReplacementPlugin", createHMRExpressionHandler(parser));
228 };
229
230 compiler.hooks.compilation.tap(
231 "HotModuleReplacementPlugin",
232 (compilation, { normalModuleFactory }) => {
233 // This applies the HMR plugin only to the targeted compiler
234 // It should not affect child compilations
235 if (compilation.compiler !== compiler) return;
236
237 //#region module.hot.* API
238 compilation.dependencyFactories.set(
239 ModuleHotAcceptDependency,
240 normalModuleFactory
241 );
242 compilation.dependencyTemplates.set(
243 ModuleHotAcceptDependency,
244 new ModuleHotAcceptDependency.Template()
245 );
246 compilation.dependencyFactories.set(
247 ModuleHotDeclineDependency,
248 normalModuleFactory
249 );
250 compilation.dependencyTemplates.set(
251 ModuleHotDeclineDependency,
252 new ModuleHotDeclineDependency.Template()
253 );
254 //#endregion
255
256 //#region import.meta.webpackHot.* API
257 compilation.dependencyFactories.set(
258 ImportMetaHotAcceptDependency,
259 normalModuleFactory
260 );
261 compilation.dependencyTemplates.set(
262 ImportMetaHotAcceptDependency,
263 new ImportMetaHotAcceptDependency.Template()
264 );
265 compilation.dependencyFactories.set(
266 ImportMetaHotDeclineDependency,
267 normalModuleFactory
268 );
269 compilation.dependencyTemplates.set(
270 ImportMetaHotDeclineDependency,
271 new ImportMetaHotDeclineDependency.Template()
272 );
273 //#endregion
274
275 let hotIndex = 0;
276 const fullHashChunkModuleHashes = {};
277 const chunkModuleHashes = {};
278
279 compilation.hooks.record.tap(
280 "HotModuleReplacementPlugin",
281 (compilation, records) => {
282 if (records.hash === compilation.hash) return;
283 const chunkGraph = compilation.chunkGraph;
284 records.hash = compilation.hash;
285 records.hotIndex = hotIndex;
286 records.fullHashChunkModuleHashes = fullHashChunkModuleHashes;
287 records.chunkModuleHashes = chunkModuleHashes;
288 records.chunkHashs = {};
289 records.chunkRuntime = {};
290 for (const chunk of compilation.chunks) {
291 records.chunkHashs[chunk.id] = chunk.hash;
292 records.chunkRuntime[chunk.id] = getRuntimeKey(chunk.runtime);
293 }
294 records.chunkModuleIds = {};
295 for (const chunk of compilation.chunks) {
296 records.chunkModuleIds[
297 chunk.id
298 ] = Array.from(
299 chunkGraph.getOrderedChunkModulesIterable(
300 chunk,
301 compareModulesById(chunkGraph)
302 ),
303 m => chunkGraph.getModuleId(m)
304 );
305 }
306 }
307 );
308 /** @type {TupleSet<[Module, Chunk]>} */
309 const updatedModules = new TupleSet();
310 /** @type {TupleSet<[Module, Chunk]>} */
311 const lazyHashedModules = new TupleSet();
312 compilation.hooks.fullHash.tap("HotModuleReplacementPlugin", hash => {
313 const chunkGraph = compilation.chunkGraph;
314 const records = compilation.records;
315 for (const chunk of compilation.chunks) {
316 /** @type {Set<Module>} */
317 const lazyHashedModulesInThisChunk = new Set();
318 const fullHashModules = chunkGraph.getChunkFullHashModulesIterable(
319 chunk
320 );
321 if (fullHashModules !== undefined) {
322 for (const module of fullHashModules) {
323 lazyHashedModules.add(module, chunk);
324 lazyHashedModulesInThisChunk.add(module);
325 }
326 }
327 const modules = chunkGraph.getChunkModulesIterable(chunk);
328 if (modules !== undefined) {
329 if (
330 records.chunkModuleHashes &&
331 records.fullHashChunkModuleHashes
332 ) {
333 for (const module of modules) {
334 const key = `${chunk.id}|${module.identifier()}`;
335 const hash = chunkGraph.getModuleHash(module, chunk.runtime);
336 if (lazyHashedModulesInThisChunk.has(module)) {
337 if (records.fullHashChunkModuleHashes[key] !== hash) {
338 updatedModules.add(module, chunk);
339 }
340 fullHashChunkModuleHashes[key] = hash;
341 } else {
342 if (records.chunkModuleHashes[key] !== hash) {
343 updatedModules.add(module, chunk);
344 }
345 chunkModuleHashes[key] = hash;
346 }
347 }
348 } else {
349 for (const module of modules) {
350 const key = `${chunk.id}|${module.identifier()}`;
351 const hash = chunkGraph.getModuleHash(module, chunk.runtime);
352 if (lazyHashedModulesInThisChunk.has(module)) {
353 fullHashChunkModuleHashes[key] = hash;
354 } else {
355 chunkModuleHashes[key] = hash;
356 }
357 }
358 }
359 }
360 }
361
362 hotIndex = records.hotIndex || 0;
363 if (updatedModules.size > 0) hotIndex++;
364
365 hash.update(`${hotIndex}`);
366 });
367 compilation.hooks.processAssets.tap(
368 {
369 name: "HotModuleReplacementPlugin",
370 stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL
371 },
372 () => {
373 const chunkGraph = compilation.chunkGraph;
374 const records = compilation.records;
375 if (records.hash === compilation.hash) return;
376 if (
377 !records.chunkModuleHashes ||
378 !records.chunkHashs ||
379 !records.chunkModuleIds
380 ) {
381 return;
382 }
383 for (const [module, chunk] of lazyHashedModules) {
384 const key = `${chunk.id}|${module.identifier()}`;
385 const hash = chunkGraph.getModuleHash(module, chunk.runtime);
386 if (records.chunkModuleHashes[key] !== hash) {
387 updatedModules.add(module, chunk);
388 }
389 chunkModuleHashes[key] = hash;
390 }
391 const hotUpdateMainContent = {
392 c: [],
393 r: [],
394 m: undefined
395 };
396
397 // Create a list of all active modules to verify which modules are removed completely
398 /** @type {Map<number|string, Module>} */
399 const allModules = new Map();
400 for (const module of compilation.modules) {
401 allModules.set(chunkGraph.getModuleId(module), module);
402 }
403
404 // List of completely removed modules
405 const allRemovedModules = new Set();
406
407 for (const key of Object.keys(records.chunkHashs)) {
408 // Check which modules are completely removed
409 for (const id of records.chunkModuleIds[key]) {
410 if (!allModules.has(id)) {
411 allRemovedModules.add(id);
412 }
413 }
414
415 let chunkId;
416 let newModules;
417 let newRuntimeModules;
418 let newFullHashModules;
419 let newRuntime;
420 const currentChunk = find(
421 compilation.chunks,
422 chunk => `${chunk.id}` === key
423 );
424 if (currentChunk) {
425 chunkId = currentChunk.id;
426 newRuntime = currentChunk.runtime;
427 newModules = chunkGraph
428 .getChunkModules(currentChunk)
429 .filter(module => updatedModules.has(module, currentChunk));
430 newRuntimeModules = Array.from(
431 chunkGraph.getChunkRuntimeModulesIterable(currentChunk)
432 ).filter(module => updatedModules.has(module, currentChunk));
433 const fullHashModules = chunkGraph.getChunkFullHashModulesIterable(
434 currentChunk
435 );
436 newFullHashModules =
437 fullHashModules &&
438 Array.from(fullHashModules).filter(module =>
439 updatedModules.has(module, currentChunk)
440 );
441 } else {
442 chunkId = `${+key}` === key ? +key : key;
443 hotUpdateMainContent.r.push(chunkId);
444 const runtime = keyToRuntime(records.chunkRuntime[key]);
445 for (const id of records.chunkModuleIds[key]) {
446 const module = allModules.get(id);
447 if (!module) continue;
448 const hash = chunkGraph.getModuleHash(module, runtime);
449 const moduleKey = `${key}|${module.identifier()}`;
450 if (hash !== records.chunkModuleHashes[moduleKey]) {
451 newModules = newModules || [];
452 newModules.push(module);
453 }
454 }
455 }
456 if (
457 (newModules && newModules.length > 0) ||
458 (newRuntimeModules && newRuntimeModules.length > 0)
459 ) {
460 const hotUpdateChunk = new HotUpdateChunk();
461 ChunkGraph.setChunkGraphForChunk(hotUpdateChunk, chunkGraph);
462 hotUpdateChunk.id = chunkId;
463 hotUpdateChunk.runtime = newRuntime;
464 if (currentChunk) {
465 for (const group of currentChunk.groupsIterable)
466 hotUpdateChunk.addGroup(group);
467 }
468 chunkGraph.attachModules(hotUpdateChunk, newModules || []);
469 chunkGraph.attachRuntimeModules(
470 hotUpdateChunk,
471 newRuntimeModules || []
472 );
473 if (newFullHashModules) {
474 chunkGraph.attachFullHashModules(
475 hotUpdateChunk,
476 newFullHashModules
477 );
478 }
479 const renderManifest = compilation.getRenderManifest({
480 chunk: hotUpdateChunk,
481 hash: records.hash,
482 fullHash: records.hash,
483 outputOptions: compilation.outputOptions,
484 moduleTemplates: compilation.moduleTemplates,
485 dependencyTemplates: compilation.dependencyTemplates,
486 codeGenerationResults: compilation.codeGenerationResults,
487 runtimeTemplate: compilation.runtimeTemplate,
488 moduleGraph: compilation.moduleGraph,
489 chunkGraph
490 });
491 for (const entry of renderManifest) {
492 /** @type {string} */
493 let filename;
494 /** @type {AssetInfo} */
495 let assetInfo;
496 if ("filename" in entry) {
497 filename = entry.filename;
498 assetInfo = entry.info;
499 } else {
500 ({
501 path: filename,
502 info: assetInfo
503 } = compilation.getPathWithInfo(
504 entry.filenameTemplate,
505 entry.pathOptions
506 ));
507 }
508 const source = entry.render();
509 compilation.additionalChunkAssets.push(filename);
510 compilation.emitAsset(filename, source, {
511 hotModuleReplacement: true,
512 ...assetInfo
513 });
514 if (currentChunk) {
515 currentChunk.files.add(filename);
516 compilation.hooks.chunkAsset.call(currentChunk, filename);
517 }
518 }
519 hotUpdateMainContent.c.push(chunkId);
520 }
521 }
522 hotUpdateMainContent.m = Array.from(allRemovedModules);
523 const source = new RawSource(JSON.stringify(hotUpdateMainContent));
524 const {
525 path: filename,
526 info: assetInfo
527 } = compilation.getPathWithInfo(
528 compilation.outputOptions.hotUpdateMainFilename,
529 {
530 hash: records.hash
531 }
532 );
533 compilation.emitAsset(filename, source, {
534 hotModuleReplacement: true,
535 ...assetInfo
536 });
537 }
538 );
539
540 compilation.hooks.additionalTreeRuntimeRequirements.tap(
541 "HotModuleReplacementPlugin",
542 (chunk, runtimeRequirements) => {
543 runtimeRequirements.add(RuntimeGlobals.hmrDownloadManifest);
544 runtimeRequirements.add(RuntimeGlobals.hmrDownloadUpdateHandlers);
545 runtimeRequirements.add(RuntimeGlobals.interceptModuleExecution);
546 runtimeRequirements.add(RuntimeGlobals.moduleCache);
547 compilation.addRuntimeModule(
548 chunk,
549 new HotModuleReplacementRuntimeModule()
550 );
551 }
552 );
553
554 normalModuleFactory.hooks.parser
555 .for("javascript/auto")
556 .tap("HotModuleReplacementPlugin", parser => {
557 applyModuleHot(parser);
558 applyImportMetaHot(parser);
559 });
560 normalModuleFactory.hooks.parser
561 .for("javascript/dynamic")
562 .tap("HotModuleReplacementPlugin", parser => {
563 applyModuleHot(parser);
564 });
565 normalModuleFactory.hooks.parser
566 .for("javascript/esm")
567 .tap("HotModuleReplacementPlugin", parser => {
568 applyImportMetaHot(parser);
569 });
570
571 NormalModule.getCompilationHooks(compilation).loader.tap(
572 "HotModuleReplacementPlugin",
573 context => {
574 context.hot = true;
575 }
576 );
577 }
578 );
579 }
580}
581
582module.exports = HotModuleReplacementPlugin;