webpack 5.107.0 → 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.
Files changed (64) hide show
  1. package/lib/BannerPlugin.js +3 -4
  2. package/lib/Chunk.js +21 -25
  3. package/lib/ChunkGroup.js +57 -15
  4. package/lib/Compilation.js +33 -11
  5. package/lib/Compiler.js +27 -3
  6. package/lib/ContextModuleFactory.js +45 -38
  7. package/lib/EvalSourceMapDevToolPlugin.js +0 -1
  8. package/lib/ExportsInfo.js +30 -34
  9. package/lib/ExternalModule.js +15 -11
  10. package/lib/ExternalModuleFactoryPlugin.js +2 -1
  11. package/lib/Module.js +1 -1
  12. package/lib/ModuleNotFoundError.js +10 -0
  13. package/lib/ModuleSourceTypeConstants.js +24 -22
  14. package/lib/MultiCompiler.js +14 -0
  15. package/lib/NormalModule.js +531 -53
  16. package/lib/NormalModuleFactory.js +38 -26
  17. package/lib/ProgressPlugin.js +1 -1
  18. package/lib/RuntimePlugin.js +1 -1
  19. package/lib/SourceMapDevToolPlugin.js +335 -57
  20. package/lib/Template.js +1 -1
  21. package/lib/TemplatedPathPlugin.js +22 -4
  22. package/lib/asset/AssetBytesGenerator.js +6 -6
  23. package/lib/asset/AssetGenerator.js +14 -14
  24. package/lib/asset/AssetModulesPlugin.js +3 -7
  25. package/lib/asset/AssetSourceGenerator.js +6 -6
  26. package/lib/buildChunkGraph.js +24 -2
  27. package/lib/cache/getLazyHashedEtag.js +9 -2
  28. package/lib/css/CssModulesPlugin.js +2 -2
  29. package/lib/dependencies/CommonJsImportsParserPlugin.js +108 -1
  30. package/lib/dependencies/CssUrlDependency.js +3 -2
  31. package/lib/dependencies/HarmonyDetectionParserPlugin.js +21 -1
  32. package/lib/dependencies/HarmonyExportInitFragment.js +8 -9
  33. package/lib/dependencies/HtmlInlineScriptDependency.js +3 -14
  34. package/lib/dependencies/HtmlInlineStyleDependency.js +17 -0
  35. package/lib/dependencies/HtmlScriptSrcDependency.js +265 -65
  36. package/lib/dependencies/HtmlSourceDependency.js +21 -2
  37. package/lib/dependencies/WorkerPlugin.js +18 -4
  38. package/lib/hmr/LazyCompilationPlugin.js +104 -0
  39. package/lib/html/HtmlGenerator.js +81 -33
  40. package/lib/html/HtmlModulesPlugin.js +87 -28
  41. package/lib/html/walkHtmlTokens.js +641 -125
  42. package/lib/index.js +2 -0
  43. package/lib/javascript/JavascriptModulesPlugin.js +2 -2
  44. package/lib/javascript/JavascriptParser.js +1 -1
  45. package/lib/library/ModuleLibraryPlugin.js +30 -24
  46. package/lib/node/NodeWatchFileSystem.js +37 -22
  47. package/lib/optimize/ConcatenatedModule.js +3 -2
  48. package/lib/optimize/SideEffectsFlagPlugin.js +1 -2
  49. package/lib/optimize/SplitChunksPlugin.js +4 -4
  50. package/lib/runtime/AutoPublicPathRuntimeModule.js +3 -3
  51. package/lib/runtime/GetChunkFilenameRuntimeModule.js +5 -5
  52. package/lib/sharing/ConsumeSharedPlugin.js +2 -8
  53. package/lib/sharing/ProvideSharedPlugin.js +4 -4
  54. package/lib/util/fs.js +6 -1
  55. package/lib/wasm-async/AsyncWebAssemblyModulesPlugin.js +1 -2
  56. package/package.json +5 -5
  57. package/schemas/WebpackOptions.check.js +1 -1
  58. package/schemas/WebpackOptions.json +11 -9
  59. package/schemas/plugins/container/ContainerReferencePlugin.check.js +1 -1
  60. package/schemas/plugins/container/ContainerReferencePlugin.json +1 -0
  61. package/schemas/plugins/container/ExternalsType.check.js +1 -1
  62. package/schemas/plugins/container/ModuleFederationPlugin.check.js +1 -1
  63. package/schemas/plugins/container/ModuleFederationPlugin.json +1 -0
  64. package/types.d.ts +472 -149
