UNPKG

next

Version:

The React Framework

204 lines (203 loc) • 8.19 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _escapeStringRegexp = _interopRequireDefault(require("next/dist/compiled/escape-string-regexp")); var _nodeHtmlParser = require("node-html-parser"); var _constants = require("./constants"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // const MIDDLEWARE_TIME_BUDGET = parseInt(process.env.__POST_PROCESS_MIDDLEWARE_TIME_BUDGET || '', 10) || 10 const MAXIMUM_IMAGE_PRELOADS = 2; const IMAGE_PRELOAD_SIZE_THRESHOLD = 2500; const middlewareRegistry = []; function registerPostProcessor(name, middleware, condition) { middlewareRegistry.push({ name, middleware, condition: condition || null }); } async function processHTML(html, data, options) { // Don't parse unless there's at least one processor middleware if (!middlewareRegistry[0]) { return html; } const root = (0, _nodeHtmlParser).parse(html); let document = html; // Calls the middleware, with some instrumentation and logging async function callMiddleWare(middleware) { // let timer = Date.now() const inspectData = middleware.inspect(root, data); document = await middleware.mutate(document, inspectData, data); // timer = Date.now() - timer // if (timer > MIDDLEWARE_TIME_BUDGET) { // TODO: Identify a correct upper limit for the postprocess step // and add a warning to disable the optimization // } return; } for(let i = 0; i < middlewareRegistry.length; i++){ let middleware = middlewareRegistry[i]; if (!middleware.condition || middleware.condition(options)) { await callMiddleWare(middlewareRegistry[i].middleware); } } return document; } class FontOptimizerMiddleware { inspect(originalDom, options) { if (!options.getFontDefinition) { return; } const fontDefinitions = []; // collecting all the requested font definitions originalDom.querySelectorAll('link').filter((tag)=>tag.getAttribute('rel') === 'stylesheet' && tag.hasAttribute('data-href') && _constants.OPTIMIZED_FONT_PROVIDERS.some(({ url })=>{ const dataHref = tag.getAttribute('data-href'); return dataHref ? dataHref.startsWith(url) : false; }) ).forEach((element)=>{ const url = element.getAttribute('data-href'); const nonce = element.getAttribute('nonce'); if (url) { fontDefinitions.push([ url, nonce ]); } }); return fontDefinitions; } constructor(){ this.mutate = async (markup, fontDefinitions, options)=>{ let result = markup; let preconnectUrls = new Set(); if (!options.getFontDefinition) { return markup; } fontDefinitions.forEach((fontDef)=>{ const [url, nonce] = fontDef; const fallBackLinkTag = `<link rel="stylesheet" href="${url}"/>`; if (result.indexOf(`<style data-href="${url}">`) > -1 || result.indexOf(fallBackLinkTag) > -1) { // The font is already optimized and probably the response is cached return; } const fontContent = options.getFontDefinition ? options.getFontDefinition(url) : null; if (!fontContent) { /** * In case of unreachable font definitions, fallback to default link tag. */ result = result.replace('</head>', `${fallBackLinkTag}</head>`); } else { const nonceStr = nonce ? ` nonce="${nonce}"` : ''; result = result.replace('</head>', `<style data-href="${url}"${nonceStr}>${fontContent}</style></head>`); // Remove inert font tag const escapedUrl = url.replace(/&/g, '&amp;').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const fontRegex = new RegExp(`<link[^>]*data-href="${escapedUrl}"[^>]*/>`); result = result.replace(fontRegex, ''); const provider = _constants.OPTIMIZED_FONT_PROVIDERS.find((p)=>url.startsWith(p.url) ); if (provider) { preconnectUrls.add(provider.preconnect); } } }); let preconnectTag = ''; preconnectUrls.forEach((url)=>{ preconnectTag += `<link rel="preconnect" href="${url}" crossorigin />`; }); result = result.replace('<meta name="next-font-preconnect"/>', preconnectTag); return result; }; } } class ImageOptimizerMiddleware { inspect(originalDom) { const imgPreloads = []; const imgElements = originalDom.querySelectorAll('img'); let eligibleImages = []; for(let i = 0; i < imgElements.length; i++){ if (isImgEligible(imgElements[i])) { eligibleImages.push(imgElements[i]); } if (eligibleImages.length >= MAXIMUM_IMAGE_PRELOADS) { break; } } for (const imgEl of eligibleImages){ const src = imgEl.getAttribute('src'); if (src) { imgPreloads.push(src); } } return imgPreloads; } constructor(){ this.mutate = async (markup, imgPreloads)=>{ let result = markup; let imagePreloadTags = imgPreloads.filter((imgHref)=>!preloadTagAlreadyExists(markup, imgHref) ).reduce((acc, imgHref)=>acc + `<link rel="preload" href="${imgHref}" as="image"/>` , ''); return result.replace('<meta name="next-image-preload"/>', imagePreloadTags); }; } } function isImgEligible(imgElement) { let imgSrc = imgElement.getAttribute('src'); return !!imgSrc && sourceIsSupportedType(imgSrc) && imageIsNotTooSmall(imgElement) && imageIsNotHidden(imgElement); } function preloadTagAlreadyExists(html, href) { const escapedHref = (0, _escapeStringRegexp).default(href); const regex = new RegExp(`<link[^>]*href[^>]*${escapedHref}`); return html.match(regex); } function imageIsNotTooSmall(imgElement) { // Skip images without both height and width--we don't know enough to say if // they are too small if (!(imgElement.hasAttribute('height') && imgElement.hasAttribute('width'))) { return true; } try { const heightAttr = imgElement.getAttribute('height'); const widthAttr = imgElement.getAttribute('width'); if (!heightAttr || !widthAttr) { return true; } if (parseInt(heightAttr) * parseInt(widthAttr) <= IMAGE_PRELOAD_SIZE_THRESHOLD) { return false; } } catch (err) { return true; } return true; } // Traverse up the dom from each image to see if it or any of it's // ancestors have the hidden attribute. function imageIsNotHidden(imgElement) { let activeElement = imgElement; while(activeElement.parentNode){ if (activeElement.hasAttribute('hidden')) { return false; } activeElement = activeElement.parentNode; } return true; } // Currently only filters out svg images--could be made more specific in the future. function sourceIsSupportedType(imgSrc) { return !imgSrc.includes('.svg'); } // Initialization registerPostProcessor('Inline-Fonts', new FontOptimizerMiddleware(), // Using process.env because passing Experimental flag through loader is not possible. // @ts-ignore (options)=>options.optimizeFonts || process.env.__NEXT_OPTIMIZE_FONTS ); registerPostProcessor('Preload Images', new ImageOptimizerMiddleware(), // @ts-ignore (options)=>options.optimizeImages || process.env.__NEXT_OPTIMIZE_IMAGES ); var _default = processHTML; exports.default = _default; //# sourceMappingURL=post-process.js.map