Skip to content

Commit 4cd428c

Browse files
committed
Add modulepreload and module script support for esbuild pipeline
- Add MODULE_SCRIPT_TAG and MODULEPRELOAD_TAG constants to media.js - Add omitCacheBusterOnModules flag (enabled when modulepreload:true) so content-hashed ESM filenames skip the redundant ?version= query string - Handle moduleScripts and modulePreloads in injectScriptsAndStyles(), injecting <script type="module"> at </body> and <link rel="modulepreload"> in <head> (before CSS) for earliest possible browser fetch - Expose locals._components before resolveMedia callback so sites can do per-component manifest lookup without holding a full state reference - Update configure() in both media.js and render.js to accept modulepreload option; defaults to false for full backwards compatibility Made-with: Cursor
1 parent 593813d commit 4cd428c

2 files changed

Lines changed: 55 additions & 2 deletions

File tree

lib/media.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

153155
function 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 = '';
431476
module.exports.configure = configure;
432477
module.exports.editStylesTags = false;
433478
module.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
436485
module.exports.getManifestAssets = getManifestAssets;

lib/render.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,12 @@ function logTime(hrStart, msg, route) {
182182
};
183183
}
184184

185-
function configure({ editAssetTags, cacheBuster }) {
186-
mediaService.configure(editAssetTags, cacheBuster);
185+
function configure({ editAssetTags, cacheBuster, modulepreload }) {
186+
const mediaOptions = editAssetTags && typeof editAssetTags === 'object'
187+
? Object.assign({}, editAssetTags, modulepreload !== undefined ? { modulepreload } : {})
188+
: editAssetTags;
189+
190+
mediaService.configure(mediaOptions, cacheBuster);
187191
}
188192

189193
/**

0 commit comments

Comments
 (0)