@@ -30,18 +30,18 @@ const { makePathsAbsolute } = require("./util/identifier");
30
30
  /** @typedef {import("./Compiler")} Compiler */
31
31
  /** @typedef {import("./Module")} Module */
32
32
  /** @typedef {import("./NormalModule").RawSourceMap} RawSourceMap */
33
- /** @typedef {import("./TemplatedPathPlugin").TemplatePath} TemplatePath */
33
+ /** @typedef {import("./TemplatedPathPlugin").TemplatePath} SourceMappingURLComment */
34
34
  /** @typedef {import("./util/fs").OutputFileSystem} OutputFileSystem */
35
35
 
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
 
@@ -70,6 +70,93 @@ const resetRegexpState = (regexp) => {
70
70
  */
71
71
  const quoteMeta = (str) => str.replace(METACHARACTERS_REGEXP, "\\$&");
72
72
 
73
+ /**
74
+ * Compilation-scoped registry of original asset sources for multi-plugin
75
+ * cooperation. The first SourceMapDevToolPlugin instance to see a file pins a
76
+ * reference to the asset's still-unwrapped {@link Source} object; later
77
+ * instances whose `asset.source.sourceAndMap()` would now return `null` (the
78
+ * earlier instance replaced the asset with a `RawSource`) can re-extract the
79
+ * map from this pinned reference. We keep the registry on a module-scoped
80
+ * `WeakMap` so the entries are reclaimed automatically when the compilation
81
+ * itself becomes unreachable; we never store anything on the compilation
82
+ * object directly.
83
+ *
84
+ * Stashing the `Source` object itself rather than an extracted map keeps the
85
+ * fast path free of cloning and source-map serialization work — the
86
+ * extraction only happens if a subsequent plugin actually needs the map.
87
+ * @type {WeakMap<Compilation, Map<string, Source>>}
88
+ */
89
+ const originalSourceRegistry = new WeakMap();
90
+
91
+ /**
92
+ * Returns (creating if necessary) the per-compilation registry of original
93
+ * asset {@link Source} objects.
94
+ * @param {Compilation} compilation compilation
95
+ * @returns {Map<string, Source>} registry
96
+ */
97
+ const getOriginalSourceRegistry = (compilation) => {
98
+ let registry = originalSourceRegistry.get(compilation);
99
+ if (registry === undefined) {
100
+ registry = new Map();
101
+ originalSourceRegistry.set(compilation, registry);
102
+ }
103
+ return registry;
104
+ };
105
+
106
+ /**
107
+ * Extracts source and source map from a Source object, falling back to a
108
+ * registered original source for assets that another SourceMapDevToolPlugin
109
+ * instance has already wrapped (whose internal map is now `null`).
110
+ *
111
+ * The returned source is read from the asset as it currently stands — that way
112
+ * any `sourceMappingURL` comments appended by earlier plugin instances survive
113
+ * — while the map is taken from the pinned original Source when the current
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.
117
+ * @param {string} file file name
118
+ * @param {Source} asset source object as currently held by the compilation
119
+ * @param {MapOptions} options map extraction options
120
+ * @param {Map<string, Source>} registry compilation-scoped original-source registry
121
+ * @returns {{ source: string, sourceMap: RawSourceMap, mapSource: Source } | undefined} extracted pair or `undefined` when no map is recoverable
122
+ */
123
+ const extractSourceAndMap = (file, asset, options, registry) => {
124
+ /** @type {string | Buffer} */
125
+ let source;
126
+ /** @type {null | RawSourceMap} */
127
+ let sourceMap;
128
+ if (asset.sourceAndMap) {
129
+ const sourceAndMap = asset.sourceAndMap(options);
130
+ source = sourceAndMap.source;
131
+ sourceMap = sourceAndMap.map;
132
+ } else {
133
+ source = asset.source();
134
+ sourceMap = asset.map(options);
135
+ }
136
+ // Bail before touching the registry if we can't return a usable string
137
+ // source — pinning a non-string-producing asset would only waste the slot.
138
+ if (typeof source !== "string") return;
139
+ if (sourceMap) {
140
+ // The current asset still owns the original map — pin a reference so
141
+ // that a later plugin instance (which will see a rewrapped asset
142
+ // without a map) can recover it on demand.
143
+ if (!registry.has(file)) registry.set(file, asset);
144
+ return { source, sourceMap, mapSource: asset };
145
+ }
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 };
158
+ };
159
+
73
160
  /**
74
161
  * Creating {@link SourceMapTask} for given file
75
162
  * @param {string} file current compiled file
@@ -78,6 +165,7 @@ const quoteMeta = (str) => str.replace(METACHARACTERS_REGEXP, "\\$&");
78
165
  * @param {MapOptions} options source map options
79
166
  * @param {Compilation} compilation compilation instance
80
167
  * @param {ItemCacheFacade} cacheItem cache item
168
+ * @param {Map<string, Source>} registry compilation-scoped original-source registry
81
169
  * @returns {SourceMapTask | undefined} created task instance or `undefined`
82
170
  */
