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
@@ -4,7 +4,10 @@
4
4
 
5
5
  "use strict";
6
6
 
7
+ const { pathToFileURL } = require("url");
8
+ const { AsyncSeriesHook, AsyncSeriesWaterfallHook } = require("tapable");
7
9
  const { RawSource } = require("webpack-sources");
10
+ const Compilation = require("../Compilation");
8
11
  const EntryOptionPlugin = require("../EntryOptionPlugin");
9
12
  const EntryPlugin = require("../EntryPlugin");
10
13
  const HotUpdateChunk = require("../HotUpdateChunk");
@@ -19,21 +22,149 @@ const HtmlInlineStyleDependency = require("../dependencies/HtmlInlineStyleDepend
19
22
  const HtmlSourceDependency = require("../dependencies/HtmlSourceDependency");
20
23
  const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
21
24
  const WebpackError = require("../errors/WebpackError");
25
+ const JavascriptModulesPlugin = require("../javascript/JavascriptModulesPlugin");
22
26
  const { compareModulesByFullName } = require("../util/comparators");
23
27
  const createHash = require("../util/createHash");
28
+ const createHooksRegistry = require("../util/createHooksRegistry");
29
+ const { getUndoPath, makePathsRelative } = require("../util/identifier");
30
+ const implicitTypeLoaderFallback = require("../util/implicitTypeLoaderFallback");
31
+ const memoize = require("../util/memoize");
24
32
  const { digestNonNumericOnly } = require("../util/nonNumericOnlyHash");
33
+ const {
34
+ PUBLIC_PATH_AUTO: autoPlaceholder
35
+ } = require("../util/publicPathPlaceholder");
25
36
  const removeBOM = require("../util/removeBOM");
26
37
  const HtmlGenerator = require("./HtmlGenerator");
27
38
  const HtmlModule = require("./HtmlModule");
28
39
  const HtmlParser = require("./HtmlParser");
40
+ const { escapeAttribute } = require("./syntax");
29
41
 
30
42
  /** @typedef {import("../../declarations/WebpackOptions").EntryDescriptionNormalized} EntryDescriptionNormalized */
43
+ /** @typedef {import("../../declarations/WebpackOptions").OutputHtmlOptions["favicon"]} FaviconOption */
44
+ /** @typedef {import("../../declarations/WebpackOptions").OutputHtmlOptions["manifest"]} ManifestOption */
45
+ /** @typedef {import("../../declarations/WebpackOptions").HtmlFaviconIcon} HtmlFaviconIcon */
46
+ /** @typedef {Exclude<HtmlFaviconIcon, string>} FaviconIcon */
31
47
  /** @typedef {import("../Compiler")} Compiler */
32
48
  /** @typedef {import("./HtmlModule").HtmlModuleBuildInfo} HtmlModuleBuildInfo */
33
- /** @typedef {{ request: string, entryName: string, type: "script" | "script-module" | "modulepreload" | "stylesheet" }} EntryScriptInfo */
49
+ /** @typedef {{ request: string, entryName: string, type: "script" | "script-module" | "modulepreload" | "stylesheet" | "html" | "preload" | "prefetch", css?: boolean }} HtmlEntryInfo */
50
+
51
+ /** @typedef {{ outputName: string }} HtmlTransformHtmlContext */
52
+ /** @typedef {{ outputName: string }} HtmlEmittedContext */
53
+ /**
54
+ * A tag to inject into an emitted page as a structured descriptor. Injected
55
+ * verbatim (not re-bundled) at the `injectTo` position.
56
+ * @typedef {object} HtmlTagDescriptor
57
+ * @property {string} tag tag name, e.g. `"script"` / `"link"` / `"meta"`
58
+ * @property {Record<string, string | boolean | undefined>=} attrs attributes; `true` renders a bare boolean attribute, `false`/`undefined` is omitted
59
+ * @property {string=} children inner content (ignored for void elements like `<link>`/`<meta>`)
60
+ * @property {("head" | "body" | "head-prepend" | "body-prepend")=} injectTo placement; defaults to `"head"`
61
+ * @property {boolean=} voidTag force a void element (no closing tag); inferred from the tag name when omitted
62
+ */
63
+ /** @typedef {{ outputName: string, html: string }} HtmlInjectTagsContext */
64
+ /**
65
+ * A `<script>`/`<link>`/`<style>`/`<meta>` tag already present in an emitted
66
+ * page, exposed for in-place mutation by `transformTags`. Mutate `attrs`, set
67
+ * `remove`, or change `injectTo` to move it; don't reorder the array itself.
68
+ * @typedef {object} HtmlMutableTag
69
+ * @property {string} tag the (lowercased) tag name
70
+ * @property {Record<string, string | boolean | undefined>} attrs mutable attributes; a string value renders `name="value"`, `true` a bare attribute, `false`/`undefined`/deleting the key drops it
71
+ * @property {("head" | "body" | "head-prepend" | "body-prepend")=} injectTo the tag's current region (`"head"`/`"body"`); set a different value to move it there (`*-prepend` to the region's start)
72
+ * @property {boolean=} remove set true to delete the whole element
73
+ */
74
+ /** @typedef {{ outputName: string, html: string }} HtmlTransformTagsContext */
75
+ /**
76
+ * @typedef {object} HtmlCompilationHooks
77
+ * @property {AsyncSeriesWaterfallHook<[HtmlTagDescriptor[], HtmlInjectTagsContext]>} injectTags called with the list of extra tags to inject into each page (initially empty) plus the current HTML; push `HtmlTagDescriptor`s and return the list — webpack serializes and places them by `injectTo`. A structured alternative to the string-level `transformHtml` for adding tags; runs before CSP so injected inline tags are hashed
78
+ * @property {AsyncSeriesHook<[HtmlMutableTag[], HtmlTransformTagsContext]>} transformTags called with the page's `<script>`/`<link>`/`<style>`/`<meta>` tags (webpack's own and any injected) as mutable descriptors; mutate `attrs` (add a `nonce`/`data-*`, switch `defer`↔`async`, …), set `remove: true`, or change `injectTo` to move a tag between `<head>` and `<body>`, and webpack rewrites the changed tags. Add new tags with `injectTags` instead
79
+ * @property {AsyncSeriesWaterfallHook<[string, HtmlTransformHtmlContext]>} transformHtml called with each emitted page's final HTML (all sentinels resolved) just before it is written; return the (possibly transformed) HTML — e.g. to minify, inject a CSP meta, or rewrite tags
80
+ * @property {AsyncSeriesHook<[HtmlEmittedContext]>} htmlEmitted called once each page's HTML asset has been finalized — a post-emit notification (nothing to return)
81
+ */
34
82
 
35
83
  const PLUGIN_NAME = "HtmlModulesPlugin";
36
84
 
85
+ // Built-in default favicon (the webpack logo). Referenced as a `file:` request
86
+ // so `<link rel="icon">` in the synthetic wrapper flows through the normal
87
+ // asset pipeline — emitted hashed, with the correct publicPath, no direct IO.
88
+ // Resolved from `__filename` (a sibling of this module) — no `path` needed.
89
+ const DEFAULT_FAVICON = new URL("./favicon.svg", pathToFileURL(__filename))
90
+ .href;
91
+
92
+ // mime-db is heavy — only load it when a favicon actually needs a `type`.
93
+ const getMimeTypes = memoize(() => require("../util/mimeTypes"));
94
+
95
+ /**
96
+ * Resolves `output.html.favicon` for one page to `[rel, icon]` pairs.
97
+ * `false`/absent → none; `true` → the webpack logo; a string → one `icon`
98
+ * link; an object maps each `rel` to an icon (a path string, an attributes
99
+ * object, or an array of those for several icons under one `rel`); a function
100
+ * receives the page name and returns any of those. Each icon is normalized to
101
+ * an object so `faviconLinkTag` reads one shape.
102
+ * @param {FaviconOption} favicon favicon option
103
+ * @param {string} name page/entry name
104
+ * @returns {[string, FaviconIcon][]} rel/icon pairs, in order
105
+ */
106
+ const resolveFaviconLinks = (favicon, name) => {
107
+ const value = typeof favicon === "function" ? favicon(name) : favicon;
108
+ if (value === true) return [["icon", { href: DEFAULT_FAVICON }]];
109
+ if (typeof value === "string") return [["icon", { href: value }]];
110
+ if (value && typeof value === "object") {
111
+ /** @type {[string, FaviconIcon][]} */
112
+ const pairs = [];
113
+ for (const [rel, icon] of Object.entries(value)) {
114
+ for (const one of Array.isArray(icon) ? icon : [icon]) {
115
+ pairs.push([rel, typeof one === "string" ? { href: one } : one]);
116
+ }
117
+ }
118
+ return pairs;
119
+ }
120
+ return [];
121
+ };
122
+
123
+ /**
124
+ * @param {string} rel link relation
125
+ * @param {FaviconIcon} icon icon `href` plus optional link attributes
126
+ * @returns {string} a `<link rel=… [attrs…] href=…>` tag; `type` defaults to the file format
127
+ */
128
+ const faviconLinkTag = (rel, icon) => {
129
+ const type = icon.type || getMimeTypes().lookup(icon.href) || "";
130
+ const attrs = [
131
+ `rel="${escapeAttribute(rel)}"`,
132
+ icon.sizes && `sizes="${escapeAttribute(icon.sizes)}"`,
133
+ type && `type="${escapeAttribute(type)}"`,
134
+ icon.media && `media="${escapeAttribute(icon.media)}"`,
135
+ icon.color && `color="${escapeAttribute(icon.color)}"`,
136
+ icon.crossorigin && `crossorigin="${escapeAttribute(icon.crossorigin)}"`,
137
+ `href="${escapeAttribute(icon.href)}"`
138
+ ];
139
+ return `<link ${attrs.filter(Boolean).join(" ")}>`;
140
+ };
141
+
142
+ /**
143
+ * Resolves `output.html.manifest` for one page to a `<link rel="manifest">`
144
+ * tag, or `""` when unset. A string is a path to an existing `.webmanifest`
145
+ * file; an object is serialized to a base64 `data:application/manifest+json`
146
+ * URL (routed to `asset/webmanifest` by a default rule, so its relative icon
147
+ * `src`s are hashed like any request); a function receives the page name.
148
+ * Base64 (not percent-encoding) is required because this tag is embedded in
149
+ * the outer `data:text/html` wrapper, which is itself percent-decoded — a
150
+ * percent-encoded inner URL would be corrupted by that decode.
151
+ * @param {ManifestOption} manifest manifest option
152
+ * @param {string} name page/entry name
153
+ * @returns {string} the `<link>` tag, or ""
154
+ */
155
+ const manifestLinkTag = (manifest, name) => {
156
+ const value = typeof manifest === "function" ? manifest(name) : manifest;
157
+ if (!value) return "";
158
+ const href =
159
+ typeof value === "string"
160
+ ? value
161
+ : `data:application/manifest+json;base64,${Buffer.from(
162
+ JSON.stringify(value),
163
+ "utf8"
164
+ ).toString("base64")}`;
165
+ return `<link rel="manifest" href="${escapeAttribute(href)}">`;
166
+ };
167
+
37
168
  // `\.html`/`\.css` request matchers for the synthetic `output.html` wrapper.
38
169
  const HTML_REQUEST_RE = /\.html(\?|$)/i;
39
170
  const CSS_REQUEST_RE = /\.css(\?|$)/i;
@@ -118,6 +249,24 @@ class HtmlModulesPlugin {
118
249
  );
119
250
  }
