webpack 5.107.1 → 5.107.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,12 +36,12 @@ const { makePathsAbsolute } = require("./util/identifier");
36
36
  /**
37
37
  * Defines the source map task type used by this module.
38
38
  * @typedef {object} SourceMapTask
39
- * @property {Source} asset
40
39
  * @property {AssetInfo} assetInfo
41
40
  * @property {(string | Module)[]} modules
42
41
  * @property {string} source
43
42
  * @property {string} file
44
43
  * @property {RawSourceMap} sourceMap
44
+ * @property {Source} mapSource the Source instance whose `sourceAndMap` we called (the current asset or, when its map was already stripped, the pinned original from `originalSources`) — what `clearCache` should target
45
45
  * @property {ItemCacheFacade} cacheItem cache item
46
46
  */
47
47
 
@@ -111,12 +111,14 @@ const getOriginalSourceRegistry = (compilation) => {
111
111
  * The returned source is read from the asset as it currently stands — that way
112
112
  * any `sourceMappingURL` comments appended by earlier plugin instances survive
113
113
  * — while the map is taken from the pinned original Source when the current
114
- * one no longer carries it.
114
+ * one no longer carries it. `mapSource` identifies which Source instance was
115
+ * actually queried for the map (the current asset, or the pinned original);
116
+ * that's the one whose internal caches the caller should release.
115
117
  * @param {string} file file name
116
118
  * @param {Source} asset source object as currently held by the compilation
117
119
  * @param {MapOptions} options map extraction options
118
120
  * @param {Map<string, Source>} registry compilation-scoped original-source registry
119
- * @returns {{ source: string, sourceMap: RawSourceMap } | undefined} extracted pair or `undefined` when no map is recoverable
121
+ * @returns {{ source: string, sourceMap: RawSourceMap, mapSource: Source } | undefined} extracted pair or `undefined` when no map is recoverable
120
122
  */
121
123
  const extractSourceAndMap = (file, asset, options, registry) => {
122
124
  /** @type {string | Buffer} */
@@ -139,20 +141,20 @@ const extractSourceAndMap = (file, asset, options, registry) => {
139
141
  // that a later plugin instance (which will see a rewrapped asset
140
142
  // without a map) can recover it on demand.
141
143
  if (!registry.has(file)) registry.set(file, asset);
142
- } else {
143
- // The current asset (typically a `RawSource` left by an earlier
144
- // SourceMapDevToolPlugin instance) has no internal map. Re-extract
145
- // the map from the original Source we pinned earlier. We keep using
146
- // `source` from the current asset so that any prior wrappers (e.g.
147
- // appended sourceMappingURL comments) are preserved.
148
- const original = registry.get(file);
149
- if (!original) return;
150
- sourceMap = original.sourceAndMap
151
- ? original.sourceAndMap(options).map
152
- : original.map(options);
153
- if (!sourceMap) return;
144
+ return { source, sourceMap, mapSource: asset };
154
145
  }
155
- return { source, sourceMap };
146
+ // The current asset (typically a `RawSource` left by an earlier
147
+ // SourceMapDevToolPlugin instance) has no internal map. Re-extract
148
+ // the map from the original Source we pinned earlier. We keep using
149
+ // `source` from the current asset so that any prior wrappers (e.g.
150
+ // appended sourceMappingURL comments) are preserved.
151
+ const original = registry.get(file);
152
+ if (!original) return;
153
+ sourceMap = original.sourceAndMap
154
+ ? original.sourceAndMap(options).map
155
+ : original.map(options);
156
+ if (!sourceMap) return;
157
+ return { source, sourceMap, mapSource: original };
156
158
  };
157
159
 
158
160
  /**
@@ -177,7 +179,7 @@ const getTaskForFile = (
177
179
  ) => {
178
180
  const extracted = extractSourceAndMap(file, asset, options, registry);
179
181
  if (!extracted) return;
180
- const { source, sourceMap } = extracted;
182
+ const { source, sourceMap, mapSource } = extracted;
181
183
  const context = compilation.options.context;
182
184
  const root = compilation.compiler.root;
183
185
  const cachedAbsolutify = makePathsAbsolute.bindContextCache(context, root);
@@ -190,10 +192,10 @@ const getTaskForFile = (
190
192
 
191
193
  return {
192
194
  file,
193
- asset,
194
195
  source: /** @type {string} */ (source),
195
196
  assetInfo,
196
197
  sourceMap,
198
+ mapSource,
197
199
  modules,
198
200
  cacheItem
199
201
  };
