@@ -12,6 +12,8 @@ const _ = require('lodash'),
1212 ASYNC_SCRIPT_TAG = 'async' ,
1313 DEFER_SCRIPT_TAG = 'defer' ,
1414 ASYNC_DEFER_SCRIPT_TAG = 'async-defer' ,
15+ MODULE_SCRIPT_TAG = 'module' ,
16+ MODULEPRELOAD_TAG = 'modulepreload' ,
1517 MEDIA_DIRECTORY = path . join ( process . cwd ( ) , 'public' ) ;
1618
1719/**
@@ -152,6 +154,10 @@ function appendMediaToBottom(scripts, html) {
152154
153155function injectTags ( fileArray , site , tag ) {
154156 var buster = module . exports . cacheBuster ? `?version=${ module . exports . cacheBuster } ` : '' ;
157+ // When omitCacheBusterOnModules is enabled, content-hashed ESM files omit the
158+ // ?version= query string — the hash in the filename already provides cache busting.
159+ // Defaults to false so existing deployments are unaffected.
160+ var moduleBuster = module . exports . omitCacheBusterOnModules ? '' : buster ;
155161
156162 return bluebird . resolve ( _ . map ( fileArray , function ( file ) {
157163 if ( tag === STYLE_TAG ) {
@@ -162,6 +168,12 @@ function injectTags(fileArray, site, tag) {
162168 return `<script defer src="${ file } " type="text/javascript"></script>` ;
163169 } else if ( tag === ASYNC_DEFER_SCRIPT_TAG ) {
164170 return `<script async defer src="${ file } " type="text/javascript"></script>` ;
171+ } else if ( tag === MODULE_SCRIPT_TAG ) {
172+ return `<script type="module" src="${ file } ${ moduleBuster } "></script>` ;
173+ } else if ( tag === MODULEPRELOAD_TAG ) {
174+ // <link rel="modulepreload"> tells the browser to fetch ESM scripts early,
175+ // during HTML parsing, before reaching the <script> tags at </body>.
176+ return `<link rel="modulepreload" href="${ file } ${ moduleBuster } ">` ;
165177 } else {
166178 return `<script type="text/javascript" src="${ file } ${ buster } "></script>` ;
167179 }
@@ -370,6 +382,14 @@ function configure(options, cacheBuster = '') {
370382 if ( options && _ . isObject ( options ) ) {
371383 module . exports . editStylesTags = options . styles || false ;
372384 module . exports . editScriptsTags = options . scripts || false ;
385+ // modulepreload: when true, <link rel="modulepreload"> hints are injected
386+ // into <head> for ESM scripts, and ?version= is omitted from module URLs
387+ // since content-hashed filenames already provide cache busting.
388+ // Opt-in only — defaults to false for backwards compatibility.
389+ if ( options . modulepreload !== undefined ) {
390+ module . exports . modulepreload = ! ! options . modulepreload ;
391+ module . exports . omitCacheBusterOnModules = ! ! options . modulepreload ;
392+ }
373393 } else {
374394 module . exports . editStylesTags = options ;
375395 module . exports . editScriptsTags = options ;
@@ -404,8 +424,29 @@ function injectScriptsAndStyles(state) {
404424 mediaMap = module . exports . getMediaMap ( state ) ;
405425
406426 // allow site to change the media map before applying it
427+ // Expose rendered component names so resolveMedia can do per-component script resolution
428+ // (e.g. pack-next manifest lookup) without needing a reference to the full state object.
429+ locals . _components = state . _components ;
407430 if ( setup . resolveMedia ) mediaMap = setup . resolveMedia ( mediaMap , locals ) || mediaMap ;
408431
432+ // moduleScripts: ESM scripts (e.g. from esbuild pack-next) that need type="module"
433+ const moduleScriptFiles = mediaMap . moduleScripts || [ ] ;
434+
435+ mediaMap . moduleScripts = moduleScriptFiles . length
436+ ? injectTags ( moduleScriptFiles , locals . site , MODULE_SCRIPT_TAG )
437+ : bluebird . resolve ( false ) ;
438+
439+ // modulePreloads: <link rel="modulepreload"> hints for <head>.
440+ // Only active when configure({ modulepreload: true }) has been called — opt-in so
441+ // sites not using the clay build pipeline are completely unaffected.
442+ const modulePreloadFiles = module . exports . modulepreload
443+ ? ( mediaMap . modulePreloads || [ ] )
444+ : [ ] ;
445+
446+ mediaMap . modulePreloads = modulePreloadFiles . length
447+ ? injectTags ( modulePreloadFiles , locals . site , MODULEPRELOAD_TAG )
448+ : bluebird . resolve ( false ) ;
449+
409450 if ( ! locals . edit ) {
410451 mediaMap . styles = combineFileContents ( mediaMap . styles , 'public/css' , '/css/' , STYLE_TAG ) ;
411452 mediaMap . scripts = ! ! mediaMap . manifestAssets && mediaMap . manifestAssets . length > 0
@@ -418,7 +459,11 @@ function injectScriptsAndStyles(state) {
418459
419460 return bluebird . props ( mediaMap )
420461 . then ( combinedFiles => {
462+ // modulepreload hints go first in <head>, before CSS, so the browser can
463+ // start fetching ESM scripts at the earliest possible moment.
464+ html = combinedFiles . modulePreloads ? appendMediaToTop ( combinedFiles . modulePreloads , html ) : html ;
421465 html = combinedFiles . styles ? appendMediaToTop ( combinedFiles . styles , html ) : html ; // If there are styles, append them
466+ html = combinedFiles . moduleScripts ? appendMediaToBottom ( combinedFiles . moduleScripts , html ) : html ; // ESM module scripts (type="module")
422467 html = combinedFiles . scripts ? appendMediaToBottom ( combinedFiles . scripts , html ) : html ; // If there are scripts, append them
423468 return html ; // Return the compiled HTML
424469 } ) ;
@@ -431,6 +476,10 @@ module.exports.cacheBuster = '';
431476module . exports . configure = configure ;
432477module . exports . editStylesTags = false ;
433478module . exports . editScriptsTags = false ;
479+ // Opt-in flags for the clay build (esbuild) pipeline.
480+ // Enable via configure({ modulepreload: true }).
481+ module . exports . modulepreload = false ;
482+ module . exports . omitCacheBusterOnModules = false ;
434483
435484// For testing
436485module . exports . getManifestAssets = getManifestAssets ;
0 commit comments