import type { RenderOpts } from './render' import type { HTMLElement } from 'next/dist/compiled/node-html-parser' import { OPTIMIZED_FONT_PROVIDERS } from '../shared/lib/constants' import { nonNullable } from '../lib/non-nullable' let optimizeAmp: typeof import('./optimize-amp').default | undefined let getFontDefinitionFromManifest: | typeof import('./font-utils').getFontDefinitionFromManifest | undefined let parse: typeof import('next/dist/compiled/node-html-parser').parse if (process.env.NEXT_RUNTIME !== 'edge') { optimizeAmp = require('./optimize-amp').default getFontDefinitionFromManifest = require('./font-utils').getFontDefinitionFromManifest parse = ( require('next/dist/compiled/node-html-parser') as typeof import('next/dist/compiled/node-html-parser') ).parse } type postProcessOptions = { optimizeFonts: any } type renderOptions = { getFontDefinition?: (url: string) => string } interface PostProcessMiddleware { inspect: (originalDom: HTMLElement, options: renderOptions) => any mutate: (markup: string, data: any, options: renderOptions) => Promise } type middlewareSignature = { name: string middleware: PostProcessMiddleware condition: ((options: postProcessOptions) => boolean) | null } const middlewareRegistry: Array = [] function registerPostProcessor( name: string, middleware: PostProcessMiddleware, condition?: (options: postProcessOptions) => boolean ) { middlewareRegistry.push({ name, middleware, condition: condition || null }) } async function processHTML( html: string, data: renderOptions, options: postProcessOptions ): Promise { // Don't parse unless there's at least one processor middleware if (!middlewareRegistry[0]) { return html } const root: HTMLElement = parse(html) let document = html // Calls the middleware, with some instrumentation and logging async function callMiddleWare(middleware: PostProcessMiddleware) { // 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 implements PostProcessMiddleware { inspect(originalDom: HTMLElement, options: renderOptions) { if (!options.getFontDefinition) { return } const fontDefinitions: (string | undefined)[][] = [] // collecting all the requested font definitions originalDom .querySelectorAll('link') .filter( (tag: HTMLElement) => tag.getAttribute('rel') === 'stylesheet' && tag.hasAttribute('data-href') && OPTIMIZED_FONT_PROVIDERS.some(({ url }) => { const dataHref = tag.getAttribute('data-href') return dataHref ? dataHref.startsWith(url) : false }) ) .forEach((element: HTMLElement) => { const url = element.getAttribute('data-href') const nonce = element.getAttribute('nonce') if (url) { fontDefinitions.push([url, nonce]) } }) return fontDefinitions } mutate = async ( markup: string, fontDefinitions: string[][], options: renderOptions ) => { let result = markup let preconnectUrls = new Set() if (!options.getFontDefinition) { return markup } fontDefinitions.forEach((fontDef) => { const [url, nonce] = fontDef const fallBackLinkTag = `` if ( result.indexOf(`` ) // Remove inert font tag const escapedUrl = url .replace(/&/g, '&') .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const fontRegex = new RegExp( `]*data-href="${escapedUrl}"[^>]*/>` ) result = result.replace(fontRegex, '') const provider = OPTIMIZED_FONT_PROVIDERS.find((p) => url.startsWith(p.url) ) if (provider) { preconnectUrls.add(provider.preconnect) } } }) let preconnectTag = '' preconnectUrls.forEach((url) => { preconnectTag += `` }) result = result.replace( '', preconnectTag ) return result } } async function postProcessHTML( pathname: string, content: string, renderOpts: RenderOpts, { inAmpMode, hybridAmp }: { inAmpMode: boolean; hybridAmp: boolean } ) { const postProcessors: Array<(html: string) => Promise> = [ process.env.NEXT_RUNTIME !== 'edge' && inAmpMode ? async (html: string) => { html = await optimizeAmp!(html, renderOpts.ampOptimizerConfig) if (!renderOpts.ampSkipValidation && renderOpts.ampValidator) { await renderOpts.ampValidator(html, pathname) } return html } : null, process.env.NEXT_RUNTIME !== 'edge' && renderOpts.optimizeFonts ? async (html: string) => { const getFontDefinition = (url: string): string => { if (renderOpts.fontManifest) { return getFontDefinitionFromManifest!( url, renderOpts.fontManifest ) } return '' } return await processHTML( html, { getFontDefinition }, { optimizeFonts: renderOpts.optimizeFonts, } ) } : null, process.env.NEXT_RUNTIME !== 'edge' && renderOpts.optimizeCss ? async (html: string) => { // eslint-disable-next-line import/no-extraneous-dependencies const Critters = require('critters') const cssOptimizer = new Critters({ ssrMode: true, reduceInlineStyles: false, path: renderOpts.distDir, publicPath: `${renderOpts.assetPrefix}/_next/`, preload: 'media', fonts: false, ...renderOpts.optimizeCss, }) return await cssOptimizer.process(html) } : null, inAmpMode || hybridAmp ? async (html: string) => { return html.replace(/&amp=1/g, '&=1') } : null, ].filter(nonNullable) for (const postProcessor of postProcessors) { if (postProcessor) { content = await postProcessor(content) } } return content } // 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 ) export { postProcessHTML }