@@ -360,6 +362,25 @@ class SourceMapDevToolPlugin {
360
362
  const tasks = [];
361
363
  let fileIndex = 0;
362
364
 
365
+ // Shared deduplication set for `Source#clearCache` calls below.
366
+ // Webpack chunks routinely share module-level `CachedSource`
367
+ // instances. A per-call WeakSet would re-walk those shared
368
+ // subtrees once per chunk — 50 chunks × thousands of shared
369
+ // modules in dev/non-minified setups — and worse, every
370
+ // chunk's `sourceAndMap` would have to recompute the cleared
371
+ // caches, churning allocations (measured: +700 MB peak RSS,
372
+ // +6 s wall time on a 50×1000 synthetic build).
373
+ //
374
+ // Sharing one set lets each shared subtree be walked exactly
375
+ // once. The trade-off is that subsequent chunks' `sourceAndMap`
376
+ // calls can repopulate a shared module's `_cachedMaps` after
377
+ // its own clear was skipped (because the module is already in
378
+ // the visited set), leaving at most one populated cache entry
379
+ // per shared module at the end of the run — bounded to a few
380
+ // MB even at the scale of #20961. That's strictly preferable
381
+ // to the alternative's hundreds of MB of transient peak RSS.
382
+ const clearCacheVisited = new WeakSet();
383
+
363
384
  asyncLib.each(
364
385
  files,
365
386
  (file, callback) => {
@@ -458,6 +479,44 @@ class SourceMapDevToolPlugin {
458
479
  originalSources
459
480
  );
460
481
 
482
+ // Release the per-instance caches that `sourceAndMap`
483
+ // just populated. The composed map (and, for
484
+ // `SourceMapSource`, the parsed `_sourceMapAsObject` /
485
+ // `_innerSourceMapAsObject`) otherwise sit on the
486
+ // CachedSource — and every shared child — until phase
487
+ // 2 replaces the asset, which is what causes the OOM
488
+ // spike on builds with thousands of chunks
489
+ // (webpack#20961). Keep `source` since downstream
490
+ // consumers reading the original asset still need it;
491
+ // `hash`/`size` default to retained because they're
492
+ // cheap to keep but expensive to rebuild.
493
+ // `clearCacheVisited` is shared across every call (see
494
+ // its declaration above for the rationale).
495
+ //
496
+ // Target `task.mapSource` (not `asset.source`): when
497
+ // `extractSourceAndMap` falls back to the pinned
498
+ // original (the current asset is a `RawSource` left
499
+ // by an earlier plugin instance), the `sourceAndMap`
500
+ // call populated the original's caches, not the
501
+ // current asset's.
502
+ //
503
+ // Feature-detected: `clearCache` landed in
504
+ // `webpack-sources` 3.5, but `compilation.assets` may
505
+ // hold `Source`-like instances from a third-party
506
+ // plugin built against an older copy of
507
+ // `webpack-sources` (or a hand-rolled implementation).
508
+ // Calling it unconditionally would throw on those.
509
+ if (task && typeof task.mapSource.clearCache === "function") {
510
+ task.mapSource.clearCache(
511
+ {
512
+ maps: true,
513
+ source: false,
514
+ parsedMap: true
515
+ },
516
+ clearCacheVisited
517
+ );
518
+ }
519
+
461
520
  if (task) {
462
521
  const modules = task.modules;
463
522
 
@@ -698,12 +757,23 @@ class SourceMapDevToolPlugin {
698
757
  outputSourceMap.debugId = debugIdValue;
699
758
  }
700
759
 
701
- const sourceMapString = JSON.stringify(outputSourceMap);
702
760
  if (sourceMapFilename) {
761
+ // External `.map` file: hold the serialized map as a
762
+ // `Buffer` instead of a V8 string. `RawSource` accepts
763
+ // a buffer directly, and the emitted asset stays in
764
+ // `compilation.assets` until the build finishes — so
765
+ // storing the bytes off the V8 heap (where Buffers
766
+ // live, accounted as `external` memory) avoids keeping
767
+ // a large V8 string alive for the rest of the build
768
+ // and reduces heap pressure on `--max-old-space-size`.
769
+ const sourceMapBuffer = Buffer.from(
770
+ JSON.stringify(outputSourceMap),
771
+ "utf8"
772
+ );
703
773
  const filename = file;
704
774
  const sourceMapContentHash = usesContentHash
705
775
  ? createHash(compilation.outputOptions.hashFunction)
706
- .update(sourceMapString)
776
+ .update(sourceMapBuffer)
707
777
  .digest("hex")
708
778
  : undefined;
709
779
 
@@ -775,7 +845,7 @@ class SourceMapDevToolPlugin {
775
845
  assetsInfo[file] = assetInfo;
776
846
  compilation.updateAsset(file, asset, assetInfo);
777
847
  // Add source map file to compilation assets and chunk files
778
- const sourceMapAsset = new RawSource(sourceMapString);
848
+ const sourceMapAsset = new RawSource(sourceMapBuffer);
779
849
  const sourceMapAssetInfo = {
780
850
  ...sourceMapInfo,
781
851
  development: true
@@ -801,6 +871,16 @@ class SourceMapDevToolPlugin {
801
871
  `${PLUGIN_NAME}: append can't be a function when no filename is provided`
802
872
  );
803
873
  }
874
+ // Inline data-URL form: `[map]` gets the raw JSON, `[url]`
875
+ // gets the same JSON base64-encoded. `URL_COMMENT_REGEXP`
876
+ // is a `/g` regex, so a user `append` template with more
877
+ // than one `[url]` placeholder would otherwise re-encode
878
+ // the same JSON per match. Pre-compute both once.
879
+ const sourceMapString = JSON.stringify(outputSourceMap);
880
+ const sourceMapBase64 = Buffer.from(
881
+ sourceMapString,
882
+ "utf8"
883
+ ).toString("base64");
804
884
  /**
805
885
  * Add source map as data url to asset
806
886
  */
@@ -811,10 +891,7 @@ class SourceMapDevToolPlugin {
811
891
  .replace(
812
892
  URL_COMMENT_REGEXP,
813
893
  () =>
814
- `data:application/json;charset=utf-8;base64,${Buffer.from(
815
- sourceMapString,
816
- "utf8"
817
- ).toString("base64")}`
894
+ `data:application/json;charset=utf-8;base64,${sourceMapBase64}`
818
895
  )
819
896
  );
820
897
  assets[file] = asset;
@@ -16,6 +16,7 @@ const { getEntryRuntime, mergeRuntime } = require("./util/runtime");
16
16
  /** @typedef {import("./DependenciesBlock")} DependenciesBlock */
17
17
  /** @typedef {import("./Dependency").DependencyLocation} DependencyLocation */
18
18
  /** @typedef {import("./Entrypoint")} Entrypoint */
19
+ /** @typedef {import("./Entrypoint").EntryOptions} EntryOptions */
19
20
  /** @typedef {import("./Module")} Module */
20
21
  /** @typedef {import("./ModuleGraph")} ModuleGraph */
21
22
  /** @typedef {import("./ModuleGraphConnection").ConnectionState} ConnectionState */
@@ -581,7 +582,27 @@ const visitModules = (
581
582
  }
582
583
  } else {
583
584
  entrypoint = /** @type {Entrypoint} */ (cgi.chunkGroup);
584
- // TODO merge entryOptions
585
+ // Fill in options the existing entrypoint hasn't set yet. We never
586
+ // overwrite: blocks that dedupe to one entrypoint (e.g. several
587
+ // workers pointing at the same module) legitimately carry distinct
588
+ // values such as `runtime`, so the first block to create the
589
+ // entrypoint wins and later ones only contribute missing keys.
590
+ // `name` is excluded: it is the entrypoint's identity, fixed at
591
+ // creation (and used to key namedChunkGroups), so back-filling it
592
+ // from a block that deduped in via its module would leave
593
+ // `entrypoint.name` out of sync and make module codegen
594
+ // order-dependent, which breaks persistent caching.
595
+ const existingOptions = entrypoint.options;
596
+ for (const key_ of Object.keys(entryOptions)) {
597
+ const key =
598
+ /** @type {keyof EntryOptions} */
599
+ (key_);
600
+ if (key === "name") continue;
601
+ if (entryOptions[key] === undefined) continue;
602
+ if (existingOptions[key] !== undefined) continue;
603
+ /** @type {EntryOptions[keyof EntryOptions]} */
604
+ (existingOptions[key]) = entryOptions[key];
605
+ }
585
606
  entrypoint.addOrigin(
586
607
  module,
587
608
  /** @type {DependencyLocation} */ (b.loc),
@@ -1290,7 +1311,8 @@ const connectChunkGroups = (
1290
1311
  // connections and modules can only create one version
1291
1312
  // TODO maybe decide this per runtime
1292
1313
  if (
1293
- // TODO is this needed?
1314
+ // Blocks with nested blocks must stay connected — skipping orphans the
1315
+ // nested block's chunk group from this block's chunk group parent.
1294
1316
  !blocksWithNestedBlocks.has(block) &&
1295
1317
  connections.every(({ chunkGroup, originChunkGroupInfo }) =>
1296
1318
  areModulesAvailable(
@@ -25,7 +25,7 @@ class LazyHashedEtag {
25
25
  * @param {HashFunction} hashFunction the hash function to use
26
26
  */
27
27
  constructor(obj, hashFunction = DEFAULTS.HASH_FUNCTION) {
28
- /** @type {HashableObject} */
28
+ /** @type {HashableObject | undefined} */
29
29
  this._obj = obj;
30
30
  /** @type {undefined | string} */
31
31
  this._hash = undefined;
@@ -40,8 +40,15 @@ class LazyHashedEtag {
40
40
  toString() {
41
41
  if (this._hash === undefined) {
42
42
  const hash = createHash(this._hashFunction);
43
- this._obj.updateHash(hash);
43
+ /** @type {HashableObject} */
44
+ (this._obj).updateHash(hash);
44
45
  this._hash = hash.digest("base64");
46
+ // Drop the captured object once the hash is memoized. The hash is
47
+ // never reset, so we never need `_obj` again — and many callers
48
+ // (e.g. `SourceMapDevToolPlugin`, `RealContentHashPlugin`) capture
49
+ // a heavy `CachedSource` here that would otherwise stay reachable
50
+ // through this etag for the lifetime of the compilation cache.
51
+ this._obj = undefined;
45
52
  }
46
53
  return this._hash;
47
54
  }
@@ -161,9 +161,6 @@ class HarmonyExportInitFragment extends InitFragment {
161
161
  * @returns {string | Source | undefined} the source code that will be included as initialization code
162
162
  */
163
163
  getContent({ runtimeTemplate, runtimeRequirements }) {
164
- runtimeRequirements.add(RuntimeGlobals.exports);
165
- runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);
166
-
167
164
  const unusedPart =
168
165
  this.unusedExports.size > 1
169
166
  ? `/* unused harmony exports ${joinIterableWithComma(
@@ -184,12 +181,14 @@ class HarmonyExportInitFragment extends InitFragment {
184
181
  )}: ${runtimeTemplate.returningFunction(value)}`
185
182
  );
186
183
  }
187
- const definePart =
188
- this.exportMap.size > 0
189
- ? `/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(${
190
- this.exportsArgument
191
- }, {${definitions.join(",")}\n/* harmony export */ });\n`
192
- : "";
184
+ let definePart = "";
185
+ if (this.exportMap.size > 0) {
186
+ runtimeRequirements.add(RuntimeGlobals.exports);
187
+ runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);
188
+ definePart = `/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(${
189
+ this.exportsArgument
190
+ }, {${definitions.join(",")}\n/* harmony export */ });\n`;
191
+ }
193
192
  return `${definePart}${unusedPart}`;
194
193
  }
195
194
  }
@@ -4,8 +4,8 @@
4
4
 
5
5
  "use strict";
6
6
 
7
+ const HtmlGenerator = require("../html/HtmlGenerator");
7
8
  const makeSerializable = require("../util/makeSerializable");
8
- const CssUrlDependency = require("./CssUrlDependency");
9
9
  const ModuleDependency = require("./ModuleDependency");
10
10
 
11
11
  /** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
@@ -101,19 +101,8 @@ HtmlInlineScriptDependency.Template = class HtmlInlineScriptDependencyTemplate e
101
101
 
102
102
  if (entrypoint) {
103
103
  const chunk = /** @type {Chunk} */ (entrypoint.getEntrypointChunk());
104
- const outputOptions = runtimeTemplate.outputOptions;
105
- const filenameTemplate =
106
- chunk.filenameTemplate ||
107
- (chunk.canBeInitial()
108
- ? outputOptions.filename
109
- : outputOptions.chunkFilename);
110
-
111
- const filename = compilation.getPath(filenameTemplate, {
112
- chunk,
113
- contentHashType: "javascript"
114
- });
115
-
116
- url = `${CssUrlDependency.PUBLIC_PATH_AUTO}${filename}`;
104
+ // Defer chunk-URL substitution to renderManifest — chunk hashes aren't ready yet.
105
+ url = HtmlGenerator.makeChunkUrlSentinel(chunk, "javascript");
117
106
  }
118
107
 
119
108
  // Insert ` src="…"` right after `<script` so the inline body is
@@ -10,12 +10,15 @@ const ModuleDependency = require("./ModuleDependency");
10
10
 
11
11
  /** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
12
12
  /** @typedef {import("../CodeGenerationResults")} CodeGenerationResults */
13
+ /** @typedef {import("../Dependency").UpdateHashContext} UpdateHashContext */
13
14
  /** @typedef {import("../Dependency")} Dependency */
14
15
  /** @typedef {import("../DependencyTemplate").DependencyTemplateContext} DependencyTemplateContext */
15
16
  /** @typedef {import("../Module")} Module */
17
+ /** @typedef {import("../Module").BuildInfo} BuildInfo */
16
18
  /** @typedef {import("../javascript/JavascriptParser").Range} Range */
17
19
  /** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
18
20
  /** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
21
+ /** @typedef {import("../util/Hash")} Hash */
19
22
  /** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
20
23
 
21
24
  /**
@@ -45,6 +48,20 @@ class HtmlInlineStyleDependency extends ModuleDependency {
45
48
  return "html-style";
46
49
  }
47
50
 
51
+ /**
52
+ * Updates the hash with the data contributed by this instance.
53
+ * @param {Hash} hash hash to be updated
54
+ * @param {UpdateHashContext} context context
55
+ * @returns {void}
56
+ */
57
+ updateHash(hash, context) {
58
+ // Recurse so the inline CSS's transitive deps (e.g. `url(asset)`) propagate up.
59
+ const { chunkGraph } = context;
60
+ const module = chunkGraph.moduleGraph.getModule(this);
61
+ if (!module) return;
62
+ module.updateHash(hash, context);
63
+ }
64
+
48
65
  /**
49
66
  * Serializes this instance into the provided serializer context.
50
67
  * @param {ObjectSerializerContext} context context
@@ -9,8 +9,8 @@ const {
9
9
  CSS_TYPE,
10
10
  JAVASCRIPT_TYPE
11
11
  } = require("../ModuleSourceTypeConstants");
12
+ const HtmlGenerator = require("../html/HtmlGenerator");
12
13
  const makeSerializable = require("../util/makeSerializable");
13
- const CssUrlDependency = require("./CssUrlDependency");
14
14
  const ModuleDependency = require("./ModuleDependency");
15
15
 
16
16
  /** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
@@ -95,39 +95,6 @@ class HtmlScriptSrcDependency extends ModuleDependency {
95
95
  }
96
96
  }
97
97
 
98
- /**
99
- * @param {Chunk} chunk a chunk
100
- * @param {import("../Compilation")} compilation compilation
101
- * @param {"javascript" | "css"} contentHashType which content hash to plug into the filename template
102
- * @returns {string} chunk filename path (no public-path prefix)
103
- */
104
- const getChunkFilename = (chunk, compilation, contentHashType) => {
105
- const outputOptions = compilation.outputOptions;
106
- let filenameTemplate;
107
- if (contentHashType === "css") {
108
- // For a CSS-typed chunk, use the same template the CSS pipeline
109
- // will use when it actually emits the `.css` file, so the `<link
110
- // rel="stylesheet" href>` URL we write into the HTML matches the
111
- // asset on disk.
112
- filenameTemplate =
113
- require("../css/CssModulesPlugin").getChunkFilenameTemplate(
114
- chunk,
115
- outputOptions
116
- );
117
- } else {
118
- filenameTemplate =
119
- chunk.filenameTemplate ||
120
- (chunk.canBeInitial()
121
- ? outputOptions.filename
122
- : outputOptions.chunkFilename);
123
- }
124
-
125
- return compilation.getPath(filenameTemplate, {
126
- chunk,
127
- contentHashType
128
- });
129
- };
130
-
131
98
  /**
132
99
  * @param {Entrypoint} entrypoint entrypoint
133
100
  * @returns {Chunk[]} every chunk this entrypoint needs in load order: the
@@ -410,15 +377,13 @@ HtmlScriptSrcDependency.Template = class HtmlScriptSrcDependencyTemplate extends
410
377
  const entryChunk = orderedChunks[orderedChunks.length - 1];
411
378
  const isStylesheet = dep.elementKind === "stylesheet";
412
379
 
413
- // Rewrite the originating tag's src/href to the entry chunk's
414
- // primary asset for that element kind: `.css` for
415
- // `<link rel="stylesheet">`, `.js` for everything else.
380
+ // Rewrite src/href to a chunk-URL sentinel (resolved by renderManifest):
381
+ // `.css` for `<link rel="stylesheet">`, `.js` for everything else.
416
382
  const entryContentHashType = isStylesheet ? "css" : "javascript";
417
- const entryUrl = `${CssUrlDependency.PUBLIC_PATH_AUTO}${getChunkFilename(
383
+ const entryUrl = HtmlGenerator.makeChunkUrlSentinel(
418
384
  entryChunk,
419
- compilation,
420
385
  entryContentHashType
421
- )}`;
386
+ );
422
387
  source.replace(dep.range[0], dep.range[1] - 1, entryUrl);
423
388
 
424
389
  if (dep.tagStart < 0 || dep.tagOpenEnd <= dep.tagStart) {
@@ -446,11 +411,7 @@ HtmlScriptSrcDependency.Template = class HtmlScriptSrcDependencyTemplate extends
446
411
  * @returns {string} a single sibling tag's HTML
447
412
  */
448
413
  const buildSibling = (chunk, kind) => {
449
- const url = `${CssUrlDependency.PUBLIC_PATH_AUTO}${getChunkFilename(
450
- chunk,
451
- compilation,
452
- kind
453
- )}`;
414
+ const url = HtmlGenerator.makeChunkUrlSentinel(chunk, kind);
454
415
  if (kind === "css" && !isStylesheet) {
455
416
  // Originating tag is `<script>` (or `<link rel=modulepreload>`)
456
417
  // but this chunk is CSS — emit a fresh `<link>` rather than
@@ -13,14 +13,17 @@ const ModuleDependency = require("./ModuleDependency");
13
13
 
14
14
  /** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
15
15
  /** @typedef {import("../CodeGenerationResults")} CodeGenerationResults */
16
+ /** @typedef {import("../Dependency").UpdateHashContext} UpdateHashContext */
16
17
  /** @typedef {import("../Dependency")} Dependency */
17
18
  /** @typedef {import("../DependencyTemplate").CssDependencyTemplateContext} DependencyTemplateContext */
18
19
  /** @typedef {import("../Module")} Module */
20
+ /** @typedef {import("../Module").BuildInfo} BuildInfo */
19
21
  /** @typedef {import("../Module").CodeGenerationResult} CodeGenerationResult */
20
22
  /** @typedef {import("../Module").CodeGenerationResultData} CodeGenerationResultData */
21
23
  /** @typedef {import("../javascript/JavascriptParser").Range} Range */
22
24
  /** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
23
25
  /** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
26
+ /** @typedef {import("../util/Hash")} Hash */
24
27
  /** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
25
28
 
26
29
  const getIgnoredRawDataUrlModule = memoize(
@@ -55,6 +58,21 @@ class HtmlSourceDependency extends ModuleDependency {
55
58
  return getIgnoredRawDataUrlModule();
56
59
  }
57
60
 
61
+ /**
62
+ * Updates the hash with the data contributed by this instance.
63
+ * @param {Hash} hash hash to be updated
64
+ * @param {UpdateHashContext} context context
65
+ * @returns {void}
66
+ */
67
+ updateHash(hash, context) {
68
+ // Fold in the asset's hash so the HTML invalidates when the embedded URL changes.
69
+ const { chunkGraph } = context;
70
+ const module = chunkGraph.moduleGraph.getModule(this);
71
+ if (!module) return;
72
+ const { hash: buildHash } = /** @type {BuildInfo} */ (module.buildInfo);
73
+ if (buildHash) hash.update(buildHash);
74
+ }
75
+
58
76
  /**
59
77
  * Serializes this instance into the provided serializer context.
60
78
  * @param {ObjectSerializerContext} context context
@@ -349,10 +349,24 @@ class WorkerPlugin {
349
349
  )
350
350
  );
351
351
  } else {
352
- Object.assign(
353
- entryOptions,
354
- importOptions.webpackEntryOptions
355
- );
352
+ // `webpackEntryOptions` is user input from a magic
353
+ // comment, so copy only safe own keys to avoid
354
+ // prototype pollution via `__proto__`/`constructor`/
355
+ // `prototype`.
356
+ const userEntryOptions = importOptions.webpackEntryOptions;
357
+ for (const key of Object.keys(userEntryOptions)) {
358
+ if (
359
+ key === "__proto__" ||
360
+ key === "constructor" ||
361
+ key === "prototype"
362
+ ) {
363
+ continue;
364
+ }
365
+ /** @type {EXPECTED_ANY} */
366
+ (entryOptions)[key] = /** @type {EXPECTED_ANY} */ (
367
+ userEntryOptions
368
+ )[key];
369
+ }
356
370
  }
357
371
  }
358
372
  if (importOptions.webpackChunkName !== undefined) {
@@ -18,6 +18,7 @@ const {
18
18
  const RuntimeGlobals = require("../RuntimeGlobals");
19
19
  const Template = require("../Template");
20
20
  const CommonJsRequireDependency = require("../dependencies/CommonJsRequireDependency");
21
+ const { resolveByProperty } = require("../util/cleverMerge");
21
22
  const { registerNotSerializable } = require("../util/serialization");
22
23
 
23
24
  /** @typedef {import("../config/defaults").WebpackOptionsNormalizedWithDefaults} WebpackOptions */
@@ -45,6 +46,96 @@ const { registerNotSerializable } = require("../util/serialization");
45
46
 
46
47
  /** @typedef {{ client: string, data: string, active: boolean }} ModuleResult */
47
48
 
49
+ /**
50
+ * Library wrappers of these types pass external modules as closure arguments
51
+ * (e.g. `__WEBPACK_EXTERNAL_MODULE_react__`) baked into the entry chunk at
52
+ * render time. When `lazyCompilation` activates a proxy for the first time,
53
+ * any external dependency the lazily-built module pulls in lands in a hot
54
+ * update chunk that lives outside the original wrapper closure, so the
55
+ * factory body can't resolve its closure identifier and throws at runtime.
56
+ * Reserving the externals up front (during the inactive build) folds them
57
+ * into the initial wrapper, so the closure identifiers are already defined
58
+ * when the activation update arrives.
59
+ */
60
+ const CLOSURE_LIBRARY_TYPES = new Set([
61
+ "umd",
62
+ "umd2",
63
+ "amd",
64
+ "amd-require",
65
+ "system"
66
+ ]);
67
+
68
+ /**
69
+ * `enabledLibraryTypes` covers both the global `output.library.type` and any
70
+ * per-entry `entry.<name>.library.type`, so a UMD/AMD/System wrapper attached
71
+ * to an individual entry is still detected.
72
+ * @param {import("../../declarations/WebpackOptions").OutputNormalized} output normalized output option
73
+ * @returns {boolean} true when at least one library wrapper passes externals as closure arguments
74
+ */
75
+ const hasClosureLibrary = (output) => {
76
+ const enabled = output.enabledLibraryTypes;
77
+ if (enabled) {
78
+ for (const type of enabled) {
79
+ if (CLOSURE_LIBRARY_TYPES.has(type)) return true;
80
+ }
81
+ }
82
+ if (output.library && output.library.type) {
83
+ return CLOSURE_LIBRARY_TYPES.has(output.library.type);
84
+ }
85
+ return false;
86
+ };
87
+
88
+ /**
89
+ * Collects request strings from statically-enumerable externals (string,
90
+ * object, and arrays of those). Function and RegExp forms are skipped because
91
+ * their effective request set isn't knowable until something asks for it.
92
+ *
93
+ * Layer resolution mirrors `ExternalModuleFactoryPlugin.resolveLayer`: the
94
+ * effective map for the proxy's layer is computed via the same
95
+ * `resolveByProperty(..., "byLayer", layer)` helper that the externals system
96
+ * uses, so `byLayer.default` fallback and function-form `byLayer` entries are
97
+ * honored the same way.
98
+ *
99
+ * Entries whose effective value is `false` are skipped — `false` explicitly
100
+ * disables externalization for that request, and reserving it would force the
101
+ * real module into the entry chunk.
102
+ * @param {import("../../declarations/WebpackOptions").Externals | undefined} externals normalized externals option
103
+ * @param {string | null} layer issuer layer for which to resolve `byLayer`
104
+ * @returns {Set<string>} requests to reserve in the entry chunk
105
+ */
106
+ const collectStaticExternalRequests = (externals, layer) => {
107
+ /** @type {Set<string>} */
108
+ const requests = new Set();
109
+ if (!externals) return requests;
110
+ /** @param {import("../../declarations/WebpackOptions").ExternalItem} item one item */
111
+ const visit = (item) => {
112
+ if (typeof item === "string") {
113
+ requests.add(item);
114
+ return;
115
+ }
116
+ if (!item || typeof item !== "object" || item instanceof RegExp) return;
117
+ const resolved = /** @type {Record<string, unknown>} */ (
118
+ resolveByProperty(
119
+ /** @type {Record<string, unknown>} */ (item),
120
+ "byLayer",
121
+ layer
122
+ )
123
+ );
124
+ for (const [request, value] of Object.entries(resolved)) {
125
+ // `false` explicitly opts the request out of externalization; reserving
126
+ // it would pull the actual module into the entry chunk.
127
+ if (value === false) continue;
128
+ requests.add(request);
129
+ }
130
+ };
131
+ if (Array.isArray(externals)) {
132
+ for (const item of externals) visit(item);
133
+ } else {
134
+ visit(externals);
135
+ }
136
+ return requests;
137
+ };
138
+
48
139
  /**
49
140
  * Defines the backend api type used by this module.
50
141
  * @typedef {object} BackendApi
@@ -213,6 +304,19 @@ class LazyCompilationProxyModule extends Module {
213
304
  const block = new AsyncDependenciesBlock({});
214
305
  block.addDependency(dep);
215
306
  this.addBlock(block);
307
+ } else if (hasClosureLibrary(compilation.options.output)) {
308
+ // Reserve statically-declared externals as dependencies of the inactive
309
+ // proxy so the initial entry chunk's library wrapper already exposes
310
+ // their closure identifiers (e.g. `__WEBPACK_EXTERNAL_MODULE_react__`).
311
+ // Once the proxy activates and the lazily-built module references those
312
+ // externals, the identifiers resolve normally instead of throwing.
313
+ const requests = collectStaticExternalRequests(
314
+ options.externals,
315
+ this.layer
316
+ );
317
+ for (const request of requests) {
318
+ this.addDependency(new CommonJsRequireDependency(request));
319
+ }
216
320
  }
217
321
  callback();
218
322
  }