source: frontend/node_modules/webpack/lib/dependencies/HtmlScriptSrcDependency.js

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

Fix frontend appearance

  • Property mode set to 100644
File size: 21.0 KB
Line 
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3*/
4
5"use strict";
6
7const {
8 CSS_IMPORT_TYPE,
9 CSS_TYPE,
10 JAVASCRIPT_TYPE
11} = require("../ModuleSourceTypeConstants");
12const makeSerializable = require("../util/makeSerializable");
13const CssUrlDependency = require("./CssUrlDependency");
14const ModuleDependency = require("./ModuleDependency");
15
16/** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
17/** @typedef {import("../Chunk")} Chunk */
18/** @typedef {import("../ChunkGraph")} ChunkGraph */
19/** @typedef {import("../Dependency")} Dependency */
20/** @typedef {import("../DependencyTemplate").DependencyTemplateContext} DependencyTemplateContext */
21/** @typedef {import("../Entrypoint")} Entrypoint */
22/** @typedef {import("../javascript/JavascriptParser").Range} Range */
23/** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
24/** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
25
26/** @typedef {"script-classic" | "script-module" | "modulepreload" | "stylesheet"} HtmlScriptElementKind */
27
28class HtmlScriptSrcDependency extends ModuleDependency {
29 /**
30 * Creates an instance of HtmlScriptSrcDependency.
31 * @param {string} request request
32 * @param {Range} range range of the attribute value in the source
33 * @param {string} entryName name of the entry this script src is bundled into
34 * @param {string=} category dependency category used for resolving and grouping
35 * @param {HtmlScriptElementKind=} elementKind shape of the originating HTML element; used when expanding sibling tags for split/runtime chunks
36 * @param {number=} tagStart position of the opening `<` of the originating tag in the source; sibling tags emitted for additional entry chunks are inserted right before this
37 * @param {number=} tagOpenEnd position of the character immediately after the opening tag's `>` in the source; combined with `tagStart` this lets the template clone the original opening tag verbatim (preserving attributes like `nonce`, `crossorigin`, `referrerpolicy`, `defer`, `async`) when generating sibling tags
38 */
39 constructor(
40 request,
41 range,
42 entryName,
43 category,
44 elementKind,
45 tagStart,
46 tagOpenEnd
47 ) {
48 super(request);
49 this.range = range;
50 this.entryName = entryName;
51 /** @type {string} */
52 this._category = category || "commonjs";
53 /** @type {HtmlScriptElementKind} */
54 this.elementKind = elementKind || "script-classic";
55 /** @type {number} */
56 this.tagStart = tagStart === undefined ? -1 : tagStart;
57 /** @type {number} */
58 this.tagOpenEnd = tagOpenEnd === undefined ? -1 : tagOpenEnd;
59 }
60
61 get type() {
62 return "html script src";
63 }
64
65 get category() {
66 return this._category;
67 }
68
69 /**
70 * Serializes this instance into the provided serializer context.
71 * @param {ObjectSerializerContext} context context
72 */
73 serialize(context) {
74 const { write } = context;
75 write(this.entryName);
76 write(this._category);
77 write(this.elementKind);
78 write(this.tagStart);
79 write(this.tagOpenEnd);
80 super.serialize(context);
81 }
82
83 /**
84 * Restores this instance from the provided deserializer context.
85 * @param {ObjectDeserializerContext} context context
86 */
87 deserialize(context) {
88 const { read } = context;
89 this.entryName = read();
90 this._category = read();
91 this.elementKind = read();
92 this.tagStart = read();
93 this.tagOpenEnd = read();
94 super.deserialize(context);
95 }
96}
97
98/**
99 * @param {Chunk} chunk a chunk
100 * @param {import("../Compilation")} compilation compilation
101 * @param {"javascript" | "css"} contentHashType which content hash to plug into the filename template
102 * @returns {string} chunk filename path (no public-path prefix)
103 */
104const getChunkFilename = (chunk, compilation, contentHashType) => {
105 const outputOptions = compilation.outputOptions;
106 let filenameTemplate;
107 if (contentHashType === "css") {
108 // For a CSS-typed chunk, use the same template the CSS pipeline
109 // will use when it actually emits the `.css` file, so the `<link
110 // rel="stylesheet" href>` URL we write into the HTML matches the
111 // asset on disk.
112 filenameTemplate =
113 require("../css/CssModulesPlugin").getChunkFilenameTemplate(
114 chunk,
115 outputOptions
116 );
117 } else {
118 filenameTemplate =
119 chunk.filenameTemplate ||
120 (chunk.canBeInitial()
121 ? outputOptions.filename
122 : outputOptions.chunkFilename);
123 }
124
125 return compilation.getPath(filenameTemplate, {
126 chunk,
127 contentHashType
128 });
129};
130
131/**
132 * @param {Entrypoint} entrypoint entrypoint
133 * @returns {Chunk[]} every chunk this entrypoint needs in load order: the
134 * runtime chunk first (when `optimization.runtimeChunk` splits it off), then
135 * any intermediate chunks (e.g. from `optimization.splitChunks`), and finally
136 * the entry chunk itself. The entry chunk is always returned last so callers
137 * can identify it as the tag whose `src`/`href` attribute is being rewritten
138 * in place. Chunks that are already loaded by an ancestor (`dependOn`) entry's
139 * own script tag — i.e. the parent entrypoint's entry chunk *and* its runtime
140 * chunk — are skipped, otherwise they would be loaded twice when the same
141 * HTML contains tags for both the leader and the dependant entries.
142 */
143const getEntrypointChunksInLoadOrder = (entrypoint) => {
144 const entryChunk = /** @type {Chunk} */ (entrypoint.getEntrypointChunk());
145 const runtimeChunk = entrypoint.getRuntimeChunk();
146
147 /** @type {Set<Chunk>} */
148 const chunksLoadedByAncestorTags = new Set();
149 /** @type {Set<import("../ChunkGroup")>} */
150 const visitedGroups = new Set();
151 const walk = (/** @type {import("../ChunkGroup")} */ group) => {
152 if (visitedGroups.has(group)) return;
153 visitedGroups.add(group);
154 for (const parent of group.parentsIterable) {
155 if (
156 typeof (/** @type {Entrypoint} */ (parent).getEntrypointChunk) ===
157 "function"
158 ) {
159 const parentEntry =
160 /** @type {Entrypoint} */
161 (parent).getEntrypointChunk();
162 if (parentEntry) chunksLoadedByAncestorTags.add(parentEntry);
163 const parentRuntime =
164 /** @type {Entrypoint} */
165 (parent).getRuntimeChunk();
166 if (parentRuntime) chunksLoadedByAncestorTags.add(parentRuntime);
167 }
168 walk(parent);
169 }
170 };
171 walk(entrypoint);
172
173 /** @type {Chunk[]} */
174 const ordered = [];
175 /** @type {Set<Chunk>} */
176 const seen = new Set();
177 const push = (/** @type {Chunk | null | undefined} */ chunk) => {
178 if (!chunk || seen.has(chunk) || chunk === entryChunk) return;
179 if (chunksLoadedByAncestorTags.has(chunk)) return;
180 seen.add(chunk);
181 ordered.push(chunk);
182 };
183 if (runtimeChunk !== entryChunk) {
184 push(runtimeChunk);
185 }
186 for (const chunk of entrypoint.chunks) {
187 push(chunk);
188 }
189 ordered.push(entryChunk);
190 return ordered;
191};
192
193/**
194 * Whether webpack will emit a `.js` file for this chunk that must be
195 * loaded with a `<script>` tag. Covers three independent reasons a
196 * chunk needs JS output: it owns one or more JS-source-type modules;
197 * it has entry modules whose source types include JavaScript (entry
198 * modules don't show up in `getChunkModulesIterableBySourceType` until
199 * they're connected as regular modules — this is why
200 * `JavascriptModulesPlugin#_chunkHasJs` checks them separately); or it
201 * is a runtime chunk — `chunk.hasRuntime()` — which produces a `.js`
202 * file holding the webpack runtime, but its `RuntimeModule`s live in
203 * a separate `runtimeModules` set and are *not* surfaced via
204 * `getChunkModulesIterableBySourceType`. Missing the runtime case
205 * would cause a `runtimeChunk`-split chunk to fall out of the
206 * `<script>` list and re-emerge after the chunks that depend on it,
207 * producing `__webpack_require__ is not defined` at load time.
208 * @param {Chunk} chunk chunk
209 * @param {ChunkGraph} chunkGraph chunk graph
210 * @returns {boolean} true if the chunk emits a `.js` file
211 */
212const chunkHasJs = (chunk, chunkGraph) => {
213 if (chunk.hasRuntime()) return true;
214 if (chunkGraph.getNumberOfEntryModules(chunk) > 0) {
215 for (const module of chunkGraph.getChunkEntryModulesIterable(chunk)) {
216 if (chunkGraph.getModuleSourceTypes(module).has(JAVASCRIPT_TYPE)) {
217 return true;
218 }
219 }
220 }
221 return Boolean(
222 chunkGraph.getChunkModulesIterableBySourceType(chunk, JAVASCRIPT_TYPE)
223 );
224};
225
226/**
227 * Whether webpack will emit a `.css` file for this chunk that must be
228 * loaded with a `<link rel="stylesheet">` tag. Matches
229 * `CssModulesPlugin.chunkHasCss` exactly — both regular CSS modules
230 * and pure `@import` placeholder modules count, since the latter
231 * still contribute a `.css` asset to the chunk.
232 * @param {Chunk} chunk chunk
233 * @param {ChunkGraph} chunkGraph chunk graph
234 * @returns {boolean} true if the chunk emits a `.css` file
235 */
236const chunkHasCss = (chunk, chunkGraph) =>
237 Boolean(chunkGraph.getChunkModulesIterableBySourceType(chunk, CSS_TYPE)) ||
238 Boolean(
239 chunkGraph.getChunkModulesIterableBySourceType(chunk, CSS_IMPORT_TYPE)
240 );
241
242/**
243 * Compare two chunks for a deterministic tie-break in CSS link ordering.
244 * `chunk.name` and `chunk.id` are both stable strings (when present);
245 * one of them is set for every chunk webpack emits. We can't rely on
246 * `Array.prototype.sort` being stable — webpack still supports Node
247 * 10.13 where V8's sort is not guaranteed stable for arrays larger
248 * than ten elements — so any time `firstCssModulePostOrderIndex`
249 * returns the same value for two chunks (most commonly when several
250 * chunks have no reachable CSS module in the entrypoint's dependency
251 * walk and all map to `Infinity`) this comparator picks the canonical
252 * order.
253 * @param {Chunk} a first chunk
254 * @param {Chunk} b second chunk
255 * @returns {-1 | 0 | 1} ordering
256 */
257const compareChunksForCssTieBreak = (a, b) => {
258 const an = `${a.name || ""} ${a.id === null || a.id === undefined ? "" : a.id}`;
259 const bn = `${b.name || ""} ${b.id === null || b.id === undefined ? "" : b.id}`;
260 if (an < bn) return -1;
261 if (an > bn) return 1;
262 return 0;
263};
264
265/**
266 * Smallest post-order index among the CSS modules of a chunk, taken
267 * from the entrypoint's view of the dependency graph. Used to sort
268 * sibling CSS chunks so they appear in source import order in the
269 * extracted HTML — `entrypoint.chunks` itself does not give that
270 * ordering for arbitrary splitChunks layouts. Considers both
271 * `CSS_TYPE` and `CSS_IMPORT_TYPE` modules so a chunk made up
272 * exclusively of `@import` placeholder modules (e.g. when splitChunks
273 * separates them from their target CSS) still sorts by its true
274 * source position rather than collapsing to `Infinity` and relying on
275 * the chunk-name tie-breaker.
276 * @param {Chunk} chunk chunk
277 * @param {Entrypoint} entrypoint entrypoint the chunk belongs to
278 * @param {ChunkGraph} chunkGraph chunk graph
279 * @returns {number} the lowest post-order index of any CSS or
280 * CSS-import module in the chunk, or `Number.POSITIVE_INFINITY` when
281 * no such module has a defined index (e.g. for a module the
282 * entrypoint never reached on its own dependency walk — runtime-only
283 * modules, modules reached via `dependOn`, etc.) so such chunks sort
284 * last among CSS chunks
285 */
286const firstCssModulePostOrderIndex = (chunk, entrypoint, chunkGraph) => {
287 let min = Number.POSITIVE_INFINITY;
288 for (const sourceType of [CSS_TYPE, CSS_IMPORT_TYPE]) {
289 const modules = chunkGraph.getChunkModulesIterableBySourceType(
290 chunk,
291 sourceType
292 );
293 if (!modules) continue;
294 for (const module of modules) {
295 const idx = entrypoint.getModulePostOrderIndex(module);
296 if (idx !== undefined && idx < min) min = idx;
297 }
298 }
299 return min;
300};
301
302const COPYABLE_LINK_ATTRS = ["nonce", "crossorigin", "referrerpolicy"];
303
304/**
305 * Build a fresh `<link rel="stylesheet" href="…">` for a CSS chunk that
306 * was pulled in by a `<script src>` entry — the originating tag was a
307 * `<script>`, but the chunk is CSS so cloning the script tag verbatim
308 * would produce nonsense (`<script src="…\.css">`). Copy
309 * `nonce`/`crossorigin`/`referrerpolicy` from the original element so
310 * the same CSP and fetch policy applies; `defer`/`async`/`type` have no
311 * meaning on `<link>` and are dropped.
312 * @param {string} originalTag the originating `<script>`/`<link>` tag's source text
313 * @param {string} href URL for the stylesheet
314 * @returns {string} the sibling `<link>` tag's HTML
315 */
316const buildStylesheetLink = (originalTag, href) => {
317 let extra = "";
318 for (const attr of COPYABLE_LINK_ATTRS) {
319 // Match ` <attr>`, ` <attr>=value`, ` <attr>="value"`, ` <attr>='value'`.
320 const re = new RegExp(
321 `\\s${attr}(?:\\s*=\\s*(?:"[^"]*"|'[^']*'|[^\\s>]+))?(?=[\\s/>])`,
322 "i"
323 );
324 const m = originalTag.match(re);
325 if (m) extra += m[0];
326 }
327 const safeHref = href.replace(/"/g, "&quot;");
328 return `<link rel="stylesheet" href="${safeHref}"${extra}>`;
329};
330
331/**
332 * Clone the original `<script>`/`<link>` opening tag with its `src`/`href`
333 * value swapped for a different chunk URL. Reusing the source text verbatim
334 * preserves attributes such as `nonce`, `crossorigin`, `referrerpolicy`,
335 * `defer`, and `async` so the sibling tags load with the same semantics as
336 * the entry tag that's already there. `integrity` is dropped because it's
337 * content-specific. When the original tag was upgraded to a module script
338 * (either by the author or by the `output.module` auto-upgrade in
339 * `HtmlParser`), the sibling is forced to `type="module"` regardless of what
340 * the source originally said.
341 * @param {string} originalTag the opening tag's source text including `>`
342 * @param {number} srcStartInTag offset of the src/href value start within `originalTag`
343 * @param {number} srcEndInTag offset of the src/href value end within `originalTag`
344 * @param {string} newUrl URL to put into the cloned tag's src/href slot
345 * @param {HtmlScriptElementKind} elementKind shape of the originating tag
346 * @returns {string} the sibling tag's HTML (including a closing `</script>` for script tags)
347 */
348const cloneTagWithUrl = (
349 originalTag,
350 srcStartInTag,
351 srcEndInTag,
352 newUrl,
353 elementKind
354) => {
355 let body =
356 originalTag.slice(0, srcStartInTag) +
357 newUrl +
358 originalTag.slice(srcEndInTag);
359
360 // Strip dangerous-to-copy attributes from the cloned tag — currently
361 // just `integrity`. The match handles all three quoting styles
362 // (`"…"`, `'…'`, unquoted) and the bare-attribute form.
363 body = body.replace(
364 /\s+integrity(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?(?=[\s/>])/gi,
365 ""
366 );
367
368 if (elementKind === "script-module") {
369 if (/\stype\s*=/i.test(body)) {
370 body = body.replace(
371 /(\stype\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s>]+)/i,
372 '$1"module"'
373 );
374 } else {
375 body = body.replace(/^<script\b/i, '<script type="module"');
376 }
377 }
378
379 // `<link>` is a void element — no closing tag. `<script>` needs `</script>`.
380 return elementKind === "modulepreload" || elementKind === "stylesheet"
381 ? body
382 : `${body}</script>`;
383};
384
385HtmlScriptSrcDependency.Template = class HtmlScriptSrcDependencyTemplate extends (
386 ModuleDependency.Template
387) {
388 /**
389 * Applies the plugin by registering its hooks on the compiler.
390 * @param {Dependency} dependency the dependency for which the template should be applied
391 * @param {ReplaceSource} source the current replace source which can be modified
392 * @param {DependencyTemplateContext} templateContext the context object
393 * @returns {void}
394 */
395 apply(dependency, source, templateContext) {
396 const { runtimeTemplate } = templateContext;
397 const dep = /** @type {HtmlScriptSrcDependency} */ (dependency);
398 const compilation = runtimeTemplate.compilation;
399 const { chunkGraph } = compilation;
400 const entrypoint = /** @type {Entrypoint | undefined} */ (
401 compilation.entrypoints.get(dep.entryName)
402 );
403
404 if (!entrypoint) {
405 source.replace(dep.range[0], dep.range[1] - 1, "data:,");
406 return;
407 }
408
409 const orderedChunks = getEntrypointChunksInLoadOrder(entrypoint);
410 const entryChunk = orderedChunks[orderedChunks.length - 1];
411 const isStylesheet = dep.elementKind === "stylesheet";
412
413 // Rewrite the originating tag's src/href to the entry chunk's
414 // primary asset for that element kind: `.css` for
415 // `<link rel="stylesheet">`, `.js` for everything else.
416 const entryContentHashType = isStylesheet ? "css" : "javascript";
417 const entryUrl = `${CssUrlDependency.PUBLIC_PATH_AUTO}${getChunkFilename(
418 entryChunk,
419 compilation,
420 entryContentHashType
421 )}`;
422 source.replace(dep.range[0], dep.range[1] - 1, entryUrl);
423
424 if (dep.tagStart < 0 || dep.tagOpenEnd <= dep.tagStart) {
425 return;
426 }
427
428 // The browser must load every chunk the entry needs, not just the
429 // entry chunk. For `<script>` entries that's the JS for sibling
430 // chunks plus — critically — the CSS for any chunk that holds
431 // stylesheets imported transitively from the JS source. Previously
432 // every sibling was cloned as a `<script>` pointing at a `.js`
433 // filename, so CSS chunks ended up as `<script src="foo.css">`
434 // pointing at non-existent `.js` files (the bug in
435 // html-webpack-plugin#1838 / webpack/mini-css-extract-plugin#959,
436 // magnified here because the entry chunk's own CSS was emitted to
437 // disk but never linked from the HTML at all).
438 const originalContent = /** @type {string} */ (source.original().source());
439 const originalTag = originalContent.slice(dep.tagStart, dep.tagOpenEnd);
440 const srcStartInTag = dep.range[0] - dep.tagStart;
441 const srcEndInTag = dep.range[1] - dep.tagStart;
442
443 /**
444 * @param {Chunk} chunk chunk to emit a sibling tag for
445 * @param {"javascript" | "css"} kind content type slice of the chunk to emit
446 * @returns {string} a single sibling tag's HTML
447 */
448 const buildSibling = (chunk, kind) => {
449 const url = `${CssUrlDependency.PUBLIC_PATH_AUTO}${getChunkFilename(
450 chunk,
451 compilation,
452 kind
453 )}`;
454 if (kind === "css" && !isStylesheet) {
455 // Originating tag is `<script>` (or `<link rel=modulepreload>`)
456 // but this chunk is CSS — emit a fresh `<link>` rather than
457 // cloning the script.
458 return buildStylesheetLink(originalTag, url);
459 }
460 return cloneTagWithUrl(
461 originalTag,
462 srcStartInTag,
463 srcEndInTag,
464 url,
465 dep.elementKind
466 );
467 };
468
469 const siblings = [];
470
471 if (isStylesheet) {
472 // `<link rel="stylesheet">` entries are CSS-only — every sibling
473 // chunk in the entrypoint is also CSS. Keep cloning the original
474 // `<link>` for them so attributes like `media` carry over.
475 for (let i = 0; i < orderedChunks.length - 1; i++) {
476 siblings.push(buildSibling(orderedChunks[i], "css"));
477 }
478 } else {
479 // CSS chunks are emitted before JS chunks so the cascade is set
480 // up before any script runs. Within CSS the order needs to match
481 // the source's import order — `entrypoint.chunks` alone doesn't
482 // give us that for arbitrary splitChunks layouts (splitChunks
483 // inserts each new chunk before the entry chunk via
484 // `insertChunk(_, before)`, so split CSS siblings end up in
485 // *reverse* of the order they were processed — exactly the
486 // html-webpack-plugin#1838 / mini-css-extract#959 symptom). We
487 // re-derive the order from the entrypoint's module post-order
488 // index, which mirrors the dependency walk and so reflects the
489 // import order.
490 /** @type {{ chunk: Chunk, index: number }[]} */
491 const cssChunkOrder = [];
492 /** @type {Chunk[]} */
493 const jsChunks = [];
494 for (let i = 0; i < orderedChunks.length - 1; i++) {
495 const chunk = orderedChunks[i];
496 const hasCss = chunkHasCss(chunk, chunkGraph);
497 const hasJs = chunkHasJs(chunk, chunkGraph);
498 if (hasCss) {
499 cssChunkOrder.push({
500 chunk,
501 index: firstCssModulePostOrderIndex(chunk, entrypoint, chunkGraph)
502 });
503 }
504 // Anything that isn't CSS-only stays on the JS lane, in the
505 // `orderedChunks` order — that preserves the runtime-first /
506 // vendor-before-entry invariant of `getEntrypointChunksInLoadOrder`.
507 // Chunks that produce no `.js` and no `.css` (e.g. wasm-only
508 // or asset-only) still get a `<script>` clone here so we
509 // keep prior behavior for users who relied on it.
510 if (hasJs || !hasCss) jsChunks.push(chunk);
511 }
512 // If the entry chunk itself contains CSS (entry JS imports CSS
513 // without splitChunks separating it), fold it into the same CSS
514 // ordering so the entry-chunk `<link>` lands in the correct
515 // cascade position relative to sibling CSS chunks.
516 if (chunkHasCss(entryChunk, chunkGraph)) {
517 cssChunkOrder.push({
518 chunk: entryChunk,
519 index: firstCssModulePostOrderIndex(
520 entryChunk,
521 entrypoint,
522 chunkGraph
523 )
524 });
525 }
526 cssChunkOrder.sort((a, b) => {
527 // Direct subtraction would yield `NaN` when both indices are
528 // `Infinity` (the documented fallback for chunks whose CSS
529 // modules the entrypoint's walk never reaches), and
530 // `Array#sort` doesn't promise stable ordering on the legacy
531 // Node 10 targets this repo still supports — so the
532 // tie-breaker must always run when the indices match,
533 // including the `Infinity === Infinity` case.
534 if (a.index < b.index) return -1;
535 if (a.index > b.index) return 1;
536 return compareChunksForCssTieBreak(a.chunk, b.chunk);
537 });
538 for (const { chunk } of cssChunkOrder) {
539 siblings.push(buildSibling(chunk, "css"));
540 }
541 for (const chunk of jsChunks) {
542 siblings.push(buildSibling(chunk, "javascript"));
543 }
544 }
545
546 if (siblings.length > 0) {
547 source.insert(dep.tagStart, siblings.join(""));
548 }
549 }
550};
551
552makeSerializable(
553 HtmlScriptSrcDependency,
554 "webpack/lib/dependencies/HtmlScriptSrcDependency"
555);
556
557module.exports = HtmlScriptSrcDependency;
Note: See TracBrowser for help on using the repository browser.