source: frontend/node_modules/webpack/lib/html/HtmlModulesPlugin.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 15.9 KB
Line 
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3*/
4
5"use strict";
6
7const { RawSource } = require("webpack-sources");
8const EntryPlugin = require("../EntryPlugin");
9const HotUpdateChunk = require("../HotUpdateChunk");
10const { HTML_TYPE } = require("../ModuleSourceTypeConstants");
11const { HTML_MODULE_TYPE } = require("../ModuleTypeConstants");
12const NormalModule = require("../NormalModule");
13const ConstDependency = require("../dependencies/ConstDependency");
14const HtmlInlineScriptDependency = require("../dependencies/HtmlInlineScriptDependency");
15const HtmlInlineStyleDependency = require("../dependencies/HtmlInlineStyleDependency");
16const HtmlScriptSrcDependency = require("../dependencies/HtmlScriptSrcDependency");
17const HtmlSourceDependency = require("../dependencies/HtmlSourceDependency");
18const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
19const { compareModulesByFullName } = require("../util/comparators");
20const removeBOM = require("../util/removeBOM");
21const HtmlGenerator = require("./HtmlGenerator");
22const HtmlParser = require("./HtmlParser");
23
24/** @typedef {import("../Compiler")} Compiler */
25/** @typedef {{ request: string, entryName: string, kind: "classic" | "esm-script" | "modulepreload" | "stylesheet" }} EntryScriptInfo */
26
27const PLUGIN_NAME = "HtmlModulesPlugin";
28
29/**
30 * @param {string} name definition name in `schemas/WebpackOptions.json`
31 * @returns {EXPECTED_OBJECT} a schema referencing `#/definitions/<name>`
32 */
33const getSchema = (name) => {
34 const { definitions } = require("../../schemas/WebpackOptions.json");
35
36 return {
37 definitions,
38 oneOf: [{ $ref: `#/definitions/${name}` }]
39 };
40};
41
42const generatorValidationOptions = {
43 name: "Html Modules Plugin",
44 baseDataPath: "generator"
45};
46
47class HtmlModulesPlugin {
48 /**
49 * Applies the plugin by registering its hooks on the compiler.
50 * @param {Compiler} compiler the compiler instance
51 * @returns {void}
52 */
53 apply(compiler) {
54 // `<script src>` and `<link rel="modulepreload">` references collected
55 // by HtmlParser become real compilation entries here. The classic
56 // and esm-script groups are chained via a leader-only dependOn so
57 // they share a runtime — the first entry of the group owns it and
58 // every subsequent entry sets `dependOn: [leader]`. Modulepreload
59 // entries are emitted as independent entries (no dependOn) so they
60 // can never be imported as a runtime leader by a later script —
61 // that's what keeps the "preload but don't execute" contract of
62 // `<link rel="modulepreload">` intact.
63 /** @type {WeakMap<import("../Compilation"), Set<string>>} */
64 const stylesheetEntriesPerCompilation = new WeakMap();
65 compiler.hooks.finishMake.tapAsync(PLUGIN_NAME, (compilation, callback) => {
66 /** @type {Promise<void>[]} */
67 const promises = [];
68 /** @type {Set<string>} */
69 const stylesheetEntries = new Set();
70 stylesheetEntriesPerCompilation.set(compilation, stylesheetEntries);
71
72 for (const module of compilation.modules) {
73 if (module.type !== HTML_MODULE_TYPE) continue;
74 const buildInfo = module.buildInfo;
75 const htmlEntryScripts =
76 buildInfo &&
77 /** @type {Record<string, EntryScriptInfo[]> | undefined} */
78 (buildInfo.htmlEntryScripts);
79 if (!htmlEntryScripts) continue;
80
81 const context = /** @type {string} */ (module.context);
82
83 for (const [groupKind, group] of Object.entries(htmlEntryScripts)) {
84 // Only the script chains (`classic`, `esm-script`) need a
85 // shared runtime via leader-only `dependOn` — the others
86 // either preload without executing (`modulepreload`) or
87 // produce CSS chunks (`stylesheet`) which have no runtime
88 // to share. CSS entries must NOT chain into a JS leader
89 // either, because the resulting chunk would mix a CSS
90 // stylesheet with a JS runtime.
91 const isChainGroup =
92 groupKind !== "modulepreload" && groupKind !== "stylesheet";
93 /** @type {string | undefined} */
94 let leaderName;
95 for (const entry of group) {
96 const dependOn =
97 isChainGroup && leaderName !== undefined
98 ? [leaderName]
99 : undefined;
100 if (isChainGroup && leaderName === undefined) {
101 leaderName = entry.entryName;
102 }
103 if (groupKind === "stylesheet") {
104 stylesheetEntries.add(entry.entryName);
105 }
106 promises.push(
107 new Promise((resolve, reject) => {
108 compilation.addEntry(
109 context,
110 EntryPlugin.createDependency(entry.request, {
111 name: entry.entryName
112 }),
113 {
114 name: entry.entryName,
115 // Each script src / modulepreload entry gets its own
116 // filename derived from the synthetic entry name so it
117 // doesn't collide with the user's `output.filename`.
118 // For CSS entries the JS `filename` is irrelevant (no
119 // `.js` is emitted) — the CSS file's name is set on the
120 // chunk via `cssFilenameTemplate` in `afterChunks` below.
121 filename:
122 compilation.outputOptions.chunkFilename || "[name].js",
123 dependOn
124 },
125 (err) => {
126 if (err) reject(err);
127 else resolve();
128 }
129 );
130 })
131 );
132 }
133 }
134 }
135
136 Promise.all(promises).then(
137 () => callback(),
138 (err) => callback(err)
139 );
140 });
141
142 compiler.hooks.compilation.tap(
143 PLUGIN_NAME,
144 (compilation, { normalModuleFactory }) => {
145 // CSS entries created by `<link rel="stylesheet">` in HTML need
146 // their `.css` filename set via `chunk.cssFilenameTemplate`
147 // (the field `CssModulesPlugin.getChunkFilenameTemplate` reads).
148 // Compilation only flows `options.filename` to `chunk.filenameTemplate`,
149 // which controls JS emit — there's no entry-level `cssFilename`.
150 // Set it ourselves after chunks are created so each stylesheet
151 // entry emits to a distinct file derived from `output.cssFilename`
152 // (or `output.cssChunkFilename` for non-initial CSS chunks).
153 compilation.hooks.afterChunks.tap(PLUGIN_NAME, () => {
154 const stylesheetEntries =
155 stylesheetEntriesPerCompilation.get(compilation);
156 if (!stylesheetEntries || stylesheetEntries.size === 0) return;
157 for (const entryName of stylesheetEntries) {
158 const entrypoint = compilation.entrypoints.get(entryName);
159 if (!entrypoint) continue;
160 const chunk = entrypoint.getEntrypointChunk();
161 if (!chunk) continue;
162 // Each html-derived stylesheet entry uses the
163 // `cssChunkFilename` template — even though the entry
164 // chunk technically `canBeInitial()`, we deliberately
165 // avoid `cssFilename` here because that template often
166 // has no per-entry placeholder (it's derived from
167 // `output.filename`, which can be a literal like
168 // `bundle0.js`), and multiple `<link rel="stylesheet">`
169 // tags would then collide on the same emitted `.css`
170 // file. `cssChunkFilename` is derived from
171 // `output.chunkFilename` which webpack auto-extends
172 // with `[id].` when needed, guaranteeing uniqueness.
173 chunk.cssFilenameTemplate =
174 compilation.outputOptions.cssChunkFilename;
175 }
176 });
177 compilation.dependencyFactories.set(
178 HtmlSourceDependency,
179 normalModuleFactory
180 );
181 compilation.dependencyTemplates.set(
182 HtmlSourceDependency,
183 new HtmlSourceDependency.Template()
184 );
185 compilation.dependencyFactories.set(
186 HtmlScriptSrcDependency,
187 normalModuleFactory
188 );
189 compilation.dependencyTemplates.set(
190 HtmlScriptSrcDependency,
191 new HtmlScriptSrcDependency.Template()
192 );
193 // Inline `<script>` content is bundled as its own entry — the
194 // same pipeline that handles `<script src>` — via a
195 // `data:text/javascript,...` request. The dependency
196 // template rewrites the original tag to `<script src=…>`.
197 compilation.dependencyFactories.set(
198 HtmlInlineScriptDependency,
199 normalModuleFactory
200 );
201 compilation.dependencyTemplates.set(
202 HtmlInlineScriptDependency,
203 new HtmlInlineScriptDependency.Template()
204 );
205 // Inline `<style>` content is routed through the CSS pipeline
206 // as a `data:text/css` module. The dependency template reads
207 // the processed CSS text from the CSS module's code
208 // generation data (`css-text` channel set by CssGenerator
209 // when `exportType` is `"text"`).
210 compilation.dependencyFactories.set(
211 HtmlInlineStyleDependency,
212 normalModuleFactory
213 );
214 compilation.dependencyTemplates.set(
215 HtmlInlineStyleDependency,
216 new HtmlInlineStyleDependency.Template()
217 );
218 compilation.dependencyTemplates.set(
219 StaticExportsDependency,
220 new StaticExportsDependency.Template()
221 );
222 // `ConstDependency` is used by HtmlParser to insert
223 // ` type="module"` into the rewritten <script> tag when
224 // `output.module` is on. Register its template so the HTML
225 // generator runs the insertion.
226 compilation.dependencyTemplates.set(
227 ConstDependency,
228 new ConstDependency.Template()
229 );
230 const cssEnabled = Boolean(
231 compiler.options.experiments && compiler.options.experiments.css
232 );
233 normalModuleFactory.hooks.createParser
234 .for(HTML_MODULE_TYPE)
235 .tap(
236 PLUGIN_NAME,
237 () =>
238 new HtmlParser(
239 compilation.outputOptions.hashFunction,
240 compiler.context,
241 compilation.outputOptions.module,
242 cssEnabled
243 )
244 );
245
246 normalModuleFactory.hooks.createGenerator
247 .for(HTML_MODULE_TYPE)
248 .tap(PLUGIN_NAME, (generatorOptions) => {
249 compiler.validate(
250 () => getSchema("HtmlGeneratorOptions"),
251 generatorOptions,
252 generatorValidationOptions,
253 (options) =>
254 require("../../schemas/plugins/HtmlGeneratorOptions.check")(
255 options
256 )
257 );
258 return new HtmlGenerator(generatorOptions, compilation.moduleGraph);
259 });
260
261 NormalModule.getCompilationHooks(compilation).processResult.tap(
262 PLUGIN_NAME,
263 (result, module) => {
264 if (module.type === HTML_MODULE_TYPE) {
265 const [source, ...rest] = result;
266
267 return [removeBOM(source), ...rest];
268 }
269
270 return result;
271 }
272 );
273
274 // Emit extracted `.html` files for any HTML module that opted
275 // into extraction. The opt-in is computed by
276 // `HtmlGenerator#_shouldExtract`: `module.generator.html.extract:
277 // true` always extracts, `false` never extracts, and when
278 // `extract` is unset the generator extracts iff the HTML module
279 // is a compilation entry — the iteration below picks up only
280 // modules whose generator reported the `html` source type, so
281 // that decision is honored implicitly. The HTML content is read
282 // from the generator's secondary `"html"` source type (see
283 // HtmlGenerator#generate). The filename template comes from
284 // `output.htmlFilename` (initial chunks) or
285 // `output.htmlChunkFilename` (non-initial chunks), mirroring
286 // the CSS pipeline. Path data follows the asset-module pattern —
287 // `module` + a relative source `filename`, with `chunk`
288 // intentionally omitted so `[name]` resolves to the HTML
289 // source's basename (e.g. `page` for `./page.html`) rather
290 // than the importing chunk's name (e.g. `main`). A per-module
291 // content hash is computed from the rewritten HTML so the
292 // template's `[contenthash]` placeholder works; the
293 // compilation hash is also forwarded so `[fullhash]` /
294 // `[hash]` work in user-supplied templates.
295 const {
296 getUndoPath,
297 makePathsRelative
298 } = require("../util/identifier");
299 const createHash = require("../util/createHash");
300 const nonNumericOnlyHash = require("../util/nonNumericOnlyHash");
301 const CssUrlDependency = require("../dependencies/CssUrlDependency");
302
303 const autoPlaceholder = CssUrlDependency.PUBLIC_PATH_AUTO;
304
305 compilation.hooks.renderManifest.tap(
306 PLUGIN_NAME,
307 (result, { chunk, codeGenerationResults, hash: compilationHash }) => {
308 // HMR's `HotUpdateChunk`s flow through the same hook
309 // but aren't real output chunks — extracting `.html`
310 // for them would create stray hot-update HTML files.
311 // `CssModulesPlugin` early-returns for the same reason.
312 if (chunk instanceof HotUpdateChunk) return result;
313 const { chunkGraph } = compilation;
314 const modules =
315 chunkGraph.getOrderedChunkModulesIterableBySourceType(
316 chunk,
317 HTML_TYPE,
318 compareModulesByFullName(compilation.compiler)
319 );
320 if (!modules) return result;
321 const outputOptions = compilation.outputOptions;
322 for (const module of modules) {
323 const normalModule = /** @type {NormalModule} */ (module);
324 const codeGenResult = codeGenerationResults.get(
325 module,
326 chunk.runtime
327 );
328 const placeholderSource = codeGenResult.sources.get(HTML_TYPE);
329 if (!placeholderSource) continue;
330
331 const filenameTemplate = chunk.canBeInitial()
332 ? outputOptions.htmlFilename
333 : outputOptions.htmlChunkFilename;
334
335 const sourceFilename = makePathsRelative(
336 compiler.context,
337 /** @type {string} */
338 (normalModule.getResource() || normalModule.resource),
339 compiler.root
340 ).replace(/^\.\//, "");
341
342 const placeholderContent = /** @type {string} */ (
343 placeholderSource.source()
344 );
345 const hashInput = createHash(outputOptions.hashFunction);
346 if (outputOptions.hashSalt) {
347 hashInput.update(outputOptions.hashSalt);
348 }
349 hashInput.update(placeholderContent);
350 const fullContentHash = /** @type {string} */ (
351 hashInput.digest(outputOptions.hashDigest)
352 );
353 const contentHash = nonNumericOnlyHash(
354 fullContentHash,
355 outputOptions.hashDigestLength
356 );
357
358 const { path: filename, info } = compilation.getPathWithInfo(
359 /** @type {import("../TemplatedPathPlugin").TemplatePath} */
360 (filenameTemplate),
361 {
362 module,
363 runtime: chunk.runtime,
364 chunkGraph,
365 contentHash,
366 contentHashType: HTML_TYPE,
367 filename: sourceFilename,
368 hash: compilationHash
369 }
370 );
371
372 // Resolve any remaining `[webpack/auto]` placeholders to
373 // an undo path computed from the emitted HTML's location.
374 // Without this, an `output.htmlFilename` that emits into
375 // a subdirectory (e.g. `pages/[name].html`) would leave
376 // asset URLs like `image.png` and chunk URLs like
377 // `main.js` root-relative, so the browser would resolve
378 // them under the HTML's directory instead of the
379 // `output.path` root.
380 const undoPath = getUndoPath(
381 filename,
382 /** @type {string} */ (outputOptions.path),
383 false
384 );
385 const finalContent = placeholderContent
386 .split(autoPlaceholder)
387 .join(undoPath);
388 const finalSource = new RawSource(finalContent);
389 // The same HTML module can land in multiple chunks
390 // with different `output.htmlFilename` /
391 // `output.htmlChunkFilename` shapes, which means
392 // different `undoPath`s and therefore different
393 // final content for the same module id. Include
394 // the emitted filename in the asset cache key and
395 // the post-undo-path content in the hash, so the
396 // asset cache can't reuse one variant's bytes at
397 // another variant's URL.
398 const finalHash = createHash(outputOptions.hashFunction);
399 if (outputOptions.hashSalt) {
400 finalHash.update(outputOptions.hashSalt);
401 }
402 finalHash.update(finalContent);
403 const finalContentHash = nonNumericOnlyHash(
404 /** @type {string} */ (
405 finalHash.digest(outputOptions.hashDigest)
406 ),
407 outputOptions.hashDigestLength
408 );
409
410 result.push({
411 render: () => finalSource,
412 filename,
413 info,
414 auxiliary: true,
415 identifier: `htmlModule${chunkGraph.getModuleId(
416 module
417 )}|${filename}`,
418 hash: finalContentHash
419 });
420 }
421 return result;
422 }
423 );
424 }
425 );
426 }
427}
428
429module.exports = HtmlModulesPlugin;
Note: See TracBrowser for help on using the repository browser.