83
171
  const getTaskForFile = (
@@ -86,24 +174,12 @@ const getTaskForFile = (
86
174
  assetInfo,
87
175
  options,
88
176
  compilation,
89
- cacheItem
177
+ cacheItem,
178
+ registry
90
179
  ) => {
91
- /** @type {string | Buffer} */
92
- let source;
93
- /** @type {null | RawSourceMap} */
94
- let sourceMap;
95
- /**
96
- * Check if asset can build source map
97
- */
98
- if (asset.sourceAndMap) {
99
- const sourceAndMap = asset.sourceAndMap(options);
100
- sourceMap = sourceAndMap.map;
101
- source = sourceAndMap.source;
102
- } else {
103
- sourceMap = asset.map(options);
104
- source = asset.source();
105
- }
106
- if (!sourceMap || typeof source !== "string") return;
180
+ const extracted = extractSourceAndMap(file, asset, options, registry);
181
+ if (!extracted) return;
182
+ const { source, sourceMap, mapSource } = extracted;
107
183
  const context = compilation.options.context;
108
184
  const root = compilation.compiler.root;
109
185
  const cachedAbsolutify = makePathsAbsolute.bindContextCache(context, root);
@@ -116,10 +192,10 @@ const getTaskForFile = (
116
192
 
117
193
  return {
118
194
  file,
119
- asset,
120
- source,
195
+ source: /** @type {string} */ (source),
121
196
  assetInfo,
122
197
  sourceMap,
198
+ mapSource,
123
199
  modules,
124
200
  cacheItem
125
201
  };
@@ -127,6 +203,29 @@ const getTaskForFile = (
127
203
 
128
204
  const PLUGIN_NAME = "SourceMapDevToolPlugin";
129
205
 
206
+ /**
207
+ * Maps a configuration value (string, RegExp, function, nullish, or array of
208
+ * such) into a JSON-serializable form. Functions and RegExps are turned into
209
+ * their `.toString()` representation so that changes to inline callbacks
210
+ * invalidate caches; everything else is returned as-is so that the surrounding
211
+ * `JSON.stringify` does the escaping.
212
+ *
213
+ * The result is used through `JSON.stringify` to build cache identifiers, so
214
+ * we deliberately avoid any homemade `|` / `,` separators that could collide
215
+ * with characters appearing inside user-provided values such as `publicPath`,
216
+ * template strings, or `sourceRoot`.
217
+ * @param {EXPECTED_ANY} value option value
218
+ * @returns {EXPECTED_ANY} JSON-serializable representation
219
+ */
220
+ const toCacheKeyValue = (value) => {
221
+ if (value === undefined || value === null) return value;
222
+ if (Array.isArray(value)) return value.map(toCacheKeyValue);
223
+ if (value instanceof RegExp || typeof value === "function") {
224
+ return value.toString();
225
+ }
226
+ return value;
227
+ };
228
+
130
229
  class SourceMapDevToolPlugin {
131
230
  /**
132
231
  * Creates an instance of SourceMapDevToolPlugin.
@@ -136,7 +235,7 @@ class SourceMapDevToolPlugin {
136
235
  constructor(options = {}) {
137
236
  /** @type {undefined | null | false | string} */
138
237
  this.sourceMapFilename = options.filename;
139
- /** @type {false | TemplatePath} */
238
+ /** @type {false | SourceMappingURLComment} */
140
239
  this.sourceMappingURLComment =
141
240
  options.append === false
142
241
  ? false
@@ -153,6 +252,28 @@ class SourceMapDevToolPlugin {
153
252
  this.namespace = options.namespace || "";
154
253
  /** @type {SourceMapDevToolPluginOptions} */
155
254
  this.options = options;
255
+ // Cache salt derived from output-affecting options, so that two
256
+ // SourceMapDevToolPlugin instances (or `devtool` + a plugin) operating
257
+ // on the same asset don't share a cache entry. We serialize via
258
+ // `JSON.stringify` rather than a homemade separator so that any
259
+ // special characters (e.g. `|` inside a publicPath or sourceRoot)
260
+ // can't accidentally make two different option sets collide.
261
+ /** @type {string} */
262
+ this._cacheSalt = JSON.stringify([
263
+ toCacheKeyValue(options.filename),
264
+ toCacheKeyValue(options.append),
265
+ toCacheKeyValue(this.moduleFilenameTemplate),
266
+ toCacheKeyValue(this.fallbackModuleFilenameTemplate),
267
+ toCacheKeyValue(this.namespace),
268
+ options.module !== false,
269
+ options.columns !== false,
270
+ Boolean(options.noSources),
271
+ Boolean(options.debugIds),
272
+ options.sourceRoot || "",
273
+ toCacheKeyValue(options.ignoreList),
274
+ options.publicPath || "",
275
+ options.fileContext || ""
276
+ ]);
156
277
  }
157
278
 
158
279
  /**
@@ -195,6 +316,14 @@ class SourceMapDevToolPlugin {
195
316
  compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
196
317
  new SourceMapDevToolModuleOptionsPlugin(options).apply(compilation);
197
318
 
319
+ // All SourceMapDevToolPlugin instances on the same compilation share
320
+ // a registry of pristine asset sources, so the second instance to
321
+ // run can still recover the original map after the first instance
322
+ // has replaced the asset with a `RawSource`. The registry lives on a
323
+ // module-scoped `WeakMap` keyed by compilation so it is released
324
+ // automatically and never pollutes the compilation object.
325
+ const originalSources = getOriginalSourceRegistry(compilation);
326
+
198
327
  compilation.hooks.processAssets.tapAsync(
199
328
  {
200
329
  name: PLUGIN_NAME,
@@ -233,24 +362,47 @@ class SourceMapDevToolPlugin {
233
362
  const tasks = [];
234
363
  let fileIndex = 0;
235
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
+
236
384
  asyncLib.each(
237
385
  files,
238
386
  (file, callback) => {
239
387
  const asset =
240
388
  /** @type {Readonly<Asset>} */
241
389
  (compilation.getAsset(file));
242
- if (asset.info.related && asset.info.related.sourceMap) {
243
- fileIndex++;
244
- return callback();
245
- }
246
390
 
247
391
  const chunk = fileToChunk.get(file);
248
392
  const sourceMapNamespace = compilation.getPath(this.namespace, {
249
393
  chunk
250
394
  });
251
395
 
396
+ // The cache item identifier must include the per-instance
397
+ // salt so two SourceMapDevToolPlugin instances that target
398
+ // the same `file` don't collide in the persistent cache —
399
+ // they'd otherwise write different content to the same key
400
+ // and invalidate every pack on each build. We encode via
401
+ // `JSON.stringify` so that special characters (e.g. `|`)
402
+ // in an asset filename can't be spoofed to collide with the
403
+ // salt portion of the identifier.
252
404
  const cacheItem = cache.getItemCache(
253
- file,
405
+ JSON.stringify([file, this._cacheSalt]),
254
406
  cache.mergeEtags(
255
407
  cache.getLazyHashedEtag(asset.source),
256
408
  sourceMapNamespace
@@ -265,6 +417,16 @@ class SourceMapDevToolPlugin {
265
417
  * If presented in cache, reassigns assets. Cache assets already have source maps.
266
418
  */
267
419
  if (cacheEntry) {
420
+ // Pin the still-unwrapped asset source in the registry
421
+ // before `compilation.updateAsset` replaces it. This is a
422
+ // pointer assignment — no source-map extraction work — and
423
+ // it lets a subsequent SourceMapDevToolPlugin instance
424
+ // extract the original map on demand even though the
425
+ // persistent cache hit lets us skip processing here.
426
+ if (!originalSources.has(file)) {
427
+ originalSources.set(file, asset.source);
428
+ }
429
+
268
430
  const { assets, assetsInfo } = cacheEntry;
269
431
  for (const cachedFile of Object.keys(assets)) {
270
432
  if (cachedFile === file) {
@@ -313,9 +475,48 @@ class SourceMapDevToolPlugin {
313
475
  columns: options.columns
314
476
  },
315
477
  compilation,
316
- cacheItem
478
+ cacheItem,
479
+ originalSources
317
480
  );
318
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
+
319
520
  if (task) {
320
521
  const modules = task.modules;
321
522
 
@@ -446,12 +647,21 @@ class SourceMapDevToolPlugin {
446
647
  "attach SourceMap"
447
648
  );
448
649
 
449
- const moduleFilenames = modules.map((m) =>
450
- moduleToSourceNameMapping.get(m)
451
- );
452
- sourceMap.sources = /** @type {string[]} */ (moduleFilenames);
650
+ const moduleFilenames =
651
+ /** @type {string[]} */
652
+ (modules.map((m) => moduleToSourceNameMapping.get(m)));
653
+ // We deliberately do NOT mutate `sourceMap` in place: the
654
+ // task's `sourceMap` reference may be shared with a
655
+ // `SourceMapSource` whose internal map cache is the same
656
+ // object (webpack-sources keeps it cached). A second
657
+ // `SourceMapDevToolPlugin` instance that reads the original
658
+ // source through the registry would otherwise see our
659
+ // rewrites. Instead we build a fresh `outputSourceMap` for
660
+ // the .map file and leave the original alone.
661
+ /** @type {number[] | undefined} */
662
+ let ignoreList;
453
663
  if (options.ignoreList) {
454
- const ignoreList = sourceMap.sources.reduce(
664
+ const list = moduleFilenames.reduce(
455
665
  /** @type {(acc: number[], sourceName: string, idx: number) => number[]} */ (
456
666
  (acc, sourceName, idx) => {
457
667
  const rule = /** @type {Rules} */ (
@@ -467,35 +677,29 @@ class SourceMapDevToolPlugin {
467
677
  ),
468
678
  []
469
679
  );
470
- if (ignoreList.length > 0) {
471
- sourceMap.ignoreList = ignoreList;
472
- }
680
+ if (list.length > 0) ignoreList = list;
473
681
  }
474
682
 
475
- if (options.noSources) {
476
- sourceMap.sourcesContent = undefined;
477
- }
478
- sourceMap.sourceRoot = options.sourceRoot || "";
479
- sourceMap.file = file;
480
683
  const usesContentHash =
481
684
  sourceMapFilename &&
482
685
  CONTENT_HASH_DETECT_REGEXP.test(sourceMapFilename);
483
686
 
484
687
  resetRegexpState(CONTENT_HASH_DETECT_REGEXP);
485
688
 
689
+ let outputFile = file;
486
690
  // If SourceMap and asset uses contenthash, avoid a circular dependency by hiding hash in `file`
487
691
  if (usesContentHash && task.assetInfo.contenthash) {
488
692
  const contenthash = task.assetInfo.contenthash;
489
693
  const pattern = Array.isArray(contenthash)
490
694
  ? contenthash.map(quoteMeta).join("|")
491
695
  : quoteMeta(contenthash);
492
- sourceMap.file = sourceMap.file.replace(
696
+ outputFile = outputFile.replace(
493
697
  new RegExp(pattern, "g"),
494
698
  (m) => "x".repeat(m.length)
495
699
  );
496
700
  }
497
701
 
498
- /** @type {false | TemplatePath} */
702
+ /** @type {false | SourceMappingURLComment} */
499
703
  let currentSourceMappingURLComment = sourceMappingURLComment;
500
704
  const cssExtensionDetected =
501
705
  CSS_EXTENSION_DETECT_REGEXP.test(file);
@@ -512,23 +716,64 @@ class SourceMapDevToolPlugin {
512
716
  );
513
717
  }
514
718
 
719
+ /** @type {string | undefined} */
720
+ let debugIdValue;
515
721
  if (options.debugIds) {
516
- const debugId = generateDebugId(source, sourceMap.file);
517
- sourceMap.debugId = debugId;
722
+ const debugId = generateDebugId(source, outputFile);
723
+ debugIdValue = debugId;
518
724
 
519
725
  const debugIdComment = `\n//# debugId=${debugId}`;
520
- currentSourceMappingURLComment =
521
- currentSourceMappingURLComment
522
- ? `${debugIdComment}${currentSourceMappingURLComment}`
523
- : debugIdComment;
726
+ if (currentSourceMappingURLComment === false) {
727
+ currentSourceMappingURLComment = debugIdComment;
728
+ } else if (
729
+ typeof currentSourceMappingURLComment === "function"
730
+ ) {
731
+ // Wrap the user's append function so the debug-id
732
+ // comment is prepended at call time. Template-string
733
+ // concatenation would coerce the function to a string
734
+ // and lose its dynamic behavior.
735
+ const wrappedFn = currentSourceMappingURLComment;
736
+ currentSourceMappingURLComment = (pathData, assetInfo) =>
737
+ `${debugIdComment}${wrappedFn(pathData, assetInfo)}`;
738
+ } else {
739
+ currentSourceMappingURLComment = `${debugIdComment}${currentSourceMappingURLComment}`;
740
+ }
741
+ }
742
+
743
+ /** @type {RawSourceMap} */
744
+ const outputSourceMap = {
745
+ ...sourceMap,
746
+ sources: moduleFilenames,
747
+ sourceRoot: options.sourceRoot || "",
748
+ file: outputFile
749
+ };
750
+ if (ignoreList !== undefined) {
751
+ outputSourceMap.ignoreList = ignoreList;
752
+ }
753
+ if (options.noSources) {
754
+ outputSourceMap.sourcesContent = undefined;
755
+ }
756
+ if (debugIdValue !== undefined) {
757
+ outputSourceMap.debugId = debugIdValue;
524
758
  }
525
759
 
526
- const sourceMapString = JSON.stringify(sourceMap);
527
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
+ );
528
773
  const filename = file;
529
774
  const sourceMapContentHash = usesContentHash
530
775
  ? createHash(compilation.outputOptions.hashFunction)
531
- .update(sourceMapString)
776
+ .update(sourceMapBuffer)
532
777
  .digest("hex")
533
778
  : undefined;
534
779
 
@@ -567,14 +812,40 @@ class SourceMapDevToolPlugin {
567
812
  })
568
813
  );
569
814
  }
815
+ // Preserve any existing related.sourceMap entries from
816
+ // earlier SourceMapDevToolPlugin runs on the same asset so
817
+ // that all generated maps remain discoverable via asset
818
+ // info (the schema allows string or string[]).
819
+ const existingSourceMap =
820
+ task.assetInfo.related &&
821
+ task.assetInfo.related.sourceMap;
822
+ /** @type {string | string[]} */
823
+ let relatedSourceMap;
824
+ if (
825
+ existingSourceMap === undefined ||
826
+ existingSourceMap === null
827
+ ) {
828
+ relatedSourceMap = sourceMapFile;
829
+ } else if (Array.isArray(existingSourceMap)) {
830
+ relatedSourceMap = existingSourceMap.includes(
831
+ sourceMapFile
832
+ )
833
+ ? existingSourceMap
834
+ : [...existingSourceMap, sourceMapFile];
835
+ } else {
836
+ relatedSourceMap =
837
+ existingSourceMap === sourceMapFile
838
+ ? existingSourceMap
839
+ : [existingSourceMap, sourceMapFile];
840
+ }
570
841
  const assetInfo = {
571
- related: { sourceMap: sourceMapFile }
842
+ related: { sourceMap: relatedSourceMap }
572
843
  };
573
844
  assets[file] = asset;
574
845
  assetsInfo[file] = assetInfo;
575
846
  compilation.updateAsset(file, asset, assetInfo);
576
847
  // Add source map file to compilation assets and chunk files
577
- const sourceMapAsset = new RawSource(sourceMapString);
848
+ const sourceMapAsset = new RawSource(sourceMapBuffer);
578
849
  const sourceMapAssetInfo = {
579
850
  ...sourceMapInfo,
580
851
  development: true
@@ -600,6 +871,16 @@ class SourceMapDevToolPlugin {
600
871
  `${PLUGIN_NAME}: append can't be a function when no filename is provided`
601
872
  );
602
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");
603
884
  /**
604
885
  * Add source map as data url to asset
605
886
  */
@@ -610,10 +891,7 @@ class SourceMapDevToolPlugin {
610
891
  .replace(
611
892
  URL_COMMENT_REGEXP,
612
893
  () =>
613
- `data:application/json;charset=utf-8;base64,${Buffer.from(
614
- sourceMapString,
615
- "utf8"
616
- ).toString("base64")}`
894
+ `data:application/json;charset=utf-8;base64,${sourceMapBase64}`
617
895
  )
618
896
  );
619
897
  assets[file] = asset;
package/lib/Template.js CHANGED
@@ -63,7 +63,7 @@ const MATCH_PADDED_HYPHENS_REPLACE_REGEX = /^-|-$/g;
63
63
  * Defines the render manifest entry templated type used by this module.
64
64
  * @typedef {object} RenderManifestEntryTemplated
65
65
  * @property {() => Source} render
66
- * @property {TemplatePath} filenameTemplate
66
+ * @property {string | import("./TemplatedPathPlugin").TemplatePathFn<EXPECTED_ANY>} filenameTemplate
67
67
  * @property {PathData=} pathOptions
68
68
  * @property {AssetInfo=} info
69
69
  * @property {string} identifier
@@ -18,6 +18,8 @@ const getMimeTypes = memoize(() => require("./util/mimeTypes"));
18
18
  /** @typedef {import("./ChunkGraph").ModuleId} ModuleId */
19
19
  /** @typedef {import("./Compilation").AssetInfo} AssetInfo */
20
20
  /** @typedef {import("./Compilation").PathData} PathData */
21
+ /** @typedef {import("./Compilation").PathDataChunk} PathDataChunk */
22
+ /** @typedef {import("./Compilation").PathDataModule} PathDataModule */
21
23
  /** @typedef {import("./Compiler")} Compiler */
22
24
 
23
25
  const REGEXP = /\[\\*([\w:]+)\\*\]/g;
@@ -138,13 +140,29 @@ const deprecated = (fn, message, code) => {
138
140
  );
139
141
  };
140
142
 
141
- /** @typedef {(pathData: PathData, assetInfo?: AssetInfo) => string} TemplatePathFn */
142
- /** @typedef {string | TemplatePathFn} TemplatePath */
143
+ /**
144
+ * Callback used to compute a path from contextual data. The type parameter
145
+ * narrows the `pathData` shape when the caller knows it operates in a chunk
146
+ * (`PathDataChunk`) or module (`PathDataModule`) context — defaults to the
147
+ * fully-optional `PathData` for backward compatibility.
148
+ * @template {PathData} [T=PathData]
149
+ * @typedef {(pathData: T, assetInfo?: AssetInfo) => string} TemplatePathFn
150
+ */
151
+
152
+ /**
153
+ * Either a raw template string (e.g. `"[name].[contenthash].js"`) or a
154
+ * generic `TemplatePathFn`. Method signatures that need to thread a narrowed
155
+ * `PathData` shape spell the function side out as `TemplatePathFn<T>`
156
+ * directly — `TemplatePath` itself stays a plain alias so local JSDoc
157
+ * re-imports keep a single shared identity.
158
+ * @typedef {string | TemplatePathFn} TemplatePath
159
+ */
143
160
 
144
161
  /**
145
162
  * Returns the interpolated path.
146
- * @param {TemplatePath} path the raw path
147
- * @param {PathData} data context data
163
+ * @template {PathData} [T=PathData]
164
+ * @param {string | TemplatePathFn<T>} path the raw path
165
+ * @param {T} data context data
148
166
  * @param {AssetInfo=} assetInfo extra info about the asset (will be written to)
149
167
  * @returns {string} the interpolated path
150
168
  */
@@ -9,11 +9,11 @@ const { RawSource } = require("webpack-sources");
9
9
  const ConcatenationScope = require("../ConcatenationScope");
10
10
  const Generator = require("../Generator");
11
11
  const {
12
+ ASSET_URL_TYPE,
13
+ ASSET_URL_TYPES,
12
14
  CSS_TYPE,
13
- CSS_URL_TYPE,
14
- CSS_URL_TYPES,
15
15
  HTML_TYPE,
16
- JAVASCRIPT_AND_CSS_URL_TYPES,
16
+ JAVASCRIPT_AND_ASSET_URL_TYPES,
17
17
  JAVASCRIPT_TYPE,
18
18
  JAVASCRIPT_TYPES,
19
19
  NO_TYPES
@@ -80,7 +80,7 @@ class AssetSourceGenerator extends Generator {
80
80
  }
81
81
  return new RawSource(sourceContent);
82
82
  }
83
- case CSS_URL_TYPE: {
83
+ case ASSET_URL_TYPE: {
84
84
  if (!originalSource) {
85
85
  return null;
86
86
  }
@@ -151,9 +151,9 @@ class AssetSourceGenerator extends Generator {
151
151
  sourceTypes.has(JAVASCRIPT_TYPE) &&
152
152
  (sourceTypes.has(CSS_TYPE) || sourceTypes.has(HTML_TYPE))
153
153
  ) {
154
- return JAVASCRIPT_AND_CSS_URL_TYPES;
154
+ return JAVASCRIPT_AND_ASSET_URL_TYPES;
155
155
  } else if (sourceTypes.has(CSS_TYPE) || sourceTypes.has(HTML_TYPE)) {
156
- return CSS_URL_TYPES;
156
+ return ASSET_URL_TYPES;
157
157
  }
158
158
  return JAVASCRIPT_TYPES;
159
159
  }