44
55"use strict" ;
66
7- const { basename, extname } = require ( "path" ) ;
87const { pathToFileURL } = require ( "url" ) ;
98const { AsyncSeriesHook, AsyncSeriesWaterfallHook } = require ( "tapable" ) ;
109const { RawSource } = require ( "webpack-sources" ) ;
@@ -28,9 +27,9 @@ const {
2827const createHash = require ( "../util/createHash" ) ;
2928const createHooksRegistry = require ( "../util/createHooksRegistry" ) ;
3029const {
30+ getRequestBaseName,
3131 getUndoPath,
32- makePathsRelative,
33- parseResource
32+ makePathsRelative
3433} = require ( "../util/identifier" ) ;
3534const implicitTypeLoaderFallback = require ( "../util/implicitTypeLoaderFallback" ) ;
3635const lazyModule = require ( "../util/lazyModule" ) ;
@@ -80,7 +79,7 @@ const getCssModulesPlugin = memoize(() => require("../css/CssModulesPlugin"));
8079/** @typedef {import("../Compilation").EntryData } EntryData */
8180/** @import { HtmlModuleBuildInfo } from "./HtmlModule" */
8281/** @import HtmlParser from "./HtmlParser" */
83- /** @typedef {{ request: string, entryName: string, index: number, type: "script" | "script-module" | "modulepreload" | "stylesheet" | "html" | "preload" | "prefetch", css?: boolean } } HtmlEntryInfo */
82+ /** @typedef {{ request: string, entryName: string, index: number, type: "script" | "script-module" | "modulepreload" | "stylesheet" | "html" | "preload" | "prefetch", css?: boolean, chunkName?: string } } HtmlEntryInfo */
8483/**
8584 * What one extracted entry needs to be named. Recorded in page order once
8685 * every page is built, and resolved to filenames once chunks exist.
@@ -114,7 +113,7 @@ const getCssModulesPlugin = memoize(() => require("../css/CssModulesPlugin"));
114113 * @property {boolean= } remove set true to delete the whole element
115114 */
116115/** @typedef {{ outputName: string, html: string } } HtmlTransformTagsContext */
117- /** @typedef {{ context: string, request: string, name: string, index: number, dependOn: string[] | undefined } } HtmlEntrySpec */
116+ /** @typedef {{ context: string, request: string, name: string, index: number, dependOn: string[] | undefined, page: boolean } } HtmlEntrySpec */
118117
119118const createCompilationHooks = ( ) => ( {
120119 /**
@@ -290,19 +289,6 @@ const getPageOnlyEntry = (compilation, module) => {
290289 return configured === undefined ? entry : undefined ;
291290} ;
292291
293- /**
294- * The basename a tag's url lends to the entry it becomes, so
295- * `src="./my-script.js"` emits `my-script.js`. Undefined for an inline body.
296- * @param {string } request the tag's request
297- * @returns {string | undefined } the basename, when the url carries one
298- */
299- const getRequestBaseName = ( request ) => {
300- // An inline `<script>` body is a `data:` request webpack writes itself.
301- if ( request . startsWith ( "data:" ) ) return undefined ;
302- const { path : file } = parseResource ( request ) ;
303- return basename ( file , extname ( file ) ) || undefined ;
304- } ;
305-
306292/**
307293 * Claims `base`, numbering it while something else already emits under that
308294 * name, so two tags sharing a basename stay distinct.
@@ -627,11 +613,11 @@ class HtmlModulesPlugin {
627613 // Per-compilation state: the HTML modules seen during make, so `finishMake`
628614 // creates their entries without scanning the module graph, plus the
629615 // stylesheet entry names and each emitted page's html options.
630- /** @type {WeakMap<import("../Compilation"), { htmlModules: Set<Module>, stylesheetEntries: Set<string>, pendingNames: Map<string, PendingName>, htmlAssetOptions: Map<string, OutputHtmlOptions> }> } */
616+ /** @type {WeakMap<import("../Compilation"), { htmlModules: Set<Module>, stylesheetEntries: Set<string>, pendingNames: Map<string, PendingName>, htmlAssetOptions: Map<string, OutputHtmlOptions>, pageNames: Map<string, string> }> } */
631617 const compilationState = new WeakMap ( ) ;
632618 /**
633619 * @param {import("../Compilation") } compilation compilation
634- * @returns {{ htmlModules: Set<Module>, stylesheetEntries: Set<string>, pendingNames: Map<string, PendingName>, htmlAssetOptions: Map<string, OutputHtmlOptions> } } per-compilation state
620+ * @returns {{ htmlModules: Set<Module>, stylesheetEntries: Set<string>, pendingNames: Map<string, PendingName>, htmlAssetOptions: Map<string, OutputHtmlOptions>, pageNames: Map<string, string> } } per-compilation state
635621 */
636622 const getState = ( compilation ) => {
637623 let state = compilationState . get ( compilation ) ;
@@ -640,14 +626,15 @@ class HtmlModulesPlugin {
640626 htmlModules : new Set ( ) ,
641627 stylesheetEntries : new Set ( ) ,
642628 pendingNames : new Map ( ) ,
643- htmlAssetOptions : new Map ( )
629+ htmlAssetOptions : new Map ( ) ,
630+ pageNames : new Map ( )
644631 } ;
645632 compilationState . set ( compilation , state ) ;
646633 }
647634 return state ;
648635 } ;
649636 compiler . hooks . finishMake . tapAsync ( PLUGIN_NAME , ( compilation , callback ) => {
650- const { htmlModules, stylesheetEntries, pendingNames } =
637+ const { htmlModules, stylesheetEntries, pendingNames, pageNames } =
651638 getState ( compilation ) ;
652639 /** @type {Set<string> } */
653640 const extractedNames = new Set ( ) ;
@@ -699,7 +686,8 @@ class HtmlModulesPlugin {
699686 request : entry . request ,
700687 name : entry . entryName ,
701688 index : entry . index ,
702- dependOn
689+ dependOn,
690+ page : groupKind === "html"
703691 } ) ;
704692 }
705693 }
@@ -783,10 +771,13 @@ class HtmlModulesPlugin {
783771 holder = compilation . moduleGraph . getIssuer ( holder ) ;
784772 }
785773 for ( const entry of ordered ) {
774+ // A name the page or `module.parser.html.chunkName` asked for is what
775+ // the file is called too; otherwise the tag's url lends its basename.
786776 const base =
787777 wrapsEntry && namedEntry
788778 ? namedEntry . name
789- : getRequestBaseName ( entry . request ) ||
779+ : entry . chunkName ||
780+ getRequestBaseName ( entry . request ) ||
790781 pageBase ||
791782 entry . entryName ;
792783 pendingNames . set ( entry . entryName , {
@@ -797,6 +788,11 @@ class HtmlModulesPlugin {
797788 }
798789 links . sort ( ( a , b ) => a . index - b . index ) ;
799790 for ( const link of links ) {
791+ // A linked page is named after its own path; a name the page asked
792+ // for replaces that, and the link to it is rewritten either way.
793+ if ( link . chunkName && ! contestedPageNames . has ( link . entryName ) ) {
794+ pageNames . set ( link . entryName , link . chunkName ) ;
795+ }
800796 const page = linkedPages . get ( link . entryName ) ;
801797 if ( page ) recordNames ( page , seen ) ;
802798 }
@@ -822,6 +818,28 @@ class HtmlModulesPlugin {
822818 ) ;
823819 } ) ;
824820
821+ // What each page name was claimed for, so a second page asking for a
822+ // taken one is refused rather than appended to the entry holding it.
823+ /** @type {Map<string, string> } */
824+ const pageRequests = new Map ( ) ;
825+ // Names a second page asked for and did not get, so it keeps the one its
826+ // own path lends and emits beside the page holding the name.
827+ /** @type {Set<string> } */
828+ const contestedPageNames = new Set ( ) ;
829+ /**
830+ * @param {string } name the name two things ask for
831+ * @param {string } taken what holds it
832+ * @param {string } request what asked for it
833+ */
834+ const reportNameTaken = ( name , taken , request ) => {
835+ contestedPageNames . add ( name ) ;
836+ compilation . errors . push (
837+ new WebpackError (
838+ `The name "${ name } " is asked for by ${ request } and is already taken by ${ taken } . A page is emitted as its name, so give one of them another.`
839+ )
840+ ) ;
841+ } ;
842+
825843 // Create one HTML module's entries. A `type: "html"` link is itself an
826844 // HTML entry, so after it builds we recurse into it — the same handling
827845 // every HTML entry already gets. `processed` guards diamonds and cycles.
@@ -833,7 +851,32 @@ class HtmlModulesPlugin {
833851 processed . add ( module ) ;
834852 await Promise . all (
835853 collectEntrySpecs ( module ) . map ( async ( spec ) => {
854+ if ( spec . page ) {
855+ const claimed = pageRequests . get ( spec . name ) ;
856+ // The page holding the name is already an entry of its own:
857+ // a second link to it adds nothing, a second page cannot have it.
858+ if ( claimed !== undefined ) {
859+ if ( claimed !== spec . request ) {
860+ reportNameTaken ( spec . name , claimed , spec . request ) ;
861+ }
862+ return ;
863+ }
864+ pageRequests . set ( spec . name , spec . request ) ;
865+ }
836866 const entryModule = await addEntry ( spec ) ;
867+ // A page entry webpack made for us holds that page alone; anything
868+ // else in it named the same and would be emitted over.
869+ if ( spec . page ) {
870+ const entry = compilation . entries . get ( spec . name ) ;
871+ if ( entry && entry . dependencies . length !== 1 ) {
872+ reportNameTaken (
873+ spec . name ,
874+ `entry "${ spec . name } "` ,
875+ spec . request
876+ ) ;
877+ return ;
878+ }
879+ }
837880 if ( entryModule && entryModule . type === HTML_MODULE_TYPE ) {
838881 linkedPages . set ( spec . name , entryModule ) ;
839882 await processEntry ( entryModule ) ;
@@ -1269,10 +1312,14 @@ class HtmlModulesPlugin {
12691312 resolvedSentinelHashCache . set ( placeholderSource , cached ) ;
12701313 }
12711314 const resource = module . getResource ( ) || module . resource ;
1272- // Synthetic `output.html` entries are `data:text/html` modules with no
1273- // real basename — name the file after the entry instead.
1315+ // A page is named after its own path, unless it asked for a name; a
1316+ // synthetic `output.html` entry has none, so its entry names it.
1317+ const requestedPageName = chunk . name
1318+ ? getState ( compilation ) . pageNames . get ( chunk . name )
1319+ : undefined ;
12741320 const sourceFilename =
1275- resource . startsWith ( "data:text/html" ) && chunk . name
1321+ requestedPageName ||
1322+ ( resource . startsWith ( "data:text/html" ) && chunk . name
12761323 ? chunk . name
12771324 : toTemplateSourceFileName (
12781325 makePathsRelative (
@@ -1281,7 +1328,7 @@ class HtmlModulesPlugin {
12811328 compiler . root
12821329 ) . replace ( / ^ \. \/ / , "" ) ,
12831330 compilation
1284- ) ;
1331+ ) ) ;
12851332 const filenameTemplate = HtmlModulesPlugin . getChunkFilenameTemplate (
12861333 chunk ,
12871334 outputOptions
0 commit comments