next
Version:
The React Framework
204 lines (203 loc) • 8.19 kB
JavaScript
;
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, '&').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