@depup/webpack 5.110.1-depup.0 → 5.110.3-depup.0

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 (70) hide show
  1. package/README.md +4 -4
  2. package/changes.json +3 -3
  3. package/lib/ChunkGraph.js +20 -18
  4. package/lib/Compilation.js +1 -1
  5. package/lib/ConcatenationScope.js +25 -35
  6. package/lib/DotenvPlugin.js +41 -13
  7. package/lib/ExportsInfo.js +10 -0
  8. package/lib/FlagDependencyUsagePlugin.js +8 -2
  9. package/lib/NormalModule.js +13 -20
  10. package/lib/RuntimePlugin.js +3 -0
  11. package/lib/RuntimeTemplate.js +437 -432
  12. package/lib/async-modules/AsyncModuleHelpers.js +10 -4
  13. package/lib/config/defaults.js +26 -16
  14. package/lib/config/normalization.js +18 -8
  15. package/lib/css/CssLoadingRuntimeModule.js +32 -13
  16. package/lib/css/CssModulesPlugin.js +9 -17
  17. package/lib/css/data.js +13 -3
  18. package/lib/css/syntax.js +78 -10
  19. package/lib/dependencies/CommonJsRequireDependency.js +18 -4
  20. package/lib/dependencies/ContextDependencyTemplateAsRequireCall.js +20 -7
  21. package/lib/dependencies/HarmonyEvaluatedImportSpecifierDependency.js +10 -4
  22. package/lib/dependencies/HarmonyExportDependencyParserPlugin.js +2 -0
  23. package/lib/dependencies/HarmonyExportImportedSpecifierDependency.js +143 -17
  24. package/lib/dependencies/HarmonyImportDependency.js +94 -41
  25. package/lib/dependencies/HarmonyImportDependencyParserPlugin.js +93 -14
  26. package/lib/dependencies/HarmonyImportSideEffectDependency.js +114 -0
  27. package/lib/dependencies/HtmlEntryDependency.js +12 -2
  28. package/lib/dependencies/ImportContextDependency.js +43 -3
  29. package/lib/dependencies/ImportDependency.js +8 -29
  30. package/lib/dependencies/ImportParserPlugin.js +24 -14
  31. package/lib/dependencies/RequireHeaderDependency.js +20 -21
  32. package/lib/dependencies/WorkerAndWorkletPlugin.js +1 -0
  33. package/lib/dependencies/WorkerDependency.js +6 -12
  34. package/lib/dependencies/WorkletDependency.js +7 -11
  35. package/lib/dependencies/importOptionsCheck.js +47 -0
  36. package/lib/errors/DotenvFileError.js +29 -0
  37. package/lib/errors/ImportedBindingAssignmentError.js +30 -0
  38. package/lib/esm/ModuleChunkLoadingPlugin.js +6 -9
  39. package/lib/esm/ModuleChunkLoadingRuntimeModule.js +7 -1
  40. package/lib/html/HtmlModulesPlugin.js +84 -38
  41. package/lib/html/syntax.js +417 -169
  42. package/lib/javascript/JavascriptModulesPlugin.js +5 -10
  43. package/lib/loaders/LoaderRunner.js +33 -5
  44. package/lib/node/ReadFileCompileAsyncWasmPlugin.js +8 -15
  45. package/lib/optimize/ConcatenatedModule.js +46 -7
  46. package/lib/optimize/ConstExportsPlugin.js +5 -0
  47. package/lib/optimize/InlineExports.js +121 -0
  48. package/lib/optimize/RealContentHashPlugin.js +111 -35
  49. package/lib/prefetch/ResourceHintPlugin.js +12 -13
  50. package/lib/runtime/AsyncModuleRuntimeModule.js +5 -1
  51. package/lib/runtime/MakeDeferredNamespaceObjectRuntime.js +11 -1
  52. package/lib/schemes/HttpUriPlugin.js +22 -5
  53. package/lib/serialization/FileMiddleware.js +15 -2
  54. package/lib/typescript/TypeScriptPlugin.js +8 -10
  55. package/lib/util/ArrayQueue.js +3 -3
  56. package/lib/util/SourceProcessor.js +24 -8
  57. package/lib/util/URLAbsoluteSpecifier.js +3 -1
  58. package/lib/util/conventions.js +5 -1
  59. package/lib/util/createMappings.js +25 -3
  60. package/lib/util/internalSerializables.js +2 -0
  61. package/lib/util/removeBOM.js +82 -1
  62. package/lib/wasm-async/AsyncWasmCompileRuntimeModule.js +5 -3
  63. package/lib/wasm-async/AsyncWasmLoadingRuntimeModule.js +5 -3
  64. package/lib/wasm-async/UniversalCompileAsyncWasmPlugin.js +7 -12
  65. package/lib/web/FetchCompileAsyncWasmPlugin.js +10 -6
  66. package/lib/web/FetchCompileWasmPlugin.js +4 -2
  67. package/package.json +7 -7
  68. package/schemas/WebpackOptions.check.js +1 -1
  69. package/schemas/WebpackOptions.json +9 -10
  70. package/types.d.ts +95 -27