120
251
 
252
+ /**
253
+ * Filename template for an extracted HTML page: `output.htmlFilename` for
254
+ * initial chunks, `output.htmlChunkFilename` otherwise — the HTML counterpart
255
+ * of `CssModulesPlugin.getChunkFilenameTemplate`.
256
+ * @param {import("../Chunk")} chunk chunk
257
+ * @param {import("../../declarations/WebpackOptions").Output} outputOptions output options
258
+ * @returns {import("../Chunk").ChunkFilenameTemplate} used filename template
259
+ */
260
+ static getChunkFilenameTemplate(chunk, outputOptions) {
261
+ return chunk.canBeInitial()
262
+ ? /** @type {import("../Chunk").ChunkFilenameTemplate} */ (
263
+ outputOptions.htmlFilename
264
+ )
265
+ : /** @type {import("../Chunk").ChunkFilenameTemplate} */ (
266
+ outputOptions.htmlChunkFilename
267
+ );
268
+ }
269
+
121
270
  /**
122
271
  * Applies the plugin by registering its hooks on the compiler.
123
272
  * @param {Compiler} compiler the compiler instance
@@ -179,9 +328,37 @@ class HtmlModulesPlugin {
179
328
  scripts.push(`<script${scriptAttr} src="${r}"></script>`);
180
329
  }
181
330
  }
182
- return `data:text/html,<!doctype html><html><head>${links.join(
183
- ""
184
- )}</head><body>${scripts.join("")}</body></html>`;
331
+ const htmlObj =
332
+ typeof html === "object"
333
+ ? html
334
+ : typeof htmlOption === "object"
335
+ ? htmlOption
336
+ : {};
337
+ const headTags = HtmlParser.buildHeadTags(htmlObj);
338
+ // Injected into webpack-generated pages only; each icon's href is a
339
+ // normal request the parser turns into a hashed asset.
340
+ const favicon =
341
+ typeof html === "object" && html.favicon !== undefined
342
+ ? html.favicon
343
+ : false;
344
+ const faviconTag = resolveFaviconLinks(favicon, name)
345
+ .map(([rel, icon]) => faviconLinkTag(rel, icon))
346
+ .join("");
347
+ const manifest =
348
+ typeof html === "object" && html.manifest !== undefined
349
+ ? html.manifest
350
+ : false;
351
+ const manifestTag = manifestLinkTag(manifest, name);
352
+ const inject = htmlObj.inject;
353
+ const scriptsInHead =
354
+ inject === "head" || (inject !== "body" && output.module);
355
+ const scriptsFirst = output.module || scriptAttr === " defer";
356
+ const headScripts = scriptsInHead ? scripts.join("") : "";
357
+ return `data:text/html,<!doctype html><html><head>${faviconTag}${manifestTag}${headTags}${
358
+ scriptsFirst
359
+ ? headScripts + links.join("")
360
+ : links.join("") + headScripts
361
+ }</head><body>${scriptsInHead ? "" : scripts.join("")}</body></html>`;
185
362
  }
186
363
  );
187
364
 
@@ -199,35 +376,46 @@ class HtmlModulesPlugin {
199
376
  // can never be imported as a runtime leader by a later script —
200
377
  // that's what keeps the "preload but don't execute" contract of
201
378
  // `<link rel="modulepreload">` intact.
202
- /** @type {WeakMap<import("../Compilation"), Set<string>>} */
203
- const stylesheetEntriesPerCompilation = new WeakMap();
379
+ // Per-compilation state: the HTML modules seen during make (so
380
+ // `finishMake` creates their entries without scanning the whole module
381
+ // graph) and the stylesheet entry names (read in `afterChunks`).
382
+ /** @type {WeakMap<import("../Compilation"), { htmlModules: Set<import("../Module")>, stylesheetEntries: Set<string>, htmlAssetNames: Set<string> }>} */
383
+ const compilationState = new WeakMap();
384
+ /**
385
+ * @param {import("../Compilation")} compilation compilation
386
+ * @returns {{ htmlModules: Set<import("../Module")>, stylesheetEntries: Set<string>, htmlAssetNames: Set<string> }} per-compilation state
387
+ */
388
+ const getState = (compilation) => {
389
+ let state = compilationState.get(compilation);
390
+ if (state === undefined) {
391
+ state = {
392
+ htmlModules: new Set(),
393
+ stylesheetEntries: new Set(),
394
+ htmlAssetNames: new Set()
395
+ };
396
+ compilationState.set(compilation, state);
397
+ }
398
+ return state;
399
+ };
204
400
  compiler.hooks.finishMake.tapAsync(PLUGIN_NAME, (compilation, callback) => {
205
- /** @type {Promise<void>[]} */
206
- const promises = [];
207
- /** @type {Set<string>} */
208
- const stylesheetEntries = new Set();
209
- stylesheetEntriesPerCompilation.set(compilation, stylesheetEntries);
210
-
211
- for (const module of compilation.modules) {
212
- if (module.type !== HTML_MODULE_TYPE) continue;
213
- const buildInfo = /** @type {HtmlModuleBuildInfo | undefined} */ (
401
+ const { htmlModules, stylesheetEntries } = getState(compilation);
402
+
403
+ // Collect the entries an HTML module asks for. Only the script chains
404
+ // (`script`, `script-module`) share a runtime via a leader-only
405
+ // `dependOn`; `modulepreload`, `stylesheet` and `html` links are all
406
+ // independent entries (a CSS or page entry must not chain into a JS
407
+ // leader, or the chunk would mix unrelated outputs).
408
+ /** @type {(module: import("../Module")) => { context: string, request: string, name: string, dependOn: string[] | undefined }[]} */
409
+ const collectEntrySpecs = (module) => {
410
+ const { htmlEntries } = /** @type {HtmlModuleBuildInfo} */ (
214
411
  module.buildInfo
215
412
  );
216
- const htmlEntryScripts = buildInfo && buildInfo.htmlEntryScripts;
217
- if (!htmlEntryScripts) continue;
218
-
413
+ if (!htmlEntries) return [];
219
414
  const context = /** @type {string} */ (module.context);
220
-
221
- for (const [groupKind, group] of Object.entries(htmlEntryScripts)) {
222
- // Only the script chains (`script`, `script-module`) need a
223
- // shared runtime via leader-only `dependOn` — the others
224
- // either preload without executing (`modulepreload`) or
225
- // produce CSS chunks (`stylesheet`) which have no runtime
226
- // to share. CSS entries must NOT chain into a JS leader
227
- // either, because the resulting chunk would mix a CSS
228
- // stylesheet with a JS runtime.
415
+ const specs = [];
416
+ for (const [groupKind, group] of Object.entries(htmlEntries)) {
229
417
  const isChainGroup =
230
- groupKind !== "modulepreload" && groupKind !== "stylesheet";
418
+ groupKind === "script" || groupKind === "script-module";
231
419
  /** @type {string | undefined} */
232
420
  let leaderName;
233
421
  for (const entry of group) {
@@ -238,48 +426,220 @@ class HtmlModulesPlugin {
238
426
  if (isChainGroup && leaderName === undefined) {
239
427
  leaderName = entry.entryName;
240
428
  }
241
- if (groupKind === "stylesheet") {
429
+ // Stylesheet entries and `as="style"` preload/prefetch entries emit
430
+ // a CSS chunk, so they need the CSS filename template below.
431
+ if (groupKind === "stylesheet" || entry.css) {
242
432
  stylesheetEntries.add(entry.entryName);
243
433
  }
244
- promises.push(
245
- new Promise((resolve, reject) => {
246
- compilation.addEntry(
247
- context,
248
- EntryPlugin.createDependency(entry.request, {
249
- name: entry.entryName
250
- }),
251
- {
252
- name: entry.entryName,
253
- // Each script src / modulepreload entry gets its own
254
- // filename derived from the synthetic entry name so it
255
- // doesn't collide with the user's `output.filename`.
256
- // For CSS entries the JS `filename` is irrelevant (no
257
- // `.js` is emitted) — the CSS file's name is set on the
258
- // chunk via `cssFilenameTemplate` in `afterChunks` below.
259
- filename:
260
- compilation.outputOptions.chunkFilename || "[name].js",
261
- dependOn
262
- },
263
- (err) => {
264
- if (err) reject(err);
265
- else resolve();
266
- }
267
- );
268
- })
269
- );
434
+ specs.push({
435
+ context,
436
+ request: entry.request,
437
+ name: entry.entryName,
438
+ dependOn
439
+ });
270
440
  }
271
441
  }
272
- }
442
+ return specs;
443
+ };
273
444
 
274
- Promise.all(promises).then(
445
+ // Push one collected reference as a compilation entry; resolves with
446
+ // the built entry module.
447
+ /** @type {(spec: { context: string, request: string, name: string, dependOn: string[] | undefined }) => Promise<import("../Module") | null>} */
448
+ const addEntry = (spec) =>
449
+ new Promise((resolve, reject) => {
450
+ compilation.addEntry(
451
+ spec.context,
452
+ EntryPlugin.createDependency(spec.request, { name: spec.name }),
453
+ {
454
+ name: spec.name,
455
+ // Each entry gets its own filename from the synthetic entry name so
456
+ // it doesn't collide with `output.filename`. CSS entries set their
457
+ // `.css` name via `cssFilenameTemplate` below; `html` page entries
458
+ // emit their file via `renderManifest`.
459
+ filename: compilation.outputOptions.chunkFilename || "[name].js",
460
+ dependOn: spec.dependOn
461
+ },
462
+ (err, entryModule) =>
463
+ err ? reject(err) : resolve(entryModule || null)
464
+ );
465
+ });
466
+
467
+ // Create one HTML module's entries. A `type: "html"` link is itself an
468
+ // HTML entry, so after it builds we recurse into it — the same handling
469
+ // every HTML entry already gets. `processed` guards diamonds and cycles.
470
+ /** @type {WeakSet<import("../Module")>} */
471
+ const processed = new WeakSet();
472
+ /** @type {(module: import("../Module")) => Promise<void>} */
473
+ const processEntry = async (module) => {
474
+ if (processed.has(module)) return;
475
+ processed.add(module);
476
+ await Promise.all(
477
+ collectEntrySpecs(module).map(async (spec) => {
478
+ const entryModule = await addEntry(spec);
479
+ if (entryModule && entryModule.type === HTML_MODULE_TYPE) {
480
+ await processEntry(entryModule);
481
+ }
482
+ })
483
+ );
484
+ };
485
+ // Seed with the HTML modules seen during make (config entries /
486
+ // imports, fresh or cache-restored); linked pages are reached by
487
+ // recursion above.
488
+ Promise.all([...htmlModules].map(processEntry)).then(
275
489
  () => callback(),
276
- (err) => callback(err)
490
+ callback
277
491
  );
278
492
  });
279
493
 
494
+ const integrity =
495
+ typeof htmlOption === "object" ? htmlOption.integrity : undefined;
496
+ const integrityOn =
497
+ integrity === true ||
498
+ typeof integrity === "function" ||
499
+ (Array.isArray(integrity) && integrity.length > 0);
500
+ // `output.html.csp` injects a `<meta http-equiv="Content-Security-Policy">`
501
+ // once the page's inline content is final (in `processAssets` below).
502
+ const csp = typeof htmlOption === "object" ? htmlOption.csp : undefined;
503
+ // `"auto"` marks implicit enablement (no user rule for HTML files) — see
504
+ // `applyExperimentsDefaults`. Only then loaders win over the built-in type
505
+ // (e.g. html-webpack-plugin's template loader in its child compiler).
506
+ // TODO webpack 6: html defaults to `true`, drop this implicit-only fallback.
507
+ const implicitlyEnabled = compiler.options.experiments.html === "auto";
280
508
  compiler.hooks.compilation.tap(
281
509
  PLUGIN_NAME,
282
510
  (compilation, { normalModuleFactory }) => {
511
+ if (implicitlyEnabled) {
512
+ implicitTypeLoaderFallback(
513
+ normalModuleFactory,
514
+ PLUGIN_NAME,
515
+ /\.html$/i,
516
+ HTML_MODULE_TYPE
517
+ );
518
+ }
519
+ const { htmlModules } = getState(compilation);
520
+ // Record HTML modules as they appear — freshly built
521
+ // (`succeedModule`) or restored from cache (`stillValidModule`) —
522
+ // so `finishMake` creates their entries without scanning every
523
+ // module in the graph. Tracking only records; entries are still
524
+ // added (and awaited) in `finishMake`.
525
+ const trackHtmlModule = (/** @type {import("../Module")} */ module) => {
526
+ if (module.type === HTML_MODULE_TYPE) htmlModules.add(module);
527
+ };
528
+ compilation.hooks.succeedModule.tap(PLUGIN_NAME, trackHtmlModule);
529
+ compilation.hooks.stillValidModule.tap(PLUGIN_NAME, trackHtmlModule);
530
+
531
+ // Resolve integrity and inline sentinels after `RealContentHashPlugin`
532
+ // so final bytes are in place. Only HTML pages carry these sentinels —
533
+ // never JS chunks that embed an HTML string.
534
+ // SRI only takes effect on a cross-origin subresource fetched
535
+ // with CORS; without `output.crossOriginLoading` the browser
536
+ // silently ignores `integrity` on cross-origin loads. Warn once
537
+ // so a CDN deployment doesn't ship no-op integrity attributes.
538
+ if (integrityOn && !compilation.outputOptions.crossOriginLoading) {
539
+ compilation.warnings.push(
540
+ new WebpackError(
541
+ 'output.html.integrity is set but output.crossOriginLoading is not. Browsers ignore Subresource Integrity on cross-origin requests made without CORS; set output.crossOriginLoading (e.g. "anonymous") if any asset is served from a different origin.'
542
+ )
543
+ );
544
+ }
545
+ compilation.hooks.processAssets.tapPromise(
546
+ {
547
+ name: PLUGIN_NAME,
548
+ stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH + 1
549
+ },
550
+ async (assets) => {
551
+ const { htmlAssetNames } = getState(compilation);
552
+ const hooks = HtmlModulesPlugin.getCompilationHooks(compilation);
553
+ /** @type {Set<string>} */
554
+ const inlinedFiles = new Set();
555
+ for (const name of Object.keys(assets)) {
556
+ if (!htmlAssetNames.has(name)) continue;
557
+ const content = assets[name].source();
558
+ if (typeof content !== "string") continue;
559
+ let resolved = content;
560
+ if (
561
+ integrityOn &&
562
+ resolved.includes("__WEBPACK_HTML_INTEGRITY__")
563
+ ) {
564
+ resolved = HtmlGenerator.resolveChunkIntegritySentinels(
565
+ resolved,
566
+ compilation,
567
+ /** @type {import("./HtmlGenerator").HtmlIntegrity} */ (
568
+ integrity
569
+ )
570
+ );
571
+ }
572
+ if (resolved.includes("__WEBPACK_HTML_INLINE__")) {
573
+ resolved = HtmlGenerator.resolveChunkInlineSentinels(
574
+ resolved,
575
+ compilation,
576
+ name,
577
+ inlinedFiles
578
+ );
579
+ }
580
+ // `injectTags` (add tags), `transformTags` (mutate/move/remove the
581
+ // page's own and injected tags), and CSP all run over a single
582
+ // parse: collect the tags/anchors once, let the hooks act, then
583
+ // render every edit — placement, attribute rewrites, moves,
584
+ // removals, CSP hashes/nonce/meta — in one pass. Injected inline
585
+ // `<script>`/`<style>` are hashed like the page's own.
586
+ const injected = await hooks.injectTags.promise([], {
587
+ outputName: name,
588
+ html: resolved
589
+ });
590
+ const transformTags = hooks.transformTags.taps.length > 0;
591
+ if (injected.length > 0 || transformTags || csp) {
592
+ const model = HtmlGenerator.collectHtml(resolved);
593
+ HtmlGenerator.addInjectedTags(model, injected);
594
+ if (transformTags) {
595
+ await hooks.transformTags.promise(model.tags, {
596
+ outputName: name,
597
+ html: resolved
598
+ });
599
+ }
600
+ resolved = HtmlGenerator.renderHtml(resolved, model, csp);
601
+ }
602
+ // Final, fully-resolved HTML — let plugins transform it (minify,
603
+ // inject a CSP meta, rewrite tags) before it is written.
604
+ resolved = await hooks.transformHtml.promise(resolved, {
605
+ outputName: name
606
+ });
607
+ if (resolved !== content) {
608
+ compilation.updateAsset(name, new RawSource(resolved));
609
+ }
610
+ await hooks.htmlEmitted.promise({ outputName: name });
611
+ }
612
+ // An inlined chunk file is dead weight once nothing else references
613
+ // it by URL (another page's tag, or the runtime's async chunk map);
614
+ // a content-hashed filename makes a substring hit a real reference.
615
+ // Materialize each other asset's source once (it can be expensive to
616
+ // render) and test all inlined files against it, rather than
617
+ // re-materializing every asset per inlined file.
618
+ if (inlinedFiles.size > 0) {
619
+ /** @type {Set<string>} */
620
+ const stillReferenced = new Set();
621
+ for (const name of Object.keys(compilation.assets)) {
622
+ if (
623
+ inlinedFiles.has(name) ||
624
+ stillReferenced.size === inlinedFiles.size
625
+ ) {
626
+ continue;
627
+ }
628
+ const source = compilation.assets[name].source();
629
+ if (typeof source !== "string") continue;
630
+ for (const file of inlinedFiles) {
631
+ if (!stillReferenced.has(file) && source.includes(file)) {
632
+ stillReferenced.add(file);
633
+ }
634
+ }
635
+ }
636
+ for (const file of inlinedFiles) {
637
+ if (!stillReferenced.has(file)) compilation.deleteAsset(file);
638
+ }
639
+ }
640
+ }
641
+ );
642
+
283
643
  // CSS entries created by `<link rel="stylesheet">` in HTML need
284
644
  // their `.css` filename set via `chunk.cssFilenameTemplate`
285
645
  // (the field `CssModulesPlugin.getChunkFilenameTemplate` reads).
@@ -289,9 +649,8 @@ class HtmlModulesPlugin {
289
649
  // entry emits to a distinct file derived from `output.cssFilename`
290
650
  // (or `output.cssChunkFilename` for non-initial CSS chunks).
291
651
  compilation.hooks.afterChunks.tap(PLUGIN_NAME, () => {
292
- const stylesheetEntries =
293
- stylesheetEntriesPerCompilation.get(compilation);
294
- if (!stylesheetEntries || stylesheetEntries.size === 0) return;
652
+ const { stylesheetEntries } = getState(compilation);
653
+ if (stylesheetEntries.size === 0) return;
295
654
  for (const entryName of stylesheetEntries) {
296
655
  const entrypoint = compilation.entrypoints.get(entryName);
297
656
  if (!entrypoint) continue;
@@ -451,13 +810,6 @@ class HtmlModulesPlugin {
451
810
  // template's `[contenthash]` placeholder works; the
452
811
  // compilation hash is also forwarded so `[fullhash]` /
453
812
  // `[hash]` work in user-supplied templates.
454
- const {
455
- getUndoPath,
456
- makePathsRelative
457
- } = require("../util/identifier");
458
- const {
459
- PUBLIC_PATH_AUTO: autoPlaceholder
460
- } = require("../util/publicPathPlaceholder");
461
813
 
462
814
  // Sentinel-resolved content and its content hash depend only on
463
815
  // the HTML-type module source — not on the chunk or output
@@ -469,9 +821,124 @@ class HtmlModulesPlugin {
469
821
  /** @type {WeakMap<import("webpack-sources").Source, { resolvedContent: string, contentHash: string }>} */
470
822
  const resolvedSentinelHashCache = new WeakMap();
471
823
 
824
+ // Compute a linked/entry HTML page's emitted filename + chunk-URL-resolved
825
+ // content + content hash for one (module, chunk). Shared by the emit loop
826
+ // below and the page-link resolver. The content hash covers only the
827
+ // chunk-URL-resolved source — links to *other* pages stay sentinels here,
828
+ // so a page's filename doesn't depend on the filenames of pages it links to.
829
+ const computePageEmit = (
830
+ /** @type {NormalModule} */ module,
831
+ /** @type {import("../Chunk")} */ chunk
832
+ ) => {
833
+ const { chunkGraph, outputOptions } = compilation;
834
+ const codeGenResult =
835
+ /** @type {import("../CodeGenerationResults")} */ (
836
+ compilation.codeGenerationResults
837
+ ).get(module, chunk.runtime);
838
+ const placeholderSource = codeGenResult.sources.get(HTML_TYPE);
839
+ if (!placeholderSource) return undefined;
840
+ let cached = resolvedSentinelHashCache.get(placeholderSource);
841
+ if (cached === undefined) {
842
+ // Resolve chunk-URL sentinels *before* hashing so the HTML's
843
+ // `[contenthash]` invalidates with the referenced chunks'
844
+ // filenames. Inlined chunks have no URL, so tag their inline
845
+ // sentinels with the chunk content hash — that keeps the emitted
846
+ // bytes (and RealContentHashPlugin's later recompute)
847
+ // content-dependent, so the page hash tracks inlined content too.
848
+ // Asset URL sentinels (from `HtmlEntryDependency` resource-hint
849
+ // tags) resolve here too — deferred from template.apply so it
850
+ // doesn't race with asset-module codegen.
851
+ const resolvedContent = HtmlGenerator.resolveAssetUrlSentinels(
852
+ HtmlGenerator.embedInlineChunkHashes(
853
+ HtmlGenerator.resolveChunkUrlSentinels(
854
+ /** @type {string} */ (placeholderSource.source()),
855
+ compilation
856
+ ),
857
+ compilation
858
+ ),
859
+ compilation
860
+ );
861
+ cached = {
862
+ resolvedContent,
863
+ contentHash: HtmlModulesPlugin.computeContentHash(
864
+ resolvedContent,
865
+ outputOptions
866
+ )
867
+ };
868
+ resolvedSentinelHashCache.set(placeholderSource, cached);
869
+ }
870
+ const resource = module.getResource() || module.resource;
871
+ // Synthetic `output.html` entries are `data:text/html` modules with no
872
+ // real basename — name the file after the entry instead.
873
+ const sourceFilename =
874
+ resource.startsWith("data:text/html") && chunk.name
875
+ ? chunk.name
876
+ : makePathsRelative(
877
+ compiler.context,
878
+ /** @type {string} */ (resource),
879
+ compiler.root
880
+ ).replace(/^\.\//, "");
881
+ const filenameTemplate = HtmlModulesPlugin.getChunkFilenameTemplate(
882
+ chunk,
883
+ outputOptions
884
+ );
885
+ const { path: filename, info } = compilation.getPathWithInfo(
886
+ /** @type {import("../TemplatedPathPlugin").TemplatePath} */
887
+ (filenameTemplate),
888
+ {
889
+ module,
890
+ runtime: chunk.runtime,
891
+ chunkGraph,
892
+ contentHash: cached.contentHash,
893
+ contentHashType: HTML_TYPE,
894
+ filename: sourceFilename,
895
+ hash: compilation.hash
896
+ }
897
+ );
898
+ return {
899
+ resolvedContent: cached.resolvedContent,
900
+ contentHash: cached.contentHash,
901
+ filename,
902
+ info
903
+ };
904
+ };
905
+
906
+ // Emitted filename of a linked `type: "html"` page, keyed by its entry
907
+ // name. Looked up from the page's own entry chunk so it's independent of
908
+ // chunk render order; cached for the compilation.
909
+ /** @type {Map<string, string>} */
910
+ const htmlPageFilenameCache = new Map();
911
+ const computePageFilenameByEntry = (
912
+ /** @type {string} */ entryName
913
+ ) => {
914
+ const cached = htmlPageFilenameCache.get(entryName);
915
+ if (cached !== undefined) return cached;
916
+ let filename = "data:,";
917
+ const entrypoint = compilation.entrypoints.get(entryName);
918
+ const chunk = entrypoint && entrypoint.getEntrypointChunk();
919
+ if (chunk) {
920
+ const modules =
921
+ compilation.chunkGraph.getOrderedChunkModulesIterableBySourceType(
922
+ chunk,
923
+ HTML_TYPE,
924
+ compareModulesByFullName(compilation.compiler)
925
+ );
926
+ const module = modules && modules[Symbol.iterator]().next().value;
927
+ if (module) {
928
+ const emit = computePageEmit(
929
+ /** @type {NormalModule} */ (module),
930
+ chunk
931
+ );
932
+ if (emit) filename = emit.filename;
933
+ }
934
+ }
935
+ htmlPageFilenameCache.set(entryName, filename);
936
+ return filename;
937
+ };
938
+
472
939
  compilation.hooks.renderManifest.tap(
473
940
  PLUGIN_NAME,
474
- (result, { chunk, codeGenerationResults, hash: compilationHash }) => {
941
+ (result, { chunk }) => {
475
942
  // HMR's `HotUpdateChunk`s flow through the same hook
476
943
  // but aren't real output chunks — extracting `.html`
477
944
  // for them would create stray hot-update HTML files.
@@ -488,108 +955,62 @@ class HtmlModulesPlugin {
488
955
  const outputOptions = compilation.outputOptions;
489
956
  for (const module of modules) {
490
957
  const normalModule = /** @type {NormalModule} */ (module);
491
- const codeGenResult = codeGenerationResults.get(
492
- module,
493
- chunk.runtime
494
- );
495
- const placeholderSource = codeGenResult.sources.get(HTML_TYPE);
496
- if (!placeholderSource) continue;
497
-
498
- // `<iframe srcdoc>` modules expose `html` only so
499
- // HtmlInlineHtmlDependency can write the processed markup back
500
- // into the host attribute — they are never standalone pages.
501
- // `generator.extract: "inline"` marks them so no `.html` file is
502
- // emitted; without this they'd collide on the `data:text/html`
503
- // → chunk-name path below (every srcdoc module in a chunk would
958
+ // `<iframe srcdoc>` modules expose `html` only so HtmlInlineHtmlDependency
959
+ // can write the processed markup back into the host attribute — they are
960
+ // never standalone pages. `generator.extract: "inline"` marks them so no
961
+ // `.html` file is emitted; without this they'd collide on the
962
+ // `data:text/html` → chunk-name path (every srcdoc module in a chunk would
504
963
  // emit `<chunk>.html`). An O(1) flag check, not a scan.
505
964
  const generatorOptions = normalModule.generatorOptions;
506
965
  if (generatorOptions && generatorOptions.extract === "inline") {
507
966
  continue;
508
967
  }
509
968
 
510
- const filenameTemplate = chunk.canBeInitial()
511
- ? outputOptions.htmlFilename
512
- : outputOptions.htmlChunkFilename;
513
-
514
- const resource =
515
- normalModule.getResource() || normalModule.resource;
516
- // Synthetic `output.html` entries are `data:text/html` modules
517
- // with no real basename — name the file after the entry instead.
518
- const sourceFilename =
519
- resource.startsWith("data:text/html") && chunk.name
520
- ? chunk.name
521
- : makePathsRelative(
522
- compiler.context,
523
- /** @type {string} */ (resource),
524
- compiler.root
525
- ).replace(/^\.\//, "");
526
-
527
- let cached = resolvedSentinelHashCache.get(placeholderSource);
528
- if (cached === undefined) {
529
- const placeholderContent = /** @type {string} */ (
530
- placeholderSource.source()
531
- );
532
- // Resolve sentinels *before* hashing so the HTML's `[contenthash]`
533
- // invalidates with the referenced chunks' filenames.
534
- const resolvedContent = HtmlGenerator.resolveChunkUrlSentinels(
535
- placeholderContent,
536
- compilation
537
- );
538
- cached = {
539
- resolvedContent,
540
- contentHash: HtmlModulesPlugin.computeContentHash(
541
- resolvedContent,
542
- outputOptions
543
- )
544
- };
545
- resolvedSentinelHashCache.set(placeholderSource, cached);
546
- }
547
- const { resolvedContent, contentHash } = cached;
548
-
549
- const { path: filename, info } = compilation.getPathWithInfo(
550
- /** @type {import("../TemplatedPathPlugin").TemplatePath} */
551
- (filenameTemplate),
552
- {
553
- module,
554
- runtime: chunk.runtime,
555
- chunkGraph,
556
- contentHash,
557
- contentHashType: HTML_TYPE,
558
- filename: sourceFilename,
559
- hash: compilationHash
560
- }
561
- );
969
+ const emit = computePageEmit(normalModule, chunk);
970
+ if (!emit) continue;
971
+ const { resolvedContent, contentHash, filename, info } = emit;
562
972
 
563
- // Resolve any remaining `[webpack/auto]` placeholders to
564
- // an undo path computed from the emitted HTML's location.
565
- // Without this, an `output.htmlFilename` that emits into
566
- // a subdirectory (e.g. `pages/[name].html`) would leave
567
- // asset URLs like `image.png` and chunk URLs like
568
- // `main.js` root-relative, so the browser would resolve
569
- // them under the HTML's directory instead of the
570
- // `output.path` root.
571
- const undoPath = getUndoPath(
572
- filename,
573
- /** @type {string} */ (outputOptions.path),
574
- false
575
- );
576
- const finalContent = resolvedContent
973
+ // Resolve any remaining `[webpack/auto]` placeholders to an undo
974
+ // path computed from the emitted HTML's location, so asset/chunk
975
+ // URLs stay relative to `output.path` even when the page emits into
976
+ // a subdirectory. A relative `<base href>` prepends `../`s so the
977
+ // base can't misdirect the rewritten URLs (see `HtmlParser`).
978
+ const basePrefix =
979
+ /** @type {HtmlModuleBuildInfo} */ (normalModule.buildInfo)
980
+ .baseUrlPrefix || "";
981
+ const undoPath =
982
+ basePrefix +
983
+ getUndoPath(
984
+ filename,
985
+ /** @type {string} */ (outputOptions.path),
986
+ false
987
+ );
988
+ // Resolve linked-page (`type: "html"`) sentinels to each page's
989
+ // emitted filename before the undo path, so the href is relative to
990
+ // this page.
991
+ const finalContent = HtmlGenerator.resolveHtmlPageUrlSentinels(
992
+ resolvedContent,
993
+ computePageFilenameByEntry
994
+ )
577
995
  .split(autoPlaceholder)
578
996
  .join(undoPath);
579
997
  const finalSource = new RawSource(finalContent);
580
- // The same HTML module can land in multiple chunks
581
- // with different `output.htmlFilename` /
582
- // `output.htmlChunkFilename` shapes, which means
583
- // different `undoPath`s and therefore different
584
- // final content for the same module id. Include
585
- // the emitted filename in the asset cache key and
586
- // the post-undo-path content in the hash, so the
587
- // asset cache can't reuse one variant's bytes at
588
- // another variant's URL.
589
- const finalContentHash = HtmlModulesPlugin.computeContentHash(
590
- finalContent,
591
- outputOptions
592
- );
998
+ // The same HTML module can land in multiple chunks with different
999
+ // `htmlFilename`/`htmlChunkFilename` shapes → different `undoPath`s and
1000
+ // final content for the same module id. Include the emitted filename in
1001
+ // the asset cache key and the post-undo-path content in the hash so the
1002
+ // asset cache can't reuse one variant's bytes at another variant's URL.
1003
+ // Unchanged content reuses the memoized hash instead of re-digesting.
1004
+ const finalContentHash =
1005
+ finalContent === resolvedContent
1006
+ ? contentHash
1007
+ : HtmlModulesPlugin.computeContentHash(
1008
+ finalContent,
1009
+ outputOptions
1010
+ );
1011
+
1012
+ // Track HTML pages for the integrity sentinel pass (never JS chunks).
1013
+ getState(compilation).htmlAssetNames.add(filename);
593
1014
 
594
1015
  result.push({
595
1016
  render: () => finalSource,
@@ -608,20 +1029,39 @@ class HtmlModulesPlugin {
608
1029
 
609
1030
  // Resolve sentinels at JS chunk render time so later passes
610
1031
  // (SourceMapDevToolPlugin, size optimizers, RealContentHash) see resolved bytes.
611
- const JavascriptModulesPlugin = require("../javascript/JavascriptModulesPlugin");
612
-
613
1032
  const jsHooks =
614
1033
  JavascriptModulesPlugin.getCompilationHooks(compilation);
615
1034
  jsHooks.render.tap(PLUGIN_NAME, (source, renderContext) => {
1035
+ // No HTML modules ⇒ no sentinels; skip materializing the JS source.
1036
+ if (htmlModules.size === 0) return source;
616
1037
  const raw = source.source();
617
1038
  if (typeof raw !== "string") return source;
618
- if (!raw.includes("__WEBPACK_HTML_CHUNK_URL__")) return source;
619
- const resolved = HtmlGenerator.resolveChunkUrlSentinels(
620
- raw,
621
- compilation
622
- )
623
- .split(autoPlaceholder)
624
- .join("");
1039
+ if (
1040
+ !raw.includes("__WEBPACK_HTML_CHUNK_URL__") &&
1041
+ !raw.includes("__WEBPACK_HTML_PAGE_URL__") &&
1042
+ !raw.includes("__WEBPACK_HTML_INTEGRITY__") &&
1043
+ !raw.includes("__WEBPACK_HTML_INLINE__") &&
1044
+ !raw.includes("__WEBPACK_HTML_ASSET_URL__")
1045
+ ) {
1046
+ return source;
1047
+ }
1048
+ // Strip integrity and inline sentinels (not resolve): a JS chunk
1049
+ // can't hold real SRI hashes for its own not-yet-final bytes, and
1050
+ // inline content belongs only in the final .html, never in JS bundles.
1051
+ const resolved = HtmlGenerator.stripChunkIntegritySentinels(
1052
+ HtmlGenerator.resolveAssetUrlSentinels(
1053
+ HtmlGenerator.resolveHtmlPageUrlSentinels(
1054
+ HtmlGenerator.resolveChunkUrlSentinels(raw, compilation),
1055
+ computePageFilenameByEntry
1056
+ ),
1057
+ compilation
1058
+ )
1059
+ .split(autoPlaceholder)
1060
+ .join("")
1061
+ ).replace(
1062
+ /__WEBPACK_HTML_INLINE__[0-9a-f]+__[a-z]+(?:__[0-9a-f]+)?__END__/g,
1063
+ ""
1064
+ );
625
1065
  if (resolved === raw) return source;
626
1066
  const chunkId = String(renderContext.chunk.id);
627
1067
  const prior = sentinelResolvedSourceCache.get(chunkId);
@@ -653,4 +1093,19 @@ class HtmlModulesPlugin {
653
1093
  }
654
1094
  }
655
1095
 
1096
+ /**
1097
+ * Per-compilation hooks for the experimental HTML support.
1098
+ * @param {Compilation} compilation the compilation
1099
+ * @returns {HtmlCompilationHooks} the hooks
1100
+ */
1101
+ HtmlModulesPlugin.getCompilationHooks = createHooksRegistry(
1102
+ () =>
1103
+ /** @type {HtmlCompilationHooks} */ ({
1104
+ injectTags: new AsyncSeriesWaterfallHook(["tags", "context"]),
1105
+ transformTags: new AsyncSeriesHook(["tags", "context"]),
1106
+ transformHtml: new AsyncSeriesWaterfallHook(["html", "context"]),
1107
+ htmlEmitted: new AsyncSeriesHook(["context"])
1108
+ })
1109
+ );
1110
+
656
1111
  module.exports = HtmlModulesPlugin;