webpack 5.108.3 → 5.109.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 (191) hide show
  1. package/lib/APIPlugin.js +48 -29
  2. package/lib/CacheFacade.js +8 -0
  3. package/lib/ChunkGroup.js +1 -0
  4. package/lib/CompatibilityPlugin.js +31 -29
  5. package/lib/Compilation.js +50 -8
  6. package/lib/ConstPlugin.js +11 -11
  7. package/lib/ContextModule.js +239 -6
  8. package/lib/ContextModuleFactory.js +139 -14
  9. package/lib/DefinePlugin.js +396 -101
  10. package/lib/Entrypoint.js +1 -1
  11. package/lib/ExportsInfoApiPlugin.js +2 -2
  12. package/lib/ExternalModule.js +107 -5
  13. package/lib/ExternalModuleFactoryPlugin.js +20 -1
  14. package/lib/FileSystemInfo.js +130 -74
  15. package/lib/FlagDependencyExportsPlugin.js +55 -8
  16. package/lib/HotModuleReplacementPlugin.js +18 -15
  17. package/lib/LazyBarrel.js +30 -3
  18. package/lib/Module.js +3 -0
  19. package/lib/ModuleFilenameHelpers.js +2 -2
  20. package/lib/ModuleTypeConstants.js +8 -1
  21. package/lib/MultiCompiler.js +13 -2
  22. package/lib/MultiWatching.js +1 -0
  23. package/lib/NodeStuffPlugin.js +51 -33
  24. package/lib/NormalModule.js +169 -405
  25. package/lib/NormalModuleFactory.js +76 -2
  26. package/lib/ProgressPlugin.js +211 -119
  27. package/lib/ProvidePlugin.js +2 -2
  28. package/lib/RuntimeGlobals.js +47 -0
  29. package/lib/RuntimePlugin.js +33 -0
  30. package/lib/RuntimeTemplate.js +304 -16
  31. package/lib/UseStrictPlugin.js +1 -1
  32. package/lib/WebpackIsIncludedPlugin.js +1 -1
  33. package/lib/WebpackOptionsApply.js +24 -19
  34. package/lib/asset/AssetGenerator.js +110 -28
  35. package/lib/asset/AssetModulesPlugin.js +92 -2
  36. package/lib/asset/WebManifestGenerator.js +164 -0
  37. package/lib/asset/WebManifestParser.js +130 -0
  38. package/lib/async-modules/AwaitDependenciesInitFragment.js +14 -4
  39. package/lib/async-modules/isGeneratorLowered.js +26 -0
  40. package/lib/cache/PackFileCacheStrategy.js +9 -3
  41. package/lib/config/browserslistTargetHandler.js +40 -0
  42. package/lib/config/defaults.js +222 -32
  43. package/lib/config/normalization.js +26 -0
  44. package/lib/config/target.js +9 -1
  45. package/lib/css/CssGenerator.js +73 -73
  46. package/lib/css/CssLoadingRuntimeModule.js +5 -1
  47. package/lib/css/CssModulesPlugin.js +92 -53
  48. package/lib/css/CssParser.js +843 -302
  49. package/lib/css/syntax.js +1219 -483
  50. package/lib/dependencies/AMDDefineDependencyParserPlugin.js +6 -6
  51. package/lib/dependencies/AMDPlugin.js +2 -2
  52. package/lib/dependencies/AMDRequireDependenciesBlockParserPlugin.js +13 -13
  53. package/lib/dependencies/CommonJsDependencyHelpers.js +39 -0
  54. package/lib/dependencies/CommonJsExportsDependency.js +47 -2
  55. package/lib/dependencies/CommonJsExportsParserPlugin.js +10 -10
  56. package/lib/dependencies/CommonJsImportsParserPlugin.js +18 -18
  57. package/lib/dependencies/CommonJsPlugin.js +2 -2
  58. package/lib/dependencies/CommonJsRequireDependency.js +26 -1
  59. package/lib/dependencies/CommonJsSelfReferenceDependency.js +32 -1
  60. package/lib/dependencies/ContextDependencyHelpers.js +3 -3
  61. package/lib/dependencies/CreateRequireParserPlugin.js +6 -6
  62. package/lib/dependencies/CssIcssExportDependency.js +190 -87
  63. package/lib/dependencies/CssUrlDependency.js +35 -4
  64. package/lib/dependencies/ExternalModuleInitFragmentDependency.js +2 -2
  65. package/lib/dependencies/HarmonyCompatibilityDependency.js +32 -14
  66. package/lib/dependencies/HarmonyDetectionParserPlugin.js +43 -13
  67. package/lib/dependencies/HarmonyExportDependencyParserPlugin.js +7 -19
  68. package/lib/dependencies/HarmonyImportDependency.js +12 -1
  69. package/lib/dependencies/HarmonyImportDependencyParserPlugin.js +34 -107
  70. package/lib/dependencies/HarmonyImportGuard.js +192 -20
  71. package/lib/dependencies/HarmonyModulesPlugin.js +6 -0
  72. package/lib/dependencies/HarmonyTopLevelThisParserPlugin.js +1 -1
  73. package/lib/dependencies/HtmlEntryDependency.js +797 -172
  74. package/lib/dependencies/HtmlSourceDependency.js +26 -0
  75. package/lib/dependencies/ImportDependency.js +43 -1
  76. package/lib/dependencies/ImportMetaContextDependencyParserPlugin.js +31 -18
  77. package/lib/dependencies/ImportMetaContextPlugin.js +38 -7
  78. package/lib/dependencies/ImportMetaGlobDependency.js +81 -0
  79. package/lib/dependencies/ImportMetaGlobDependencyParserPlugin.js +78 -0
  80. package/lib/dependencies/ImportMetaGlobHelpers.js +660 -0
  81. package/lib/dependencies/ImportMetaPlugin.js +532 -180
  82. package/lib/dependencies/ImportMetaResolveDependency.js +90 -0
  83. package/lib/dependencies/ImportParserPlugin.js +87 -26
  84. package/lib/dependencies/ImportPhase.js +10 -8
  85. package/lib/dependencies/LoaderPlugin.js +5 -1
  86. package/lib/dependencies/RequireContextDependencyParserPlugin.js +1 -1
  87. package/lib/dependencies/RequireEnsureDependenciesBlockParserPlugin.js +3 -6
  88. package/lib/dependencies/RequireIncludeDependencyParserPlugin.js +4 -11
  89. package/lib/dependencies/SystemPlugin.js +2 -4
  90. package/lib/dependencies/TopLevelAwaitDependency.js +93 -0
  91. package/lib/dependencies/URLDependency.js +141 -21
  92. package/lib/dependencies/{WorkerPlugin.js → WorkerAndWorkletPlugin.js} +324 -112
  93. package/lib/dependencies/WorkerDependency.js +86 -18
  94. package/lib/dependencies/WorkletDependency.js +278 -0
  95. package/lib/dll/DelegatedModuleFactoryPlugin.js +19 -0
  96. package/lib/errors/EnvironmentNotSupportAsyncWarning.js +1 -1
  97. package/lib/errors/LoaderLoadingError.js +20 -0
  98. package/lib/errors/ModuleParseError.js +39 -22
  99. package/lib/errors/NodeStuffInWebError.js +1 -1
  100. package/lib/esm/ModuleChunkFormatPlugin.js +47 -14
  101. package/lib/esm/ModuleChunkLoadingPlugin.js +8 -1
  102. package/lib/esm/ModuleChunkLoadingRuntimeModule.js +29 -1
  103. package/lib/hmr/lazyCompilationBackend.js +32 -8
  104. package/lib/html/HtmlGenerator.js +940 -91
  105. package/lib/html/HtmlModule.js +3 -2
  106. package/lib/html/HtmlModulesPlugin.js +625 -170
  107. package/lib/html/HtmlParser.js +812 -340
  108. package/lib/html/favicon.svg +1 -0
  109. package/lib/html/syntax.js +2116 -834
  110. package/lib/ids/IdHelpers.js +1 -1
  111. package/lib/index.js +6 -0
  112. package/lib/javascript/ArrayPushCallbackChunkFormatPlugin.js +4 -5
  113. package/lib/javascript/BasicEvaluatedExpression.js +366 -48
  114. package/lib/javascript/JavascriptGenerator.js +197 -49
  115. package/lib/javascript/JavascriptModulesPlugin.js +178 -24
  116. package/lib/javascript/JavascriptParser.js +711 -355
  117. package/lib/javascript/JavascriptParserHelpers.js +3 -6
  118. package/lib/javascript/syntax.js +4380 -0
  119. package/lib/library/ModuleLibraryPlugin.js +38 -1
  120. package/lib/loaders/LoaderRunner.js +714 -0
  121. package/lib/loaders/loadLoader.js +100 -0
  122. package/lib/node/ReadFileCompileAsyncWasmPlugin.js +19 -0
  123. package/lib/node/ReadFileCompileWasmPlugin.js +14 -0
  124. package/lib/optimize/ConcatenatedModule.js +120 -16
  125. package/lib/optimize/FlagIncludedChunksPlugin.js +13 -3
  126. package/lib/optimize/InnerGraphPlugin.js +50 -30
  127. package/lib/optimize/ModuleConcatenationPlugin.js +69 -3
  128. package/lib/optimize/SideEffectsFlagPlugin.js +86 -43
  129. package/lib/prefetch/ChunkPrefetchPreloadPlugin.js +15 -0
  130. package/lib/prefetch/ChunkPreloadTriggerRuntimeModule.js +24 -13
  131. package/lib/prefetch/ResourceHintPlugin.js +663 -0
  132. package/lib/prefetch/ResourceHintRuntimeModule.js +149 -0
  133. package/lib/prefetch/StartupAssetHintRuntimeModule.js +210 -0
  134. package/lib/prefetch/parseResourceHintOptions.js +103 -0
  135. package/lib/rules/RuleSetCompiler.js +114 -0
  136. package/lib/runtime/AsyncModuleGeneratorRuntimeModule.js +56 -0
  137. package/lib/runtime/AsyncModuleRuntimeModule.js +24 -8
  138. package/lib/runtime/AutoPublicPathRuntimeModule.js +7 -1
  139. package/lib/runtime/CommonJsWrapRuntimeModule.js +37 -0
  140. package/lib/runtime/GetWorkletBootstrapRuntimeModule.js +59 -0
  141. package/lib/runtime/MakeDeferredNamespaceObjectRuntime.js +204 -8
  142. package/lib/runtime/SetAnonymousDefaultNameRuntimeModule.js +4 -1
  143. package/lib/schemes/HttpUriPlugin.js +40 -1
  144. package/lib/schemes/VirtualUrlPlugin.js +1 -2
  145. package/lib/serialization/FileMiddleware.js +29 -3
  146. package/lib/stats/DefaultStatsFactoryPlugin.js +32 -3
  147. package/lib/stats/DefaultStatsPresetPlugin.js +2 -0
  148. package/lib/stats/DefaultStatsPrinterPlugin.js +12 -2
  149. package/lib/stats/StatsFactory.js +15 -8
  150. package/lib/url/URLParserPlugin.js +98 -19
  151. package/lib/util/AsyncQueue.js +122 -72
  152. package/lib/util/LocConverter.js +9 -8
  153. package/lib/util/SetHelpers.js +1 -1
  154. package/lib/util/SourceProcessor.js +41 -28
  155. package/lib/util/StackedMap.js +122 -48
  156. package/lib/util/binarySearchBounds.js +2 -2
  157. package/lib/util/concatenate.js +40 -14
  158. package/lib/util/createHooksRegistry.js +13 -5
  159. package/lib/util/dataURL.js +2 -2
  160. package/lib/util/deterministicGrouping.js +7 -5
  161. package/lib/util/formatSize.js +6 -2
  162. package/lib/util/globUtils.js +710 -0
  163. package/lib/util/identifier.js +23 -6
  164. package/lib/util/implicitTypeLoaderFallback.js +62 -0
  165. package/lib/util/internalSerializables.js +88 -71
  166. package/lib/util/magicComment.js +121 -25
  167. package/lib/util/numberHash.js +2 -2
  168. package/lib/wasm-async/AsyncWasmCompileRuntimeModule.js +24 -18
  169. package/lib/wasm-async/AsyncWasmLoadingRuntimeModule.js +33 -22
  170. package/lib/wasm-sync/WasmChunkLoadingRuntimeModule.js +79 -16
  171. package/lib/web/FetchCompileAsyncWasmPlugin.js +19 -0
  172. package/lib/web/FetchCompileWasmPlugin.js +14 -0
  173. package/lib/web/JsonpChunkLoadingRuntimeModule.js +2 -0
  174. package/lib/webpack.js +65 -0
  175. package/lib/webworker/ImportScriptsChunkLoadingRuntimeModule.js +3 -0
  176. package/module.d.ts +21 -0
  177. package/package.json +14 -13
  178. package/schemas/WebpackOptions.check.js +1 -1
  179. package/schemas/WebpackOptions.json +759 -36
  180. package/schemas/plugins/HtmlParserOptions.check.js +1 -1
  181. package/schemas/plugins/ProgressPlugin.check.js +1 -1
  182. package/schemas/plugins/ProgressPlugin.json +17 -1
  183. package/schemas/plugins/container/ContainerReferencePlugin.check.js +1 -1
  184. package/schemas/plugins/container/ContainerReferencePlugin.json +1 -0
  185. package/schemas/plugins/container/ExternalsType.check.js +1 -1
  186. package/schemas/plugins/container/ModuleFederationPlugin.check.js +1 -1
  187. package/schemas/plugins/container/ModuleFederationPlugin.json +1 -0
  188. package/schemas/plugins/css/CssAutoOrModuleParserOptions.check.js +1 -1
  189. package/schemas/plugins/css/CssModuleParserOptions.check.js +1 -1
  190. package/schemas/plugins/css/CssParserOptions.check.js +1 -1
  191. package/types.d.ts +1300 -325