@@ -50,7 +50,7 @@ const {
50
50
  const createHash = require("../util/createHash");
51
51
  const createHooksRegistry = require("../util/createHooksRegistry");
52
52
  const { digestNonNumericOnlyWithFull } = require("../util/nonNumericOnlyHash");
53
- const removeBOM = require("../util/removeBOM");
53
+ const { removeBOMFromResult } = require("../util/removeBOM");
54
54
  const { intersectRuntime } = require("../util/runtime");
55
55
  const { getAllChunks } = require("./ChunkHelpers");
56
56
  const JavascriptGenerator = require("./JavascriptGenerator");
@@ -528,15 +528,8 @@ class JavascriptModulesPlugin {
528
528
 
529
529
  NormalModule.getCompilationHooks(compilation).processResult.tap(
530
530
  PLUGIN_NAME,
531
- (result, module) => {
532
- if (module.type === type) {
533
- const [source, ...rest] = result;
534
-
535
- return [removeBOM(source), ...rest];
536
- }
537
-
538
- return result;
539
- }
531
+ (result, module) =>
532
+ module.type === type ? removeBOMFromResult(result) : result
540
533
  );
541
534
  }
542
535
 
@@ -1687,6 +1680,8 @@ class JavascriptModulesPlugin {
1687
1680
  continue;
1688
1681
  }
1689
1682
  }
1683
+ // An async entry is not awaited here: that needs top-level `await`,
1684
+ // gated on an option held back from the patch release (revert #21881).
1690
1685
  let i = jsEntries.length;
