44
55"use strict" ;
66
7+ const {
8+ CSS_IMPORT_TYPE ,
9+ CSS_TYPE ,
10+ JAVASCRIPT_TYPE
11+ } = require ( "../ModuleSourceTypeConstants" ) ;
712const makeSerializable = require ( "../util/makeSerializable" ) ;
813const CssUrlDependency = require ( "./CssUrlDependency" ) ;
914const ModuleDependency = require ( "./ModuleDependency" ) ;
1015
1116/** @typedef {import("webpack-sources").ReplaceSource } ReplaceSource */
1217/** @typedef {import("../Chunk") } Chunk */
18+ /** @typedef {import("../ChunkGraph") } ChunkGraph */
1319/** @typedef {import("../Dependency") } Dependency */
1420/** @typedef {import("../DependencyTemplate").DependencyTemplateContext } DependencyTemplateContext */
1521/** @typedef {import("../Entrypoint") } Entrypoint */
@@ -184,6 +190,144 @@ const getEntrypointChunksInLoadOrder = (entrypoint) => {
184190 return ordered ;
185191} ;
186192
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+ */
212+ const 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+ */
236+ const 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+ */
257+ const 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+ */
286+ const 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+
302+ const 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+ */
316+ const 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, """ ) ;
328+ return `<link rel="stylesheet" href="${ safeHref } "${ extra } >` ;
329+ } ;
330+
187331/**
188332 * Clone the original `<script>`/`<link>` opening tag with its `src`/`href`
189333 * value swapped for a different chunk URL. Reusing the source text verbatim
@@ -252,6 +396,7 @@ HtmlScriptSrcDependency.Template = class HtmlScriptSrcDependencyTemplate extends
252396 const { runtimeTemplate } = templateContext ;
253397 const dep = /** @type {HtmlScriptSrcDependency } */ ( dependency ) ;
254398 const compilation = runtimeTemplate . compilation ;
399+ const { chunkGraph } = compilation ;
255400 const entrypoint = /** @type {Entrypoint | undefined } */ (
256401 compilation . entrypoints . get ( dep . entryName )
257402 ) ;
@@ -263,50 +408,144 @@ HtmlScriptSrcDependency.Template = class HtmlScriptSrcDependencyTemplate extends
263408
264409 const orderedChunks = getEntrypointChunksInLoadOrder ( entrypoint ) ;
265410 const entryChunk = orderedChunks [ orderedChunks . length - 1 ] ;
266- const contentHashType =
267- dep . elementKind === "stylesheet" ? "css" : "javascript" ;
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" ;
268417 const entryUrl = `${ CssUrlDependency . PUBLIC_PATH_AUTO } ${ getChunkFilename (
269418 entryChunk ,
270419 compilation ,
271- contentHashType
420+ entryContentHashType
272421 ) } `;
273422 source . replace ( dep . range [ 0 ] , dep . range [ 1 ] - 1 , entryUrl ) ;
274423
275- if (
276- orderedChunks . length <= 1 ||
277- dep . tagStart < 0 ||
278- dep . tagOpenEnd <= dep . tagStart
279- ) {
424+ if ( dep . tagStart < 0 || dep . tagOpenEnd <= dep . tagStart ) {
280425 return ;
281426 }
282427
283- // The browser must load every chunk in dependency order, not just the
284- // entry chunk. Clone the original tag for each non-entry chunk so the
285- // preserved attributes (nonce, crossorigin, …) match the entry tag,
286- // and insert the clones before the original tag's `<`.
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).
287438 const originalContent = /** @type {string } */ ( source . original ( ) . source ( ) ) ;
288439 const originalTag = originalContent . slice ( dep . tagStart , dep . tagOpenEnd ) ;
289440 const srcStartInTag = dep . range [ 0 ] - dep . tagStart ;
290441 const srcEndInTag = dep . range [ 1 ] - dep . tagStart ;
291442
292- const siblings = [ ] ;
293- for ( let i = 0 ; i < orderedChunks . length - 1 ; i ++ ) {
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 ) => {
294449 const url = `${ CssUrlDependency . PUBLIC_PATH_AUTO } ${ getChunkFilename (
295- orderedChunks [ i ] ,
450+ chunk ,
296451 compilation ,
297- contentHashType
452+ kind
298453 ) } `;
299- siblings . push (
300- cloneTagWithUrl (
301- originalTag ,
302- srcStartInTag ,
303- srcEndInTag ,
304- url ,
305- dep . elementKind
306- )
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
307466 ) ;
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 ( "" ) ) ;
308548 }
309- source . insert ( dep . tagStart , siblings . join ( "" ) ) ;
310549 }
311550} ;
312551
0 commit comments