@@ -14,12 +14,147 @@ const {
14
14
  } = require("../ModuleSourceTypeConstants");
15
15
  const RuntimeGlobals = require("../RuntimeGlobals");
16
16
  const Template = require("../Template");
17
+ const WebpackError = require("../errors/WebpackError");
18
+ const { getUndoPath } = require("../util/identifier");
19
+ const memoize = require("../util/memoize");
17
20
  const { PUBLIC_PATH_AUTO } = require("../util/publicPathPlaceholder");
21
+ const { NodeType, SourceProcessor, escapeAttribute } = require("./syntax");
22
+
23
+ // Lazy so loading the HTML generator doesn't pull in the whole CSS pipeline.
24
+ const getCssModulesPlugin = memoize(() => require("../css/CssModulesPlugin"));
25
+ // The CSS tokenizer, used to locate `url(...)` when rebasing inlined stylesheets.
26
+ const getCssSyntax = memoize(() => require("../css/syntax"));
27
+ // node's crypto — real SHA hashes for Content-Security-Policy source lists
28
+ // (the wasm hashes are xxhash, unusable for CSP), loaded only when CSP is on.
29
+ const getCreateNodeHash = memoize(() => require("crypto").createHash);
30
+
31
+ // HTML void elements — serialized without a closing tag when a tag descriptor
32
+ // (`output.html` `injectTags`) doesn't say otherwise.
33
+ const VOID_TAGS = new Set([
34
+ "area",
35
+ "base",
36
+ "br",
37
+ "col",
38
+ "embed",
39
+ "hr",
40
+ "img",
41
+ "input",
42
+ "link",
43
+ "meta",
44
+ "param",
45
+ "source",
46
+ "track",
47
+ "wbr"
48
+ ]);
49
+
50
+ /**
51
+ * Serializes an attribute map: `true` → bare attribute, `false`/`undefined`/
52
+ * `null` → omitted, else a quoted (escaped) value.
53
+ * @param {Record<string, string | boolean | undefined> | undefined} attrs attributes
54
+ * @returns {string} the leading-space-prefixed attribute string
55
+ */
56
+ const serializeAttrs = (attrs) => {
57
+ let out = "";
58
+ if (attrs) {
59
+ for (const name of Object.keys(attrs)) {
60
+ const value = attrs[name];
61
+ if (value === false || value === undefined || value === null) continue;
62
+ out +=
63
+ value === true
64
+ ? ` ${name}`
65
+ : ` ${name}="${escapeAttribute(`${value}`)}"`;
66
+ }
67
+ }
68
+ return out;
69
+ };
70
+
71
+ /**
72
+ * Serializes one tag descriptor. `attrs` values: `true` → bare attribute,
73
+ * `false`/`undefined` → omitted, else a quoted (escaped) value.
74
+ * @param {HtmlTagDescriptor} tag the descriptor
75
+ * @returns {string} the tag's HTML
76
+ */
77
+ const serializeTag = (tag) => {
78
+ const attrs = serializeAttrs(tag.attrs);
79
+ const isVoid =
80
+ tag.voidTag !== undefined
81
+ ? tag.voidTag
82
+ : VOID_TAGS.has(tag.tag.toLowerCase());
83
+ return isVoid
84
+ ? `<${tag.tag}${attrs}>`
85
+ : `<${tag.tag}${attrs}>${tag.children || ""}</${tag.tag}>`;
86
+ };
87
+
88
+ /**
89
+ * Canonical string of an attribute map (present keys, sorted) — used to detect
90
+ * whether an `transformTags` plugin changed a tag, so unchanged tags stay untouched.
91
+ * `false`/`undefined`/`null` count as absent.
92
+ * @param {Record<string, string | boolean | undefined>} attrs attributes
93
+ * @returns {string} a comparable snapshot
94
+ */
95
+ const snapshotAttrs = (attrs) =>
96
+ JSON.stringify(
97
+ Object.keys(attrs)
98
+ .filter((name) => {
99
+ const value = attrs[name];
100
+ return value !== false && value !== undefined && value !== null;
101
+ })
102
+ .sort()
103
+ .map((name) => [name, attrs[name]])
104
+ );
105
+
106
+ /**
107
+ * Whether an attribute renders (present and not `false`/`undefined`/`null`).
108
+ * @param {Record<string, string | boolean | undefined>} attrs attributes
109
+ * @param {string} name attribute name
110
+ * @returns {boolean} true if it renders
111
+ */
112
+ const attrPresent = (attrs, name) => {
113
+ const value = attrs[name];
114
+ return value !== undefined && value !== false && value !== null;
115
+ };
116
+
117
+ /**
118
+ * Whether a `<meta>`'s attributes declare a Content-Security-Policy (case-
119
+ * insensitive `http-equiv`), used to detect an author-declared CSP.
120
+ * @param {Record<string, string | boolean | undefined>} attrs attributes
121
+ * @returns {boolean} true if it is a CSP meta
122
+ */
123
+ const metaIsCsp = (attrs) => {
124
+ for (const name of Object.keys(attrs)) {
125
+ const value = attrs[name];
126
+ if (
127
+ name.toLowerCase() === "http-equiv" &&
128
+ typeof value === "string" &&
129
+ value.trim().toLowerCase() === "content-security-policy"
130
+ ) {
131
+ return true;
132
+ }
133
+ }
134
+ return false;
135
+ };
18
136
 