1691
1686
  for (const [entryModule, entrypoint] of jsEntries) {
1692
1687
  const chunks =
@@ -7,6 +7,7 @@
7
7
 
8
8
  const { readFile } = require("fs");
9
9
  const { parseResource } = require("../util/identifier");
10
+ const { adjustSourceMapForRemovedBOM } = require("../util/removeBOM");
10
11
  const loadLoader = require("./loadLoader");
11
12
 
12
13
  // Set on a loader context to have each loader's own run measured. Absent
@@ -59,6 +60,8 @@ const HASH_ESCAPE_REGEXP = /#/g;
59
60
  const UTF8_BOM_0 = 0xef;
60
61
  const UTF8_BOM_1 = 0xbb;
61
62
  const UTF8_BOM_2 = 0xbf;
63
+ // U+FEFF, the BOM once decoded
64
+ const BOM_CHAR_CODE = 0xfeff;
62
65
 
63
66
  /**
64
67
  * @param {Buffer} buf buffer
@@ -76,6 +79,14 @@ function utf8BufferToString(buf) {
76
79
  return buf.toString("utf8");
77
80
  }
78
81
 
82
+ /**
83
+ * @param {string} str string
84
+ * @returns {string} string, with a leading BOM removed
85
+ */
86
+ function stringWithoutBOM(str) {
87
+ return str.charCodeAt(0) === BOM_CHAR_CODE ? str.slice(1) : str;
88
+ }
89
+
79
90
  /**
80
91
  * Escape `#` with a preceding `\0` byte; short-circuits when there is no `#`.
81
92
  * @param {string} str input string
@@ -292,9 +303,19 @@ function runSyncOrAsync(fn, context, args, callback) {
292
303
  /**
293
304
  * @param {EXPECTED_ANY[]} args arguments
294
305
  * @param {boolean | null=} raw whether the loader wants a Buffer
306
+ * @param {boolean=} fromLoader whether args came from a loader rather than the resource
295
307
  * @returns {void}
296
308
  */
297
- function convertArgs(args, raw) {
309
+ function convertArgs(args, raw, fromLoader) {
310
+ // A BOM only means anything at the start of a file, so one a loader produced
311
+ // is content the next loader must not see. The resource keeps its own.
312
+ if (fromLoader && typeof args[0] === "string") {
313
+ const withoutBOM = stringWithoutBOM(args[0]);
314
+ if (withoutBOM !== args[0]) {
315
+ args[0] = withoutBOM;
316
+ if (args[1]) args[1] = adjustSourceMapForRemovedBOM(args[1]);
317
+ }
318
+ }
298
319
  if (!raw && Buffer.isBuffer(args[0])) {
299
320
  args[0] = utf8BufferToString(args[0]);
300
321
  } else if (raw && typeof args[0] === "string") {
@@ -307,9 +328,16 @@ function convertArgs(args, raw) {
307
328
  * @param {LoaderContext} loaderContext the loader context
308
329
  * @param {EXPECTED_ANY[]} args arguments
309
330
  * @param {(err: Error | null, args?: EXPECTED_ANY[]) => void} callback callback
331
+ * @param {boolean=} fromLoader whether args came from a loader rather than the resource
310
332
  * @returns {void}
311
333
  */
312
- function iterateNormalLoaders(options, loaderContext, args, callback) {
334
+ function iterateNormalLoaders(
335
+ options,
336
+ loaderContext,
337
+ args,
338
+ callback,
339
+ fromLoader
340
+ ) {
313
341
  while (loaderContext.loaderIndex >= 0) {
314
342
  const currentLoaderObject =
315
343
  loaderContext.loaders[loaderContext.loaderIndex];
@@ -327,11 +355,11 @@ function iterateNormalLoaders(options, loaderContext, args, callback) {
327
355
  continue;
328
356
  }
329
357
 
330
- convertArgs(args, currentLoaderObject.raw);
358
+ convertArgs(args, currentLoaderObject.raw, fromLoader);
331
359
 
332
360
  return runSyncOrAsync(fn, loaderContext, args, (err, ...nextArgs) => {
333
361
  if (err) return callback(err);
334
- iterateNormalLoaders(options, loaderContext, nextArgs, callback);
362
+ iterateNormalLoaders(options, loaderContext, nextArgs, callback, true);
335
363
  });
336
364
  }
337
365
 
@@ -410,7 +438,7 @@ function iteratePitchingLoaders(options, loaderContext, callback) {
410
438
  }
411
439
  if (hasArg) {
412
440
  loaderContext.loaderIndex--;
413
- iterateNormalLoaders(options, loaderContext, args, callback);
441
+ iterateNormalLoaders(options, loaderContext, args, callback, true);
414
442
  } else {
415
443
  iteratePitchingLoaders(options, loaderContext, callback);
416
444
  }
@@ -62,27 +62,20 @@ class ReadFileCompileAsyncWasmPlugin {
62
62
  // settled while the plugin is being applied.
63
63
  /**
64
64
  * @param {string} path rendered path to the wasm file
65
- * @param {RuntimeSpec} runtime the runtime this loader serves
65
+ * @param {boolean} analyzable whether the call site bakes a complete URL
66
66
  * @returns {string} the argument to hand the reader
67
67
  */
68
- const wasmUrlArg = (path, runtime) =>
69
- compilation.runtimeTemplate.supportsAnalyzable(
70
- "wasm",
71
- undefined,
72
- undefined,
73
- runtime
74
- )
75
- ? path
76
- : compilation.runtimeTemplate.importMetaUrl(path);
68
+ const wasmUrlArg = (path, analyzable) =>
69
+ analyzable ? path : compilation.runtimeTemplate.importMetaUrl(path);
77
70
  /**
78
- * @type {(path: string, runtime: RuntimeSpec) => string} callback to generate code to load the wasm file
71
+ * @type {(path: string, runtime: RuntimeSpec, analyzable: boolean) => string} callback to generate code to load the wasm file
79
72
  */
80
73
  const generateLoadBinaryCode = this._import
81
- ? (path, runtime) =>
74
+ ? (path, runtime, analyzable) =>
82
75
  Template.asString([
83
76
  "Promise.all([import('fs'), import('url')]).then(([{ readFile }, { URL }]) => new Promise((resolve, reject) => {",
84
77
  Template.indent([
85
- `readFile(${wasmUrlArg(path, runtime)}, (err, buffer) => {`,
78
+ `readFile(${wasmUrlArg(path, analyzable)}, (err, buffer) => {`,
86
79
  Template.indent([
87
80
  "if (err) return reject(err);",
88
81
  "",
@@ -154,7 +147,7 @@ class ReadFileCompileAsyncWasmPlugin {
154
147
  if (
155
148
  !compilation.runtimeTemplate.supportsAnalyzable(
156
149
  "wasm",
157
- undefined,
150
+ chunkGraph,
158
151
  undefined,
159
152
  chunk.runtime
160
153
  ) &&
@@ -196,7 +189,7 @@ class ReadFileCompileAsyncWasmPlugin {
196
189
  if (
197
190
  !compilation.runtimeTemplate.supportsAnalyzable(
198
191
  "wasm",
199
- undefined,
192
+ chunkGraph,
200
193
  undefined,
201
194
  chunk.runtime
202
195
  ) &&
@@ -181,6 +181,7 @@ const findIdentifiers = (source) => {
181
181
  * @property {string=} comment
182
182
  * @property {ExportName} ids
183
183
  * @property {ExportName} exportName
184
+ * @property {boolean=} accessorCall the raw name is the wrapper accessor call itself, so it binds looser than the identifier it replaces
184
185
  */
185
186
 
186
187
  /**
@@ -494,11 +495,14 @@ const getFinalBinding = (
494
495
  /** @type {NonNullable<ConcatenatedModuleInfo["namespaceObjectName"]>} */
495
496
  (info.namespaceObjectName),
496
497
  ids: /** @type {ExportName} */ (usedName),
497
- exportName
498
+ exportName,
499
+ accessorCall: info.wrapped
498
500
  };
499
501
  }
500
502
 
501
- if (exportName.length === 0) {
503
+ if (moduleExportsAccess) {
504
+ // Skip ESM interop when require() targets a module outside the concatenation
505
+ } else if (exportName.length === 0) {
502
506
  switch (exportsType) {
503
507
  case "default-only":
504
508
  if (deferred) info.deferredNamespaceObjectUsed = true;
@@ -509,7 +513,8 @@ const getFinalBinding = (
509
513
  ? /** @type {string} */ (info.deferredNamespaceObjectName)
510
514
  : getInteropExpr(info, info.interopNamespaceObject2Name),
511
515
  ids: exportName,
512
- exportName
516
+ exportName,
517
+ accessorCall: !deferred && info.wrapped
513
518
  };
514
519
  case "default-with-named":
515
520
  if (deferred) info.deferredNamespaceObjectUsed = true;
@@ -520,7 +525,8 @@ const getFinalBinding = (
520
525
  ? /** @type {string} */ (info.deferredNamespaceObjectName)
521
526
  : getInteropExpr(info, info.interopNamespaceObjectName),
522
527
  ids: exportName,
523
- exportName
528
+ exportName,
529
+ accessorCall: !deferred && info.wrapped
524
530
  };
525
531
  case "namespace":
526
532
  case "dynamic":
@@ -611,7 +617,8 @@ const getFinalBinding = (
611
617
  info,
612
618
  rawName: getExternalModuleExpr(info),
613
619
  ids: exportName,
614
- exportName
620
+ exportName,
621
+ accessorCall: info.wrapped
615
622
  };
616
623
  }
617
624
  info.interopDefaultAccessUsed = true;
@@ -657,7 +664,8 @@ const getFinalBinding = (
657
664
  /** @type {NonNullable<ConcatenatedModuleInfo["namespaceObjectName"]>} */
658
665
  (info.namespaceObjectName),
659
666
  ids: exportName,
660
- exportName
667
+ exportName,
668
+ accessorCall: info.wrapped
661
669
  };
662
670
  case "external":
663
671
  if (deferred) {
@@ -673,7 +681,8 @@ const getFinalBinding = (
673
681
  info,
674
682
  rawName: getExternalModuleExpr(info),
675
683
  ids: exportName,
676
- exportName
684
+ exportName,
685
+ accessorCall: info.wrapped
677
686
  };
678
687
  }
679
688
  }
@@ -938,9 +947,13 @@ const getFinalName = (
938
947
  let reference;
939
948
  /** @type {boolean} */
940
949
  let isPropertyAccess;
950
+ // the raw name is the wrapper accessor call itself, so the reference binds
951
+ // looser than the identifier it stands in for
952
+ let accessorCall = false;
941
953
  if ("rawName" in binding) {
942
954
  reference = `${binding.rawName}${comment || ""}${propertyAccess(ids)}`;
943
955
  isPropertyAccess = ids.length > 0;
956
+ accessorCall = binding.accessorCall === true;
944
957
  } else {
945
958
  const { info, name: exportId } = binding;
946
959
  const name = info.internalNames.get(exportId);
@@ -970,6 +983,10 @@ const getFinalName = (
970
983
  } else if (binding.info.wrapped) {
971
984
  return asiSafe === false ? `;(${reference})` : `(${reference})`;
972
985
  }
986
+ } else if (accessorCall && !asCall) {
987
+ // without parentheses `new X()` would construct the accessor rather
988
+ // than what it returns
989
+ return asiSafe === false ? `;(${reference})` : `(${reference})`;
973
990
  }
974
991
 
975
992
  return reference;
@@ -1415,10 +1432,18 @@ class ConcatenatedModule extends Module {
1415
1432
  runtimeCondition,
1416
1433
  runtime
1417
1434
  );
1435
+ const wasDeferredOnly = !entry.nonDeferAccess;
1418
1436
  entry.nonDeferAccess = mergeNonDeferAccess(
1419
1437
  entry.nonDeferAccess,
1420
1438
  nonDeferAccess
1421
1439
  );
1440
+ // A deferred import does not evaluate, so the first eager reference
1441
+ // fixes the position; insertion order is emit order.
1442
+ if (wasDeferredOnly && nonDeferAccess) {
1443
+ entry.connection = connection;
1444
+ referencesMap.delete(module);
1445
+ referencesMap.set(module, entry);
1446
+ }
1422
1447
  }
1423
1448
  if (targets !== undefined) importConnections.set(module, targets);
1424
1449
  return referencesMap.values();
@@ -1841,6 +1866,13 @@ class ConcatenatedModule extends Module {
1841
1866
  for (const reference of ConcatenationScope.findModuleReferences(
1842
1867
  /** @type {Source} */ (info.internalSource)
1843
1868
  )) {
1869
+ if (reference.leaked) {
1870
+ throw new Error(
1871
+ `Unsubstituted module reference "${reference.name}" in ${info.module.readableIdentifier(
1872
+ requestShortener
1873
+ )}`
1874
+ );
1875
+ }
1844
1876
  if (visitedReferences.has(reference.name)) continue;
1845
1877
  visitedReferences.add(reference.name);
1846
1878
  const match = ConcatenationScope.matchModuleReference(
@@ -1934,6 +1966,13 @@ class ConcatenatedModule extends Module {
1934
1966
  ignoredScopes
1935
1967
  );
1936
1968
  } else {
1969
+ if (ConcatenationScope.isLeakedModuleReference(name)) {
1970
+ throw new Error(
1971
+ `Unsubstituted module reference "${name}" in ${info.module.readableIdentifier(
1972
+ requestShortener
1973
+ )}`
1974
+ );
1975
+ }
1937
1976
  allUsedNames.add(name);
1938
1977
  freeNames.add(name);
1939
1978
  }
@@ -15,6 +15,7 @@ const { VariableInfoFlags } = require("../javascript/JavascriptParser");
15
15
  const {
16
16
  InlinedUsedName,
17
17
  enableInlineExports,
18
+ findModulesInCycles,
18
19
  toInlinedValue
19
20
  } = require("./InlineExports");
20
21
 
@@ -162,11 +163,15 @@ class ConstExportsPlugin {
162
163
 
163
164
  const moduleGraph = compilation.moduleGraph;
164
165
  compilation.hooks.optimizeDependencies.tap(PLUGIN_NAME, (modules) => {
166
+ // A cycle peer may read an export during its temporal dead zone,
167
+ // where the binding must throw and a literal would answer.
168
+ const inCycle = findModulesInCycles(moduleGraph, modules);
165
169
  /** @type {Set<ExportsInfo>} */
166
170
  const visited = new Set();
167
171
  /** @type {ExportsInfo[]} */
168
172
  let queue = [];
169
173
  for (const module of modules) {
174
+ if (inCycle.has(module)) continue;
170
175
  queue.push(moduleGraph.getExportsInfo(module));
171
176
  }
172
177
  while (queue.length > 0) {
@@ -172,10 +172,131 @@ const isExportInlined = (moduleGraph, module, ids, runtime) => {
172
172
  return moduleGraph.getExportsInfo(module).hasInlinedUsedName(ids, runtime);
173
173
  };
174
174
 
175
+ /**
176
+ * Collects, by an iterative Tarjan pass, the modules a cycle peer may read in
177
+ * their dead zone. A cycle of bare side-effect imports still inlines.
178
+ * @param {ModuleGraph} moduleGraph module graph
179
+ * @param {Iterable<Module>} modules all modules of the compilation
180
+ * @returns {Set<Module>} modules whose exports must not be inlined
181
+ */
182
+ const findModulesInCycles = (moduleGraph, modules) => {
183
+ const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency");
184
+ const HarmonyImportSideEffectDependency = require("../dependencies/HarmonyImportSideEffectDependency");
185
+
186
+ /** @type {Set<Module>} */
187
+ const inCycle = new Set();
188
+ /** @type {[Module, Module][]} */
189
+ const bindingReads = [];
190
+ /** @type {Map<Module, number>} */
191
+ const componentOf = new Map();
192
+ let nextComponent = 0;
193
+ /** @type {Map<Module, number>} */
194
+ const index = new Map();
195
+ /** @type {Map<Module, number>} */
196
+ const low = new Map();
197
+ /** @type {Set<Module>} */
198
+ const onStack = new Set();
199
+ /** @type {Module[]} */
200
+ const componentStack = [];
201
+ let counter = 0;
202
+ /**
203
+ * @param {Module} module the module
204
+ * @returns {Module[]} modules it imports, self included when self-importing
205
+ */
206
+ const targetsOf = (module) => {
207
+ const connections = moduleGraph.getOutgoingConnectionsByModule(module);
208
+ if (!connections) return [];
209
+ /** @type {Module[]} */
210
+ const targets = [];
211
+ for (const [target, moduleConnections] of connections) {
212
+ if (
213
+ !target ||
214
+ !moduleConnections.some(
215
+ (c) => c.dependency instanceof HarmonyImportDependency
216
+ )
217
+ ) {
218
+ continue;
219
+ }
220
+ // Anything but a bare side-effect import may observe the binding.
221
+ if (
222
+ moduleConnections.some(
223
+ (c) =>
224
+ c.dependency instanceof HarmonyImportDependency &&
225
+ !(c.dependency instanceof HarmonyImportSideEffectDependency)
226
+ )
227
+ ) {
228
+ bindingReads.push([module, target]);
229
+ }
230
+ targets.push(target);
231
+ }
232
+ return targets;
233
+ };
234
+ /** @type {{ node: Module, targets: Module[], i: number }[]} */
235
+ const work = [];
236
+ /**
237
+ * @param {Module} node node to open
238
+ */
239
+ const open = (node) => {
240
+ index.set(node, counter);
241
+ low.set(node, counter);
242
+ counter++;
243
+ componentStack.push(node);
244
+ onStack.add(node);
245
+ work.push({ node, targets: targetsOf(node), i: 0 });
246
+ };
247
+ for (const seed of modules) {
248
+ if (index.has(seed)) continue;
249
+ open(seed);
250
+ while (work.length > 0) {
251
+ const frame = work[work.length - 1];
252
+ if (frame.i < frame.targets.length) {
253
+ const next = frame.targets[frame.i++];
254
+ if (!index.has(next)) {
255
+ open(next);
256
+ } else if (onStack.has(next)) {
257
+ const nodeLow = /** @type {number} */ (low.get(frame.node));
258
+ const nextIndex = /** @type {number} */ (index.get(next));
259
+ if (nextIndex < nodeLow) low.set(frame.node, nextIndex);
260
+ }
261
+ continue;
262
+ }
263
+ work.pop();
264
+ const node = frame.node;
265
+ const nodeLow = /** @type {number} */ (low.get(node));
266
+ if (work.length > 0) {
267
+ const parent = work[work.length - 1].node;
268
+ if (nodeLow < /** @type {number} */ (low.get(parent))) {
269
+ low.set(parent, nodeLow);
270
+ }
271
+ }
272
+ if (nodeLow === index.get(node)) {
273
+ const id = nextComponent++;
274
+ let member;
275
+ do {
276
+ member = /** @type {Module} */ (componentStack.pop());
277
+ onStack.delete(member);
278
+ componentOf.set(member, id);
279
+ } while (member !== node);
280
+ }
281
+ }
282
+ }
283
+ // Reader and target evaluate together exactly when they share a component.
284
+ for (const [origin, target] of bindingReads) {
285
+ if (
286
+ origin === target ||
287
+ componentOf.get(origin) === componentOf.get(target)
288
+ ) {
289
+ inCycle.add(target);
290
+ }
291
+ }
292
+ return inCycle;
293
+ };
294
+
175
295
  module.exports.InlinedUsedName = InlinedUsedName;
176
296
  module.exports.InlinedValue = InlinedValue;
177
297
  module.exports.MAX_INLINE_BYTES = MAX_INLINE_BYTES;
178
298
  module.exports.enableInlineExports = enableInlineExports;
299
+ module.exports.findModulesInCycles = findModulesInCycles;
179
300
  module.exports.isExportInlined = isExportInlined;
180
301
  module.exports.isInlineEnabled = isInlineEnabled;
181
302
  module.exports.isInlineExportsEnabled = isInlineExportsEnabled;
@@ -345,47 +345,68 @@ ${referencingAssets
345
345
  }
346
346
  return hashes;
347
347
  };
348
+ // Tarjan: hashes naming each other come out as one group where a chain
349
+ // walk would never end, and groups arrive dependencies-first as needed.
350
+ /** @type {string[][]} */
351
+ const groupsInOrder = [];
352
+ /** @type {Map<string, number>} */
353
+ const indexOf = new Map();
354
+ /** @type {Map<string, number>} */
355
+ const lowOf = new Map();
356
+ /** @type {Set<string>} */
357
+ const onStack = new Set();
358
+ /** @type {string[]} */
359
+ const stack = [];
348
360
  /**
349
- * Returns the hash info.
361
+ * Visits one hash and everything its assets reference.
350
362
  * @param {string} hash the hash
351
- * @returns {string} the hash info
363
+ * @returns {void}
352
364
  */
353
- const hashInfo = (hash) => {
354
- const assets = hashToAssets.get(hash);
355
- return `${hash} (${Array.from(
356
- /** @type {AssetInfoForRealContentHash[]} */ (assets),
357
- (a) => a.name
358
- )})`;
359
- };
360
- /** @type {Hashes} */
361
- const hashesInOrder = new Set();
362
- for (const hash of hashToAssets.keys()) {
363
- /**
364
- * Processes the provided hash.
365
- * @param {string} hash the hash
366
- * @param {Set<string>} stack stack of hashes
367
- */
368
- const add = (hash, stack) => {
369
- const deps = getDependencies(hash);
370
- if (!deps) return;
371
- stack.add(hash);
365
+ const strongConnect = (hash) => {
366
+ const index = indexOf.size;
367
+ indexOf.set(hash, index);
368
+ lowOf.set(hash, index);
369
+ stack.push(hash);
370
+ onStack.add(hash);
371
+ // An unowned reference reports the caching problem and stays a leaf.
372
+ const deps = getDependencies(hash);
373
+ if (deps) {
372
374
  for (const dep of deps) {
373
- if (hashesInOrder.has(dep)) continue;
374
- if (stack.has(dep)) {
375
- throw new Error(
376
- `Circular hash dependency ${Array.from(
377
- stack,
378
- hashInfo
379
- ).join(" -> ")} -> ${hashInfo(dep)}`
375
+ if (!indexOf.has(dep)) {
376
+ strongConnect(dep);
377
+ lowOf.set(
378
+ hash,
379
+ Math.min(
380
+ /** @type {number} */ (lowOf.get(hash)),
381
+ /** @type {number} */ (lowOf.get(dep))
382
+ )
383
+ );
384
+ } else if (onStack.has(dep)) {
385
+ lowOf.set(
386
+ hash,
387
+ Math.min(
388
+ /** @type {number} */ (lowOf.get(hash)),
389
+ /** @type {number} */ (indexOf.get(dep))
390
+ )
380
391
  );
381
392
  }
382
- add(dep, stack);
383
393
  }
384
- hashesInOrder.add(hash);
385
- stack.delete(hash);
386
- };
387
- if (hashesInOrder.has(hash)) continue;
388
- add(hash, new Set());
394
+ }
395
+ if (lowOf.get(hash) === indexOf.get(hash)) {
396
+ /** @type {string[]} */
397
+ const group = [];
398
+ /** @type {string} */
399
+ let member;
400
+ do {
401
+ member = /** @type {string} */ (stack.pop());
402
+ onStack.delete(member);
403
+ if (hashToAssets.has(member)) group.push(member);
404
+ } while (member !== hash);
405
+ if (group.length > 0) groupsInOrder.push(group);
406
+ }
407
+ };
408
+ for (const hash of hashToAssets.keys()) {
409
+ if (!indexOf.has(hash)) strongConnect(hash);
389
410
  }
390
411
  /** @type {Map<string, string>} */
391
412
  const hashToNewHash = new Map();
@@ -474,7 +495,62 @@ ${referencingAssets
474
495
  };
475
496
  /** @type {Comparator<AssetInfoForRealContentHash>} */
476
497
  const comparator = compareSelect((a) => a.name, compareStrings);
477
- for (const oldHash of hashesInOrder) {
498
+ for (const group of groupsInOrder) {
499
+ // A cycle is assigned as one group: members are told apart by stable
500
+ // markers, so the group hashes to a fixed point instead of chasing one.
501
+ if (group.length > 1) {
502
+ const members = group.sort();
503
+ /** @type {Map<string, string>} */
504
+ const markerOf = new Map(
505
+ members.map((hash, i) => [hash, `|webpack/scc/${i}|`])
506
+ );
507
+ for (let i = 0; i < members.length; i++) {
508
+ const oldHash = members[i];
509
+ const memberAssets =
510
+ /** @type {AssetInfoForRealContentHash[]} */
511
+ (hashToAssets.get(oldHash));
512
+ memberAssets.sort(comparator);
513
+ const hash = createHash(this._hashFunction);
514
+ if (compilation.outputOptions.hashSalt) {
515
+ hash.update(compilation.outputOptions.hashSalt);
516
+ }
517
+ // Two members can normalize to the same text; the position keeps
518
+ // their hashes apart, and sorting keeps the position stable.
519
+ hash.update(`${i}`);
520
+ for (const asset of memberAssets) {
521
+ if (Buffer.isBuffer(asset.content)) {
522
+ hash.update(asset.content);
523
+ continue;
524
+ }
525
+ const withoutOwn =
526
+ /** @type {Hashes} */
527
+ (asset.ownHashes).has(oldHash);
528
+ hash.update(
529
+ asset.content.replace(hashRegExp, (matched) => {
530
+ // Its own occurrences vanish, exactly as a lone hash's do.
531
+ if (
532
+ withoutOwn &&
533
+ /** @type {Hashes} */ (asset.ownHashes).has(matched)
534
+ ) {
535
+ return "";
536
+ }
537
+ const marker = markerOf.get(matched);
538
+ if (marker !== undefined) return marker;
539
+ const mapped = hashToNewHash.get(matched);
540
+ return mapped === undefined ? matched : mapped;
541
+ })
542
+ );
543
+ }
544
+ const digest = hash.digest(
545
+ /** @type {HashDigest} */ (
546
+ hashToDigest.get(oldHash) || this._hashDigest
547
+ )
548
+ );
549
+ hashToNewHash.set(oldHash, digest.slice(0, oldHash.length));
550
+ }
551
+ continue;
552
+ }
553
+ const oldHash = group[0];
478
554
  const assets =
479
555
  /** @type {AssetInfoForRealContentHash[]} */
480
556
  (hashToAssets.get(oldHash));
@@ -300,7 +300,12 @@ const collectEntrypointHints = (compilation, entryName, hints) => {
300
300
  const out = [];
301
301
  /** @type {Set<string>} */
302
302
  const seenKeys = new Set();
303
- const push = (/** @type {EntrypointHint} */ h) => {
303
+ /**
304
+ * Appends a hint, dropping one already emitted for the same rel + href.
305
+ * @param {EntrypointHint} h hint to append
306
+ * @returns {void}
307
+ */
308
+ const push = (h) => {
304
309
  const key = `${h.rel}\0${h.href}`;
305
310
  if (seenKeys.has(key)) return;
306
311
  seenKeys.add(key);
@@ -481,19 +486,13 @@ const collectStartupAssetHintLines = (
481
486
  seenWorkerChunks.add(workerChunk.id);
482
487
  // The same specifier the `new Worker(...)` call site bakes, asked of the
483
488
  // same module, so the two agree on whether a literal is spellable.
484
- const specifier = runtimeTemplate.supportsAnalyzable(
485
- "url",
489
+ const specifier = runtimeTemplate.getAnalyzableWorkerUrl(
490
+ dep.options.publicPath,
491
+ workerChunk,
492
+ module,
486
493
  chunkGraph,
487
- module
488
- )
489
- ? runtimeTemplate._getAnalyzableChunkSpecifier(
490
- dep.options.publicPath,
491
- workerChunk,
492
- module,
493
- chunkGraph,
494
- set
495
- )
496
- : null;
494
+ set
495
+ );
497
496
  // Without one the call site keeps the runtime form, where it wraps its own
498
497
  // href — firing the `<link>` here too would emit the call twice.
499
498
  if (specifier === null) continue;