diff --git a/lib/media.js b/lib/media.js index f9165f7..379701f 100644 --- a/lib/media.js +++ b/lib/media.js @@ -12,6 +12,8 @@ const _ = require('lodash'), ASYNC_SCRIPT_TAG = 'async', DEFER_SCRIPT_TAG = 'defer', ASYNC_DEFER_SCRIPT_TAG = 'async-defer', + MODULE_SCRIPT_TAG = 'module', + MODULEPRELOAD_TAG = 'modulepreload', MEDIA_DIRECTORY = path.join(process.cwd(), 'public'); /** @@ -152,6 +154,10 @@ function appendMediaToBottom(scripts, html) { function injectTags(fileArray, site, tag) { var buster = module.exports.cacheBuster ? `?version=${module.exports.cacheBuster}` : ''; + // When omitCacheBusterOnModules is enabled, content-hashed ESM files omit the + // ?version= query string — the hash in the filename already provides cache busting. + // Defaults to false so existing deployments are unaffected. + var moduleBuster = module.exports.omitCacheBusterOnModules ? '' : buster; return bluebird.resolve(_.map(fileArray, function (file) { if (tag === STYLE_TAG) { @@ -162,6 +168,12 @@ function injectTags(fileArray, site, tag) { return ``; } else if (tag === ASYNC_DEFER_SCRIPT_TAG) { return ``; + } else if (tag === MODULE_SCRIPT_TAG) { + return ``; + } else if (tag === MODULEPRELOAD_TAG) { + // tells the browser to fetch ESM scripts early, + // during HTML parsing, before reaching the `; } @@ -274,6 +286,14 @@ function getStyleFiles(state) { } } + // check for clay compile linked? clay compile font files? + const defaultFontPath = path.join(assetDir, 'css', '_linked-fonts._default.css'); + const siteFontPath = path.join(assetDir, 'css', `_linked-fonts.${siteStyleguide}.css`); + + // add any default and site font css + cssFilePaths.push(defaultFontPath); + cssFilePaths.push(siteFontPath); + return cssFilePaths .filter(files.fileExists) .map(pathJoin(assetHost, assetPath, assetDir)); @@ -370,6 +390,14 @@ function configure(options, cacheBuster = '') { if (options && _.isObject(options)) { module.exports.editStylesTags = options.styles || false; module.exports.editScriptsTags = options.scripts || false; + // modulepreload: when true, hints are injected + // into
for ESM scripts, and ?version= is omitted from module URLs + // since content-hashed filenames already provide cache busting. + // Opt-in only — defaults to false for backwards compatibility. + if (options.modulepreload !== undefined) { + module.exports.modulepreload = !!options.modulepreload; + module.exports.omitCacheBusterOnModules = !!options.modulepreload; + } } else { module.exports.editStylesTags = options; module.exports.editScriptsTags = options; @@ -404,8 +432,29 @@ function injectScriptsAndStyles(state) { mediaMap = module.exports.getMediaMap(state); // allow site to change the media map before applying it + // Expose rendered component names so resolveMedia can do per-component script resolution + // (e.g. pack-next manifest lookup) without needing a reference to the full state object. + locals._components = state._components; if (setup.resolveMedia) mediaMap = setup.resolveMedia(mediaMap, locals) || mediaMap; + // moduleScripts: ESM scripts (e.g. from esbuild pack-next) that need type="module" + const moduleScriptFiles = mediaMap.moduleScripts || []; + + mediaMap.moduleScripts = moduleScriptFiles.length + ? injectTags(moduleScriptFiles, locals.site, MODULE_SCRIPT_TAG) + : bluebird.resolve(false); + + // modulePreloads: hints for . + // Only active when configure({ modulepreload: true }) has been called — opt-in so + // sites not using the clay build pipeline are completely unaffected. + const modulePreloadFiles = module.exports.modulepreload + ? (mediaMap.modulePreloads || []) + : []; + + mediaMap.modulePreloads = modulePreloadFiles.length + ? injectTags(modulePreloadFiles, locals.site, MODULEPRELOAD_TAG) + : bluebird.resolve(false); + if (!locals.edit) { mediaMap.styles = combineFileContents(mediaMap.styles, 'public/css', '/css/', STYLE_TAG); mediaMap.scripts = !!mediaMap.manifestAssets && mediaMap.manifestAssets.length > 0 @@ -418,7 +467,11 @@ function injectScriptsAndStyles(state) { return bluebird.props(mediaMap) .then(combinedFiles => { + // modulepreload hints go first in , before CSS, so the browser can + // start fetching ESM scripts at the earliest possible moment. + html = combinedFiles.modulePreloads ? appendMediaToTop(combinedFiles.modulePreloads, html) : html; html = combinedFiles.styles ? appendMediaToTop(combinedFiles.styles, html) : html; // If there are styles, append them + html = combinedFiles.moduleScripts ? appendMediaToBottom(combinedFiles.moduleScripts, html) : html; // ESM module scripts (type="module") html = combinedFiles.scripts ? appendMediaToBottom(combinedFiles.scripts, html) : html; // If there are scripts, append them return html; // Return the compiled HTML }); @@ -431,6 +484,10 @@ module.exports.cacheBuster = ''; module.exports.configure = configure; module.exports.editStylesTags = false; module.exports.editScriptsTags = false; +// Opt-in flags for the clay build (esbuild) pipeline. +// Enable via configure({ modulepreload: true }). +module.exports.modulepreload = false; +module.exports.omitCacheBusterOnModules = false; // For testing module.exports.getManifestAssets = getManifestAssets; diff --git a/lib/render.js b/lib/render.js index fd5adb3..5be6d69 100644 --- a/lib/render.js +++ b/lib/render.js @@ -40,7 +40,7 @@ function applyRenderHooks(ref, data, locals) { * @param {Object} locals * @returns {Function} */ -function applyPostRenderHooks(ref, locals) { +function applyPostRenderHooks(ref, locals, res, self) { return (html) => { // skip postRender hooks in edit mode @@ -50,7 +50,7 @@ function applyPostRenderHooks(ref, locals) { return setup.plugins.filter((plugin) => plugin.postRender) .reduce((val, plugin) => { - return plugin.postRender(ref, html, locals); + return plugin.postRender(ref, html, locals, res, self); }, html); }; } @@ -148,7 +148,7 @@ function render(data, meta, res) { return applyRenderHooks(state._layoutRef || state.self, data, state.locals) .then(makeHtml(state)) .then(mediaService.injectScriptsAndStyles(state)) - .then(applyPostRenderHooks(state._layoutRef || state.self, state.locals)) + .then(applyPostRenderHooks(state._layoutRef || state.self, state.locals, res, meta._ref)) .then(result => { res.type('text/html'); res.send(result); @@ -182,8 +182,12 @@ function logTime(hrStart, msg, route) { }; } -function configure({ editAssetTags, cacheBuster }) { - mediaService.configure(editAssetTags, cacheBuster); +function configure({ editAssetTags, cacheBuster, modulepreload }) { + const mediaOptions = editAssetTags && typeof editAssetTags === 'object' + ? Object.assign({}, editAssetTags, modulepreload !== undefined ? { modulepreload } : {}) + : editAssetTags; + + mediaService.configure(mediaOptions, cacheBuster); } /** diff --git a/package-lock.json b/package-lock.json index 4ad7d53..45c848c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "amphora-html", - "version": "6.0.0-7", + "version": "6.0.1-dev.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "amphora-html", - "version": "6.0.0-6", + "version": "6.0.1-dev.0", "license": "MIT", "dependencies": { "amphora-fs": "^2.0.0", diff --git a/package.json b/package.json index 98d1174..9185a35 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amphora-html", - "version": "6.0.0-7", + "version": "6.0.1-dev.0", "description": "An HTML renderer for component data", "main": "index.js", "scripts": {