19
137
  /** @typedef {import("webpack-sources").Source} Source */
20
138
  /** @typedef {import("../../declarations/WebpackOptions").HtmlGeneratorOptions} HtmlGeneratorOptions */
139
+ /** @typedef {import("../../declarations/WebpackOptions").OutputHtmlOptions["csp"]} CspOption */
140
+ /** @typedef {import("./HtmlModulesPlugin").HtmlTagDescriptor} HtmlTagDescriptor */
141
+ /** @typedef {import("./HtmlModulesPlugin").HtmlMutableTag} HtmlMutableTag */
142
+ /** @typedef {"head" | "body"} HtmlTagLocation */
143
+ /**
144
+ * @typedef {object} HtmlTagSpan
145
+ * @property {number} start opening `<` offset
146
+ * @property {number} openEnd offset just after the opening `>`
147
+ * @property {number} end offset just after the whole element
148
+ * @property {HtmlTagLocation} location the `<head>`/`<body>` region it sits in
149
+ * @property {number} nameEnd offset after the tag name (nonce insertion point), `-1` for non-`<script>`/`<style>`
150
+ * @property {number} contentEnd raw-text body end for `<script>`/`<style>`, `-1` otherwise
151
+ * @property {boolean} selfClosing whether the element is self-closing
152
+ */
153
+ /** @typedef {{ headOpen: number, headEnd: number, bodyOpen: number, bodyEnd: number }} HtmlAnchors */
154
+ /** @typedef {{ tags: HtmlMutableTag[], spans: (HtmlTagSpan | null)[], originals: (string | null)[], descriptors: (HtmlTagDescriptor | null)[], anchors: HtmlAnchors }} HtmlModel */
21
155
  /** @typedef {import("../Chunk")} Chunk */
22
156
  /** @typedef {import("../Compilation")} Compilation */
157
+ /** @typedef {boolean | string[] | ((asset: { chunk: Chunk, filename: string }) => string[] | false)} HtmlIntegrity */
23
158
  /** @typedef {import("../Compilation").DependencyConstructor} DependencyConstructor */
24
159
  /** @typedef {import("../CodeGenerationResults")} CodeGenerationResults */
25
160
  /** @typedef {import("../Dependency")} Dependency */
@@ -31,6 +166,7 @@ const { PUBLIC_PATH_AUTO } = require("../util/publicPathPlaceholder");
31
166
  /** @typedef {import("../Module").ConcatenationBailoutReasonContext} ConcatenationBailoutReasonContext */
32
167
  /** @typedef {import("../ModuleGraph")} ModuleGraph */
33
168
  /** @typedef {import("../NormalModule")} NormalModule */
169
+ /** @typedef {import("./HtmlModule").HtmlModuleBuildInfo} HtmlModuleBuildInfo */
34
170
  /** @typedef {import("../Module").RuntimeRequirements} RuntimeRequirements */
35
171
  /** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */
36
172
  /** @typedef {import("../util/Hash")} Hash */
@@ -47,12 +183,117 @@ const JAVASCRIPT_AND_HTML_TYPES = new Set([JAVASCRIPT_TYPE, HTML_TYPE]);
47
183
  /** @type {WeakMap<Compilation, Map<string, Chunk>>} */
48
184
  const chunksByIdCache = new WeakMap();
49
185
 
186
+ /**
187
+ * `String(chunk.id)` → chunk lookup, memoized per compilation and shared by the
188
+ * URL and integrity sentinel resolvers.
189
+ * @param {Compilation} compilation compilation
190
+ * @returns {Map<string, Chunk>} chunks keyed by stringified id
191
+ */
192
+ const getChunksById = (compilation) => {
193
+ let chunksById = chunksByIdCache.get(compilation);
194
+ if (chunksById === undefined) {
195
+ chunksById = new Map();
196
+ for (const chunk of compilation.chunks) {
197
+ chunksById.set(String(chunk.id), chunk);
198
+ }
199
+ chunksByIdCache.set(compilation, chunksById);
200
+ }
201
+ return chunksById;
202
+ };
203
+
50
204
  // Hoisted so `resolveChunkUrlSentinels` (per chunk × module) doesn't allocate a
51
205
  // fresh `RegExp` each call. `String#replace` resets `lastIndex`, so sharing the
52
206
  // global-flag instance is safe under these synchronous, non-reentrant calls.
53
207
  const CHUNK_URL_SENTINEL_REGEXP =
54
208
  /__WEBPACK_HTML_CHUNK_URL__([0-9a-f]+)__([a-z]+)__END__/g;
55
209
 
210
+ // Sentinel for a linked HTML page (a tag mapped to the `html` source type);
211
+ // resolved to the page's emitted filename once it has been built. Keyed by the
212
+ // linked module's id so it shares the `<iframe srcdoc>` nested-module mechanism.
213
+ const HTML_PAGE_URL_SENTINEL_REGEXP =
214
+ /__WEBPACK_HTML_PAGE_URL__([0-9a-f]+)__END__/g;
215
+
216
+ // Sentinel for a URL-referenced asset (font/image/…) emitted into a resource-hint
217
+ // `<link>` tag by `HtmlEntryDependency`; resolved to the asset's real emitted
218
+ // URL from its codegen data during `HtmlModulesPlugin` render. Deferred because
219
+ // HtmlEntry deps run *while* the HTML module is being codegen'd — the asset
220
+ // module's own codegen might not have completed yet. Keyed by the asset
221
+ // module's identifier, hex-encoded so any character in it is safe in a URL.
222
+ const ASSET_URL_SENTINEL_REGEXP =
223
+ /__WEBPACK_HTML_ASSET_URL__([0-9a-f]+)__END__/g;
224
+
225
+ // Matches the whole ` integrity="<sentinel>"` so the attribute can be dropped
226
+ // entirely when the per-asset `integrity` function returns `false`.
227
+ const INTEGRITY_SENTINEL_REGEXP =
228
+ / integrity="__WEBPACK_HTML_INTEGRITY__([0-9a-f]+)__([a-z]+)__END__"/g;
229
+
230
+ // Same attribute, but tolerant of the `\"` escaping the surrounding quotes get
231
+ // when the HTML lives inside a JS string literal — used only to strip it there.
232
+ const INTEGRITY_SENTINEL_STRIP_REGEXP =
233
+ / integrity=\\?"__WEBPACK_HTML_INTEGRITY__[0-9a-f]+__[a-z]+__END__\\?"/g;
234
+
235
+ // The trailing `(?:__<hash>)?` is the inlined chunk's content hash, embedded by
236
+ // `embedInlineChunkHashes` so the host HTML's bytes (and its `[contenthash]`,
237
+ // incl. RealContentHashPlugin's recompute) change with the inlined content; the
238
+ // resolver ignores it.
239
+ const INLINE_SENTINEL_REGEXP =
240
+ /__WEBPACK_HTML_INLINE__([0-9a-f]+)__([a-z]+)(?:__[0-9a-f]+)?__END__/g;
241
+
242
+ /**
243
+ * Rewrite relative `url(...)` references in a CSS chunk so they still resolve
244
+ * after the styles are inlined into the HTML document (which may sit in a
245
+ * different directory). webpack emits every url as `undoPath(cssFile) + <asset
246
+ * path from the output root>`, so swapping the CSS file's undo prefix for the
247
+ * HTML file's re-bases them — no path math. Uses the CSS tokenizer to find url
248
+ * tokens exactly (never a `url(` inside a comment or string); webpack emits
249
+ * unquoted url()s (special characters `\`-escaped), so only that shape occurs.
250
+ * @param {string} css the CSS content
251
+ * @param {string} fromFile the CSS file's output name
252
+ * @param {string} toFile the HTML file's output name
253
+ * @param {string} outputPath the compilation output path
254
+ * @returns {string} the CSS with rebased urls
255
+ */
256
+ const rebaseCssUrls = (css, fromFile, toFile, outputPath) => {
257
+ const fromUndo = getUndoPath(fromFile, outputPath, false);
258
+ const toUndo = getUndoPath(toFile, outputPath, false);
259
+ if (fromUndo === toUndo) return css;
260
+ const { readToken, TT_URL } = getCssSyntax();
261
+ const token = {
262
+ type: 0,
263
+ start: 0,
264
+ end: 0,
265
+ isId: false,
266
+ contentStart: 0,
267
+ contentEnd: 0,
268
+ unitStart: 0
269
+ };
270
+ let pos = 0;
271
+ let last = 0;
272
+ let out = "";
273
+ while (readToken(css, pos, token) !== undefined) {
274
+ pos = token.end;
275
+ if (token.type !== TT_URL) continue;
276
+ const url = css.slice(token.contentStart, token.contentEnd);
277
+ // Rebase only the undo-path-relative urls; a root-relative (`/`), fragment
278
+ // (`#`) or scheme (`data:`, …, a `:` before any `/`) url is absolute.
279
+ const colon = url.indexOf(":");
280
+ const slash = url.indexOf("/");
281
+ if (
282
+ !url.startsWith(fromUndo) ||
283
+ url[0] === "/" ||
284
+ url[0] === "#" ||
285
+ (colon !== -1 && (slash === -1 || colon < slash))
286
+ ) {
287
+ continue;
288
+ }
289
+ out += `${css.slice(last, token.contentStart)}${toUndo}${url.slice(
290
+ fromUndo.length
291
+ )}`;
292
+ last = token.contentEnd;
293
+ }
294
+ return last === 0 ? css : out + css.slice(last);
295
+ };
296
+
56
297
  class HtmlGenerator extends Generator {
57
298
  /**
58
299
  * Emit a sentinel for a chunk URL that can't be resolved at code-gen time
@@ -78,34 +319,29 @@ class HtmlGenerator extends Generator {
78
319
  static resolveChunkUrlSentinels(content, compilation) {
79
320
  if (!content.includes("__WEBPACK_HTML_CHUNK_URL__")) return content;
80
321
  const outputOptions = compilation.outputOptions;
81
- let chunksById = chunksByIdCache.get(compilation);
82
- if (chunksById === undefined) {
83
- chunksById = new Map();
84
- for (const chunk of compilation.chunks) {
85
- chunksById.set(String(chunk.id), chunk);
86
- }
87
- chunksByIdCache.set(compilation, chunksById);
88
- }
322
+ const chunksById = getChunksById(compilation);
89
323
  return content.replace(
90
324
  CHUNK_URL_SENTINEL_REGEXP,
91
325
  (_, hexId, contentHashType) => {
92
326
  const chunkId = Buffer.from(hexId, "hex").toString("utf8");
93
327
  const chunk = chunksById.get(chunkId);
94
328
  if (!chunk) return "data:,";
329
+ // Reuse each pipeline's own filename-template resolver so the URL
330
+ // matches exactly what that plugin emits (worker/hot-update chunk
331
+ // handling included), mirroring the CSS branch.
95
332
  let filenameTemplate;
96
333
  if (contentHashType === "css") {
97
- const CssModulesPlugin = require("../css/CssModulesPlugin");
98
-
99
- filenameTemplate = CssModulesPlugin.getChunkFilenameTemplate(
334
+ filenameTemplate = getCssModulesPlugin().getChunkFilenameTemplate(
100
335
  chunk,
101
336
  outputOptions
102
337
  );
103
338
  } else {
104
- filenameTemplate =
105
- chunk.filenameTemplate ||
106
- (chunk.canBeInitial()
107
- ? outputOptions.filename
108
- : outputOptions.chunkFilename);
339
+ const JavascriptModulesPlugin = require("../javascript/JavascriptModulesPlugin");
340
+
341
+ filenameTemplate = JavascriptModulesPlugin.getChunkFilenameTemplate(
342
+ chunk,
343
+ outputOptions
344
+ );
109
345
  }
110
346
  const filename = compilation.getPath(
111
347
  /** @type {import("../TemplatedPathPlugin").TemplatePath} */
@@ -120,6 +356,642 @@ class HtmlGenerator extends Generator {
120
356
  );
121
357
  }
122
358
 
359
+ /**
360
+ * Emit a sentinel for a linked HTML page's URL (a tag the user mapped to
361
+ * the `html` source `type`). The page's emitted filename isn't known at
362
+ * code-gen time; `resolveHtmlPageUrlSentinels` swaps it for
363
+ * `${PUBLIC_PATH_AUTO}<htmlFilename>` once the page has been built.
364
+ * @param {string | number} moduleId id of the linked HTML module
365
+ * @returns {string} sentinel
366
+ */
367
+ static makeHtmlPageUrlSentinel(moduleId) {
368
+ const hexId = Buffer.from(String(moduleId), "utf8").toString("hex");
369
+ return `__WEBPACK_HTML_PAGE_URL__${hexId}__END__`;
370
+ }
371
+
372
+ /**
373
+ * Replace every `makeHtmlPageUrlSentinel` sentinel in `content` with
374
+ * `${PUBLIC_PATH_AUTO}<htmlFilename>`, resolving each linked page's emitted
375
+ * filename through `getFilename`.
376
+ * @param {string} content content
377
+ * @param {(moduleId: string) => string} getFilename maps a linked module id to its emitted html filename
378
+ * @returns {string} resolved content
379
+ */
380
+ static resolveHtmlPageUrlSentinels(content, getFilename) {
381
+ if (!content.includes("__WEBPACK_HTML_PAGE_URL__")) return content;
382
+ return content.replace(
383
+ HTML_PAGE_URL_SENTINEL_REGEXP,
384
+ (_, hexId) =>
385
+ `${PUBLIC_PATH_AUTO}${getFilename(
386
+ Buffer.from(hexId, "hex").toString("utf8")
387
+ )}`
388
+ );
389
+ }
390
+
391
+ /**
392
+ * Emit a sentinel for a URL-referenced asset module. Resolved by
393
+ * `resolveAssetUrlSentinels` from the asset module's own codegen data
394
+ * (`data.get("url")`), which isn't populated when `HtmlEntryDependency`
395
+ * runs — the asset module's own codegen may still be pending.
396
+ * @param {string} moduleIdentifier the referenced asset module's `identifier()`
397
+ * @returns {string} sentinel
398
+ */
399
+ static makeAssetUrlSentinel(moduleIdentifier) {
400
+ const hex = Buffer.from(moduleIdentifier, "utf8").toString("hex");
401
+ return `__WEBPACK_HTML_ASSET_URL__${hex}__END__`;
402
+ }
403
+
404
+ /**
405
+ * Replace every `makeAssetUrlSentinel` in `content` with the asset module's
406
+ * emitted URL, read from its codegen data. Unresolved sentinels (module
407
+ * absent, no codegen result yet, or no URL channel — e.g. an inlined
408
+ * data-URI) map to `data:,` so the surrounding `<link>` degrades cleanly
409
+ * instead of shipping the raw placeholder.
410
+ * @param {string} content content
411
+ * @param {Compilation} compilation compilation
412
+ * @returns {string} resolved content
413
+ */
414
+ static resolveAssetUrlSentinels(content, compilation) {
415
+ if (!content.includes("__WEBPACK_HTML_ASSET_URL__")) return content;
416
+ /** @type {Map<string, import("../Module")>} */
417
+ const byIdentifier = new Map();
418
+ for (const m of compilation.modules) byIdentifier.set(m.identifier(), m);
419
+ const codeGenerationResults =
420
+ /** @type {import("../CodeGenerationResults")} */
421
+ (compilation.codeGenerationResults);
422
+ return content.replace(ASSET_URL_SENTINEL_REGEXP, (_, hex) => {
423
+ const id = Buffer.from(hex, "hex").toString("utf8");
424
+ const module = byIdentifier.get(id);
425
+ if (!module) return "data:,";
426
+ if (!codeGenerationResults.has(module, undefined)) return "data:,";
427
+ const codeGen = codeGenerationResults.get(module, undefined);
428
+ const data = codeGen.data;
429
+ if (!data) return "data:,";
430
+ const url =
431
+ /** @type {Record<string, string | undefined> | undefined} */
432
+ (data.get("url"));
433
+ // Prefer the `asset-url` channel — it's the actual URL string
434
+ // (publicPath + filename). Also handled: assets reached only from JS
435
+ // have no `asset-url` channel (their only URL is the JS expression
436
+ // `__webpack_require__.p + "…"`); for those we reconstruct the URL
437
+ // from the emitted `filename` + `PUBLIC_PATH_AUTO` so the same
438
+ // undo-path substitution runs at asset render.
439
+ if (url && url["asset-url"]) return url["asset-url"];
440
+ const filename = /** @type {string | undefined} */ (data.get("filename"));
441
+ if (filename) return `${PUBLIC_PATH_AUTO}${filename}`;
442
+ return "data:,";
443
+ });
444
+ }
445
+
446
+ /**
447
+ * Emit a sentinel for a chunk's SRI hash. Resolved late (after
448
+ * `RealContentHashPlugin`) by `resolveChunkIntegritySentinels`, since the
449
+ * hash must cover the chunk's final emitted bytes.
450
+ * @param {Chunk} chunk chunk
451
+ * @param {"javascript" | "css"} contentHashType which chunk asset to hash
452
+ * @returns {string} sentinel
453
+ */
454
+ static makeChunkIntegritySentinel(chunk, contentHashType) {
455
+ const hexId = Buffer.from(String(chunk.id), "utf8").toString("hex");
456
+ return `__WEBPACK_HTML_INTEGRITY__${hexId}__${contentHashType}__END__`;
457
+ }
458
+
459
+ /**
460
+ * Drop every ` integrity="<sentinel>"` from `content`. Used on the HTML
461
+ * string embedded in a JS chunk (e.g. `<iframe srcdoc>`), where a real SRI
462
+ * hash can't be produced — the chunk's own bytes aren't final at render time
463
+ * — and resolving it later would mutate the chunk after its content hash was
464
+ * computed. Only real HTML output assets keep the sentinel for late resolution.
465
+ * @param {string} content content
466
+ * @returns {string} content with integrity sentinels removed
467
+ */
468
+ static stripChunkIntegritySentinels(content) {
469
+ if (!content.includes("__WEBPACK_HTML_INTEGRITY__")) return content;
470
+ return content.replace(INTEGRITY_SENTINEL_STRIP_REGEXP, "");
471
+ }
472
+
473
+ /**
474
+ * Replace every ` integrity="<sentinel>"` with the chunk's real SRI hashes,
475
+ * or drop the attribute when `integrity` (a function) returns `false` for it.
476
+ * @param {string} content content
477
+ * @param {Compilation} compilation compilation
478
+ * @param {HtmlIntegrity} integrity the `output.html.integrity` option
479
+ * @returns {string} resolved content
480
+ */
481
+ static resolveChunkIntegritySentinels(content, compilation, integrity) {
482
+ if (!content.includes("__WEBPACK_HTML_INTEGRITY__")) return content;
483
+
484
+ const crypto = require("crypto");
485
+
486
+ // Report each bad hash algorithm once, not once per referencing tag.
487
+ const reportedBadAlgorithms = new Set();
488
+
489
+ const chunksById = getChunksById(compilation);
490
+ return content.replace(
491
+ INTEGRITY_SENTINEL_REGEXP,
492
+ (_, hexId, contentHashType) => {
493
+ const chunkId = Buffer.from(hexId, "hex").toString("utf8");
494
+ const chunk = chunksById.get(chunkId);
495
+ if (!chunk) return "";
496
+ // `chunk.files` holds the real emitted names (after
497
+ // `RealContentHashPlugin`), so hash those bytes — not a name
498
+ // re-derived from the template, which still has the pre-hash value.
499
+ let filename;
500
+ for (const file of chunk.files) {
501
+ if ((contentHashType === "css") === file.endsWith(".css")) {
502
+ filename = file;
503
+ break;
504
+ }
505
+ }
506
+ if (!filename) return "";
507
+ const asset = compilation.getAsset(filename);
508
+ if (!asset) return "";
509
+ const algorithms =
510
+ typeof integrity === "function"
511
+ ? integrity({ chunk, filename })
512
+ : integrity === true
513
+ ? ["sha384"]
514
+ : integrity;
515
+ if (!algorithms || algorithms.length === 0) return "";
516
+ const buffer = asset.source.buffer();
517
+ const parts = [];
518
+ for (const algorithm of algorithms) {
519
+ // `crypto.createHash` throws on an unknown algorithm name — turn
520
+ // that into a webpack error and skip the algorithm instead of
521
+ // crashing the whole compilation out of `processAssets`.
522
+ try {
523
+ parts.push(
524
+ `${algorithm}-${crypto
525
+ .createHash(algorithm)
526
+ .update(buffer)
527
+ .digest("base64")}`
528
+ );
529
+ } catch (_err) {
530
+ if (!reportedBadAlgorithms.has(algorithm)) {
531
+ reportedBadAlgorithms.add(algorithm);
532
+ compilation.errors.push(
533
+ new WebpackError(
534
+ `output.html.integrity: unsupported hash algorithm ${JSON.stringify(
535
+ algorithm
536
+ )}`
537
+ )
538
+ );
539
+ }
540
+ }
541
+ }
542
+ if (parts.length === 0) return "";
543
+ return ` integrity="${parts.join(" ")}"`;
544
+ }
545
+ );
546
+ }
547
+
548
+ /**
549
+ * @param {Chunk} chunk chunk
550
+ * @param {"javascript" | "css"} contentHashType which chunk asset to inline
551
+ * @returns {string} sentinel
552
+ */
553
+ static makeChunkInlineSentinel(chunk, contentHashType) {
554
+ const hexId = Buffer.from(String(chunk.id), "utf8").toString("hex");
555
+ return `__WEBPACK_HTML_INLINE__${hexId}__${contentHashType}__END__`;
556
+ }
557
+
558
+ /**
559
+ * Replace every inline sentinel with the chunk's actual source content.
560
+ * Must run after `RealContentHashPlugin` so the final emitted filenames are
561
+ * known (we look up the asset by filename from `chunk.files`).
562
+ * @param {string} content content
563
+ * @param {Compilation} compilation compilation
564
+ * @param {string} htmlFilename output name of the HTML file being resolved (to rebase CSS urls)
565
+ * @param {Set<string>=} inlinedFiles collects every chunk file inlined here, so the caller can drop now-unreferenced assets
566
+ * @returns {string} resolved content
567
+ */
568
+ static resolveChunkInlineSentinels(
569
+ content,
570
+ compilation,
571
+ htmlFilename,
572
+ inlinedFiles
573
+ ) {
574
+ if (!content.includes("__WEBPACK_HTML_INLINE__")) return content;
575
+ const chunksById = getChunksById(compilation);
576
+ const outputPath = /** @type {string} */ (compilation.outputOptions.path);
577
+ return content.replace(
578
+ INLINE_SENTINEL_REGEXP,
579
+ (_, hexId, contentHashType) => {
580
+ const chunkId = Buffer.from(hexId, "hex").toString("utf8");
581
+ const chunk = chunksById.get(chunkId);
582
+ if (!chunk) return "";
583
+ const isCss = contentHashType === "css";
584
+ let out = "";
585
+ for (const file of chunk.files) {
586
+ if (isCss !== file.endsWith(".css")) continue;
587
+ const asset = compilation.getAsset(file);
588
+ if (!asset) continue;
589
+ let raw = String(asset.source.source());
590
+ if (isCss) {
591
+ // Styles move into the document, so relative `url(...)` must be
592
+ // rebased; then escape `</style` so it can't close the block early.
593
+ raw = rebaseCssUrls(raw, file, htmlFilename, outputPath).replace(
594
+ /<\/(style)/gi,
595
+ "<\\/$1"
596
+ );
597
+ } else {
598
+ // Escape `</script` (early close) and `<!--` (enters the script-data
599
+ // escaped state, where a later `<script` hides the real close); both
600
+ // only occur inside a string/comment/regex, where `<\` is inert.
601
+ raw = raw
602
+ .replace(/<\/(script)/gi, "<\\/$1")
603
+ .replace(/<!--/g, "<\\!--");
604
+ }
605
+ out += raw;
606
+ if (inlinedFiles) inlinedFiles.add(file);
607
+ }
608
+ return out;
609
+ }
610
+ );
611
+ }
612
+
613
+ /**
614
+ * Parses the final HTML once and collects everything the `output.html`
615
+ * post-processing stages need: every `<script>`/`<link>`/`<style>`/`<meta>` as
616
+ * a mutable descriptor (for `transformTags`) paired with its source span, an
617
+ * attribute snapshot (to detect changes), and — for `<script>`/`<style>` — the
618
+ * raw-text body range and nonce insertion point (for CSP); plus the
619
+ * `<head>`/`<body>` open/content-end anchors (for `injectTags`). One parse feeds
620
+ * `injectTags` + `transformTags` + CSP, which `renderHtml` then applies in a
621
+ * single pass. Uses the pipeline's own parser (no regex).
622
+ * @param {string} content the final HTML (inline content already resolved)
623
+ * @returns {HtmlModel} the collected model
624
+ */
625
+ static collectHtml(content) {
626
+ /** @type {HtmlMutableTag[]} */
627
+ const tags = [];
628
+ /** @type {(HtmlTagSpan | null)[]} */
629
+ const spans = [];
630
+ /** @type {(string | null)[]} */
631
+ const originals = [];
632
+ /** @type {(HtmlTagDescriptor | null)[]} */
633
+ const descriptors = [];
634
+ let headOpen = -1;
635
+ let headEnd = -1;
636
+ let bodyOpen = -1;
637
+ let bodyEnd = -1;
638
+ let headDepth = 0;
639
+ let bodyDepth = 0;
640
+ // `skip.text` populates `contentEnd` (raw-text body end) without
641
+ // materializing text nodes; `</script` stays escaped, so CSP hashes match
642
+ // the exact bytes the browser sees.
643
+ new SourceProcessor()
644
+ .use({
645
+ [NodeType.Element]: {
646
+ enter: (path) => {
647
+ const name = path.tagName();
648
+ if (name === "head") {
649
+ if (headOpen < 0) headOpen = headEnd = path.tagEnd();
650
+ headDepth++;
651
+ return;
652
+ }
653
+ if (name === "body") {
654
+ if (bodyOpen < 0) bodyOpen = bodyEnd = path.tagEnd();
655
+ bodyDepth++;
656
+ return;
657
+ }
658
+ const end = path.end();
659
+ if (headDepth > 0 && end > headEnd) headEnd = end;
660
+ if (bodyDepth > 0 && end > bodyEnd) bodyEnd = end;
661
+ if (
662
+ name !== "script" &&
663
+ name !== "link" &&
664
+ name !== "style" &&
665
+ name !== "meta"
666
+ ) {
667
+ return;
668
+ }
669
+ /** @type {Record<string, string | boolean | undefined>} */
670
+ const attrs = {};
671
+ const count = path.attributeCount();
672
+ for (let i = 0; i < count; i++) {
673
+ const attr = path.attributeAt(i);
674
+ attrs[path.attributeName(attr)] =
675
+ path.attributeValueStart(attr) === -1
676
+ ? true
677
+ : path.attributeValue(attr);
678
+ }
679
+ const isRawText = name === "script" || name === "style";
680
+ tags.push({
681
+ tag: name,
682
+ attrs,
683
+ injectTo: bodyDepth > 0 ? "body" : "head"
684
+ });
685
+ spans.push({
686
+ start: path.start(),
687
+ openEnd: path.tagEnd(),
688
+ end,
689
+ location: bodyDepth > 0 ? "body" : "head",
690
+ nameEnd: isRawText ? path.nameEnd() : -1,
691
+ contentEnd: isRawText ? path.contentEnd() : -1,
692
+ selfClosing: path.selfClosing()
693
+ });
694
+ originals.push(snapshotAttrs(attrs));
695
+ descriptors.push(null);
696
+ },
697
+ exit: (path) => {
698
+ const name = path.tagName();
699
+ if (name === "head") {
700
+ if (headDepth > 0) headDepth--;
701
+ } else if (name === "body" && bodyDepth > 0) {
702
+ bodyDepth--;
703
+ }
704
+ }
705
+ }
706
+ })
707
+ .process(content, { skip: { text: true, doctype: true } });
708
+ return {
709
+ tags,
710
+ spans,
711
+ originals,
712
+ descriptors,
713
+ anchors: { headOpen, headEnd, bodyOpen, bodyEnd }
714
+ };
715
+ }
716
+
717
+ /**
718
+ * Appends `injectTags` descriptors to the model as spanless tags (pure inserts
719
+ * placed by `injectTo`), so `transformTags` sees them alongside the page's own
720
+ * tags and CSP hashes any injected inline `<script>`/`<style>`. `attrs` is
721
+ * copied so a `transformTags` mutation never corrupts the caller's descriptor.
722
+ * @param {HtmlModel} model the value from `collectHtml`
723
+ * @param {HtmlTagDescriptor[]} injected the `injectTags` descriptors
724
+ * @returns {void}
725
+ */
726
+ static addInjectedTags(model, injected) {
727
+ for (const descriptor of injected) {
728
+ model.tags.push({
729
+ tag: descriptor.tag,
730
+ attrs: descriptor.attrs ? { ...descriptor.attrs } : {},
731
+ injectTo: descriptor.injectTo || "head"
732
+ });
733
+ model.spans.push(null);
734
+ model.originals.push(null);
735
+ model.descriptors.push(descriptor);
736
+ }
737
+ }
738
+
739
+ /**
740
+ * Renders the collected (and hook-mutated) model back into HTML in a single
741
+ * pass: injected tags are placed by `injectTo`; an existing tag is left
742
+ * byte-for-byte unless it was removed, moved (`<head>`↔`<body>`, honoring
743
+ * `*-prepend`), or had its `attrs` changed; and — when `csp` is set and the
744
+ * page declares no CSP — a strict `<meta http-equiv="Content-Security-Policy">`
745
+ * is injected with a hash of every inline `<script>`/`<style>` (page's own and
746
+ * injected) plus an optional placeholder `nonce` on each. Edits run
747
+ * right-to-left; at a shared offset a delete/replace runs before an insert.
748
+ * @param {string} content the collected HTML (inline content already resolved)
749
+ * @param {HtmlModel} model the value from `collectHtml` after the hooks
750
+ * @param {CspOption} csp the `output.html.csp` option
751
+ * @returns {string} the rendered HTML
752
+ */
753
+ static renderHtml(content, model, csp) {
754
+ const { tags, spans, originals, descriptors, anchors } = model;
755
+ // CSP setup: an author-declared policy (page's own or injected) wins.
756
+ const options = csp ? (csp === true ? {} : csp) : undefined;
757
+ const nonce = options && options.nonce;
758
+ let hasCsp = false;
759
+ if (options) {
760
+ for (let i = 0; i < tags.length; i++) {
761
+ const tag = tags[i];
762
+ if (!tag.remove && tag.tag === "meta" && metaIsCsp(tag.attrs)) {
763
+ hasCsp = true;
764
+ break;
765
+ }
766
+ }
767
+ }
768
+ const addCsp = Boolean(options) && !hasCsp;
769
+ /** @type {Set<string>} */
770
+ const scriptSrc = new Set();
771
+ /** @type {Set<string>} */
772
+ const styleSrc = new Set();
773
+ let hashOf;
774
+ if (addCsp) {
775
+ const hashFunction =
776
+ /** @type {Exclude<CspOption, boolean | undefined>} */ (options)
777
+ .hashFunction || "sha256";
778
+ const createHash = getCreateNodeHash();
779
+ hashOf = (/** @type {string} */ body) =>
780
+ `'${hashFunction}-${createHash(hashFunction)
781
+ .update(body, "utf8")
782
+ .digest("base64")}'`;
783
+ }
784
+ // Insertions accumulated per offset (placement order preserved); range edits
785
+ // replace/delete an existing tag's span; region groups collect placements so
786
+ // `*-prepend` precedes the plain name at a shared (empty-region) anchor.
787
+ /** @type {Map<number, string>} */
788
+ const inserts = new Map();
789
+ /** @type {(pos: number, text: string) => void} */
790
+ const at = (pos, text) => {
791
+ if (text) inserts.set(pos, (inserts.get(pos) || "") + text);
792
+ };
793
+ // Placements accumulate per region so `*-prepend` always precedes the plain
794
+ // name at a shared offset (an empty `<head>`/`<body>`); emitted at the anchors
795
+ // after the loop.
796
+ /** @type {Record<string, string>} */
797
+ const groups = {
798
+ "head-prepend": "",
799
+ head: "",
800
+ "body-prepend": "",
801
+ body: ""
802
+ };
803
+ /** @type {(region: string | undefined, text: string) => void} */
804
+ const place = (region, text) => {
805
+ if (text) {
806
+ groups[region !== undefined && region in groups ? region : "head"] +=
807
+ text;
808
+ }
809
+ };
810
+ /** @type {{ start: number, end: number, text: string, insert: boolean }[]} */
811
+ const edits = [];
812
+ for (let i = 0; i < tags.length; i++) {
813
+ const tag = tags[i];
814
+ const span = spans[i];
815
+ const isRawText = tag.tag === "script" || tag.tag === "style";
816
+ const needsNonce =
817
+ addCsp &&
818
+ Boolean(nonce) &&
819
+ isRawText &&
820
+ !attrPresent(tag.attrs, "nonce");
821
+ // Hash an inline `<script>`/`<style>` (page's own or injected). External
822
+ // `<script src>` is covered by `'self'`; a self-closing tag has no body.
823
+ if (addCsp && isRawText && !tag.remove) {
824
+ const isScript = tag.tag === "script";
825
+ let body = "";
826
+ if (span) {
827
+ if (
828
+ !span.selfClosing &&
829
+ !(isScript && attrPresent(tag.attrs, "src"))
830
+ ) {
831
+ body = content.slice(span.openEnd, span.contentEnd);
832
+ }
833
+ } else if (!(isScript && attrPresent(tag.attrs, "src"))) {
834
+ body =
835
+ /** @type {HtmlTagDescriptor} */ (descriptors[i]).children || "";
836
+ }
837
+ if (body !== "") {
838
+ (isScript ? scriptSrc : styleSrc).add(
839
+ /** @type {(body: string) => string} */ (hashOf)(body)
840
+ );
841
+ }
842
+ }
843
+ if (!span) {
844
+ // Injected tag: serialize (with a nonce when CSP asks) and place it.
845
+ if (tag.remove) continue;
846
+ const descriptor = /** @type {HtmlTagDescriptor} */ (descriptors[i]);
847
+ const attrs = needsNonce
848
+ ? { nonce: /** @type {string} */ (nonce), ...tag.attrs }
849
+ : tag.attrs;
850
+ place(
851
+ tag.injectTo || "head",
852
+ serializeTag({
853
+ tag: tag.tag,
854
+ attrs,
855
+ children: descriptor.children,
856
+ voidTag: descriptor.voidTag
857
+ })
858
+ );
859
+ continue;
860
+ }
861
+ // Existing tag.
862
+ if (tag.remove) {
863
+ edits.push({
864
+ start: span.start,
865
+ end: span.end,
866
+ text: "",
867
+ insert: false
868
+ });
869
+ continue;
870
+ }
871
+ const attrsChanged = snapshotAttrs(tag.attrs) !== originals[i];
872
+ const moved =
873
+ tag.injectTo !== undefined && tag.injectTo !== span.location;
874
+ const nonceStr = needsNonce
875
+ ? ` nonce="${escapeAttribute(/** @type {string} */ (nonce))}"`
876
+ : "";
877
+ if (moved) {
878
+ const opening = `<${tag.tag}${nonceStr}${serializeAttrs(tag.attrs)}>`;
879
+ edits.push({
880
+ start: span.start,
881
+ end: span.end,
882
+ text: "",
883
+ insert: false
884
+ });
885
+ place(tag.injectTo, opening + content.slice(span.openEnd, span.end));
886
+ } else if (attrsChanged) {
887
+ // Rewrite the opening tag (a nonce, if any, lands first).
888
+ edits.push({
889
+ start: span.start,
890
+ end: span.openEnd,
891
+ text: `<${tag.tag}${nonceStr}${serializeAttrs(tag.attrs)}>`,
892
+ insert: false
893
+ });
894
+ } else if (nonceStr) {
895
+ // Nonce-only change: insert right after the tag name, rest verbatim.
896
+ edits.push({
897
+ start: span.nameEnd,
898
+ end: span.nameEnd,
899
+ text: nonceStr,
900
+ insert: true
901
+ });
902
+ }
903
+ }
904
+ if (addCsp) {
905
+ if (nonce) {
906
+ scriptSrc.add(`'nonce-${nonce}'`);
907
+ styleSrc.add(`'nonce-${nonce}'`);
908
+ }
909
+ // Strict baseline; a user `policy` directive replaces one, hashes/nonce append.
910
+ /** @type {Map<string, Set<string>>} */
911
+ const directives = new Map([
912
+ ["script-src", new Set(["'self'"])],
913
+ ["style-src", new Set(["'self'"])],
914
+ ["object-src", new Set(["'none'"])],
915
+ ["base-uri", new Set(["'self'"])]
916
+ ]);
917
+ const policyOption =
918
+ /** @type {Exclude<CspOption, boolean | undefined>} */ (options).policy;
919
+ if (policyOption) {
920
+ for (const [name, value] of Object.entries(policyOption)) {
921
+ directives.set(name, new Set(Array.isArray(value) ? value : [value]));
922
+ }
923
+ }
924
+ /** @type {(name: string, extra: Set<string>) => void} */
925
+ const append = (name, extra) => {
926
+ let set = directives.get(name);
927
+ if (!set) directives.set(name, (set = new Set()));
928
+ for (const value of extra) set.add(value);
929
+ };
930
+ append("script-src", scriptSrc);
931
+ append("style-src", styleSrc);
932
+ const policy = [...directives]
933
+ .filter(([, values]) => values.size > 0)
934
+ .map(([name, values]) => `${name} ${[...values].join(" ")}`)
935
+ .join("; ");
936
+ const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(
937
+ policy
938
+ )}">`;
939
+ // First at the head anchor, so it precedes any head-prepend tag.
940
+ at(anchors.headOpen, meta);
941
+ }
942
+ // Emit region placements at the `<head>`/`<body>` anchors (after any CSP
943
+ // meta, prepend before the plain name). The parser always synthesizes an
944
+ // implied `<head>`/`<body>`, so both anchors are always present.
945
+ at(anchors.headOpen, groups["head-prepend"]);
946
+ at(anchors.headEnd, groups.head);
947
+ at(anchors.bodyOpen, groups["body-prepend"]);
948
+ at(anchors.bodyEnd, groups.body);
949
+ for (const [pos, text] of inserts) {
950
+ edits.push({ start: pos, end: pos, text, insert: true });
951
+ }
952
+ if (edits.length === 0) return content;
953
+ edits.sort(
954
+ (a, b) => b.start - a.start || Number(a.insert) - Number(b.insert)
955
+ );
956
+ let result = content;
957
+ for (const edit of edits) {
958
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
959
+ }
960
+ return result;
961
+ }
962
+
963
+ /**
964
+ * Embeds each inlined chunk's content hash into its inline sentinel
965
+ * (`…__<type>__END__` → `…__<type>__<hash>__END__`). The sentinel otherwise
966
+ * carries only the stable chunk id, so the host HTML's bytes wouldn't change
967
+ * when the inlined content changes — the resolved chunk URL that normally
968
+ * busts the page `[contenthash]` is gone once inlined. Embedding the hash
969
+ * makes the emitted bytes content-dependent, so both the initial hash and
970
+ * RealContentHashPlugin's recompute pick up inlined-content changes. The
971
+ * resolver (`resolveChunkInlineSentinels`) ignores this segment.
972
+ * @param {string} content content with chunk-url sentinels already resolved
973
+ * @param {Compilation} compilation compilation
974
+ * @returns {string} content with content-hash-tagged inline sentinels
975
+ */
976
+ static embedInlineChunkHashes(content, compilation) {
977
+ if (!content.includes("__WEBPACK_HTML_INLINE__")) return content;
978
+ const chunksById = getChunksById(compilation);
979
+ return content.replace(
980
+ INLINE_SENTINEL_REGEXP,
981
+ (match, hexId, contentHashType) => {
982
+ const chunkId = Buffer.from(hexId, "hex").toString("utf8");
983
+ const chunk = chunksById.get(chunkId);
984
+ if (!chunk) return match;
985
+ const hash =
986
+ (chunk.contentHash && chunk.contentHash[contentHashType]) ||
987
+ chunk.renderedHash ||
988
+ chunk.hash;
989
+ if (!hash) return match;
990
+ return `__WEBPACK_HTML_INLINE__${hexId}__${contentHashType}__${hash}__END__`;
991
+ }
992
+ );
993
+ }
994
+
123
995
  /**
124
996
  * Creates an instance of HtmlGenerator.
125
997
  * @param {HtmlGeneratorOptions=} options generator options
@@ -162,7 +1034,9 @@ class HtmlGenerator extends Generator {
162
1034
  /**
163
1035
  * Whether this HTML module is reached as a compilation entry. Entry
164
1036
  * modules have at least one incoming connection without an
165
- * `originModule` (the EntryDependency added by `compilation.addEntry`).
1037
+ * `originModule` (the EntryDependency added by `compilation.addEntry`) —
1038
+ * this covers both HTML-as-entry-point and a linked `type: "html"` page
1039
+ * (added as its own entry by HtmlModulesPlugin).
166
1040
  * @param {NormalModule} module module
167
1041
  * @returns {boolean} true when the module is an entry
168
1042
  */
@@ -178,7 +1052,8 @@ class HtmlGenerator extends Generator {
178
1052
  * Whether to emit the extracted `.html` file for this module.
179
1053
  * `options.extract === true` always extracts; `false` and `"inline"` never;
180
1054
  * when the option is left unspecified, extraction defaults to on for HTML
181
- * modules used as compilation entries — that's the HTML-as-entry-point case.
1055
+ * modules used as compilation entries — that's the HTML-as-entry-point case
1056
+ * and a linked `type: "html"` page (which is added as its own entry).
182
1057
  * @param {NormalModule} module module
183
1058
  * @returns {boolean} true when the `.html` file should be emitted
184
1059
  */
@@ -238,25 +1113,53 @@ class HtmlGenerator extends Generator {
238
1113
 
239
1114
  /**
240
1115
  * Processes the provided module.
241
- * @param {NormalModule} module the current module
242
1116
  * @param {Dependency} dependency the dependency to generate
243
- * @param {InitFragment<GenerateContext>[]} initFragments mutable list of init fragments
244
1117
  * @param {ReplaceSource} source the current replace source which can be modified
245
- * @param {GenerateContext} generateContext the render context
1118
+ * @param {DependencyTemplateContext} templateContext the template context (shared across all dependencies of the module)
246
1119
  * @returns {void}
247
1120
  */
248
- sourceDependency(module, dependency, initFragments, source, generateContext) {
1121
+ sourceDependency(dependency, source, templateContext) {
249
1122
  const constructor =
250
1123
  /** @type {DependencyConstructor} */
251
1124
  (dependency.constructor);
252
- const template = generateContext.dependencyTemplates.get(constructor);
1125
+ const template = templateContext.dependencyTemplates.get(constructor);
253
1126
  if (!template) {
254
1127
  throw new Error(
255
1128
  `No template for dependency: ${dependency.constructor.name}`
256
1129
  );
257
1130
  }
258
1131
 
259
- /** @type {DependencyTemplateContext} */
1132
+ template.apply(dependency, source, templateContext);
1133
+ }
1134
+
1135
+ /**
1136
+ * Processes the provided dependencies block.
1137
+ * @param {import("../DependenciesBlock")} block the dependencies block which will be processed
1138
+ * @param {ReplaceSource} source the current replace source which can be modified
1139
+ * @param {DependencyTemplateContext} templateContext the template context
1140
+ * @returns {void}
1141
+ */
1142
+ sourceBlock(block, source, templateContext) {
1143
+ for (const dependency of block.dependencies) {
1144
+ this.sourceDependency(dependency, source, templateContext);
1145
+ }
1146
+
1147
+ for (const childBlock of block.blocks) {
1148
+ this.sourceBlock(childBlock, source, templateContext);
1149
+ }
1150
+ }
1151
+
1152
+ /**
1153
+ * Processes the provided module.
1154
+ * @param {NormalModule} module the module to generate
1155
+ * @param {InitFragment<GenerateContext>[]} initFragments mutable list of init fragments
1156
+ * @param {ReplaceSource} source the current replace source which can be modified
1157
+ * @param {GenerateContext} generateContext the generateContext
1158
+ * @returns {void}
1159
+ */
1160
+ sourceModule(module, initFragments, source, generateContext) {
1161
+ // Only `dependency` varies across `template.apply` calls, so one context
1162
+ // (and one lazy `chunkInitFragments` getter) serves the whole module.
260
1163
  /** @type {InitFragment<GenerateContext>[] | undefined} */
261
1164
  let chunkInitFragments;
262
1165
  /** @type {DependencyTemplateContext} */
@@ -289,79 +1192,18 @@ class HtmlGenerator extends Generator {
289
1192
  }
290
1193
  };
291
1194
 
292
- template.apply(dependency, source, templateContext);
293
- }
294
-
295
- /**
296
- * Processes the provided dependencies block.
297
- * @param {NormalModule} module the module to generate
298
- * @param {import("../DependenciesBlock")} block the dependencies block which will be processed
299
- * @param {InitFragment<GenerateContext>[]} initFragments mutable list of init fragments
300
- * @param {ReplaceSource} source the current replace source which can be modified
301
- * @param {GenerateContext} generateContext the generateContext
302
- * @returns {void}
303
- */
304
- sourceBlock(module, block, initFragments, source, generateContext) {
305
- for (const dependency of block.dependencies) {
306
- this.sourceDependency(
307
- module,
308
- dependency,
309
- initFragments,
310
- source,
311
- generateContext
312
- );
313
- }
314
-
315
- for (const childBlock of block.blocks) {
316
- this.sourceBlock(
317
- module,
318
- childBlock,
319
- initFragments,
320
- source,
321
- generateContext
322
- );
323
- }
324
- }
325
-
326
- /**
327
- * Processes the provided module.
328
- * @param {NormalModule} module the module to generate
329
- * @param {InitFragment<GenerateContext>[]} initFragments mutable list of init fragments
330
- * @param {ReplaceSource} source the current replace source which can be modified
331
- * @param {GenerateContext} generateContext the generateContext
332
- * @returns {void}
333
- */
334
- sourceModule(module, initFragments, source, generateContext) {
335
1195
  for (const dependency of module.dependencies) {
336
- this.sourceDependency(
337
- module,
338
- dependency,
339
- initFragments,
340
- source,
341
- generateContext
342
- );
1196
+ this.sourceDependency(dependency, source, templateContext);
343
1197
  }
344
1198
 
345
1199
  if (module.presentationalDependencies !== undefined) {
346
1200
  for (const dependency of module.presentationalDependencies) {
347
- this.sourceDependency(
348
- module,
349
- dependency,
350
- initFragments,
351
- source,
352
- generateContext
353
- );
1201
+ this.sourceDependency(dependency, source, templateContext);
354
1202
  }
355
1203
  }
356
1204
 
357
1205
  for (const childBlock of module.blocks) {
358
- this.sourceBlock(
359
- module,
360
- childBlock,
361
- initFragments,
362
- source,
363
- generateContext
364
- );
1206
+ this.sourceBlock(childBlock, source, templateContext);
365
1207
  }
366
1208
  }
367
1209
 
@@ -403,7 +1245,12 @@ class HtmlGenerator extends Generator {
403
1245
 
404
1246
  // JS-export path — resolve `[webpack/auto]` inline; chunk-URL sentinels
405
1247
  // stay for `HtmlModulesPlugin`'s `JavascriptModulesPlugin.render` tap.
406
- return rawContent.split(PUBLIC_PATH_AUTO).join(undoPath);
1248
+ if (!rawContent.includes(PUBLIC_PATH_AUTO)) return rawContent;
1249
+ // A relative `<base href>` prepends `../`s so the base can't misdirect
1250
+ // the rewritten URLs (see `HtmlParser`).
1251
+ const basePrefix =
1252
+ /** @type {HtmlModuleBuildInfo} */ (module.buildInfo).baseUrlPrefix || "";
1253
+ return rawContent.split(PUBLIC_PATH_AUTO).join(basePrefix + undoPath);
407
1254
  }
408
1255
 
409
1256
  /**
@@ -603,10 +1450,12 @@ class HtmlGenerator extends Generator {
603
1450
  // Source-type set changes when html-type exposure flips; the HMR shim's
604
1451
  // `extracting` branch changes when the emit decision flips. They differ
605
1452
  // only for `"inline"` (exposes html, but does not emit), so hash both.
606
- if (this._shouldExposeHtmlType(updateHashContext.module)) {
1453
+ // One `_shouldExtract` walk covers both checks (expose = extract ∪ inline).
1454
+ const extract = this._shouldExtract(updateHashContext.module);
1455
+ if (extract || this.options.extract === "inline") {
607
1456
  hash.update("html-type");
608
1457
  }
609
- if (this._shouldExtract(updateHashContext.module)) {
1458
+ if (extract) {
610
1459
  hash.update("extract");
611
1460
  }
612
1461
  // The HMR shim emits additional self-accept / DOM-patch code that