From eff6343711c32a0dd69ed03679282efdfde81394 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Mon, 15 Jun 2020 09:26:47 -0700 Subject: [PATCH 01/49] adding a font gatherer webpack plugin --- packages/next/build/webpack-config.ts | 2 + .../font-stylesheet-gathering-plugin.ts | 129 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 23dbae7c485e8..d8b185dddb1ab 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -52,6 +52,7 @@ import WebpackConformancePlugin, { } from './webpack/plugins/webpack-conformance-plugin' import { WellKnownErrorsPlugin } from './webpack/plugins/wellknown-errors-plugin' import { codeFrameColumns } from '@babel/code-frame' +import FontStylesheetGatheringPlugin from './webpack/plugins/font-stylesheet-gathering-plugin' type ExcludesFalse = (x: T | false) => x is T @@ -939,6 +940,7 @@ export default async function getBaseWebpackConfig( chunkFilename: (inputChunkName: string) => inputChunkName.replace(/\.js$/, '.module.js'), }), + !dev && !isServer && new FontStylesheetGatheringPlugin(), config.experimental.conformance && !dev && new WebpackConformancePlugin({ diff --git a/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts b/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts new file mode 100644 index 0000000000000..ead032ece1dee --- /dev/null +++ b/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts @@ -0,0 +1,129 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { NodePath } from 'ast-types/lib/node-path' +import { visit } from 'next/dist/compiled/recast' +import { compilation as CompilationType, Compiler } from 'webpack' +import { namedTypes } from 'ast-types' +import { RawSource } from 'webpack-sources' + +const https = require('https') + +interface VisitorMap { + [key: string]: (path: NodePath) => void +} + +export default class FontStylesheetGatheringPlugin { + compiler?: Compiler + gatheredStylesheets: Array = [] + + private parserHandler = ( + factory: CompilationType.NormalModuleFactory + ): void => { + const JS_TYPES = ['auto', 'esm', 'dynamic'] + // Do an extra walk per module and add interested visitors to the walk. + for (const type of JS_TYPES) { + factory.hooks.parser + .for('javascript/' + type) + .tap(this.constructor.name, (parser) => { + var that = this + parser.hooks.program.tap(this.constructor.name, (ast: any) => { + visit(ast, { + visitCallExpression: function (path) { + const { node }: { node: namedTypes.CallExpression } = path + if (!node.arguments || node.arguments.length < 2) { + return false + } + if (isNodeCreatingLinkElement(node)) { + const propsNode = node + .arguments[1] as namedTypes.ObjectExpression + if (!propsNode.properties) { + return false + } + const props: { + [key: string]: string + } = propsNode.properties.reduce( + (originalProps, prop: any) => { + // todo check the type of prop + // @ts-ignore + originalProps[prop.key.name] = prop.value.value + return originalProps + }, + {} + ) + + if (!props.href) { + return false + } + that.gatheredStylesheets.push(props.href) + } + this.traverse(path) + return false + }, + }) + }) + }) + } + } + + public apply(compiler: Compiler) { + this.compiler = compiler + compiler.hooks.normalModuleFactory.tap( + this.constructor.name, + this.parserHandler + ) + compiler.hooks.make.tapAsync(this.constructor.name, (compilation, cb) => { + compilation.hooks.finishModules.tapAsync( + this.constructor.name, + async (_, modulesFinished) => { + const allContent = this.gatheredStylesheets.map((url) => getFile(url)) + const manifestContent = (await Promise.allSettled(allContent)).map( + (promise) => promise.value + ) + compilation.assets['font-manifest.json'] = new RawSource( + JSON.stringify(manifestContent, null, ' ') + ) + modulesFinished() + } + ) + cb() + }) + } +} + +function isNodeCreatingLinkElement(node: namedTypes.CallExpression) { + const callee = node.callee as namedTypes.Identifier + if (callee.type !== 'Identifier') { + return false + } + const componentNode = node.arguments[0] as namedTypes.Literal + if (componentNode.type !== 'Literal') { + return false + } + // Next has pragma: __jsx. + return callee.name === '__jsx' && componentNode.value === 'link' +} + +function getFile(url: string): Promise { + return new Promise((resolve) => { + let rawData: any = '' + https.get( + url, + { + headers: { + 'user-agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36', + }, + }, + (res: any) => { + res.on('data', (chunk: any) => { + rawData += chunk + }) + res.on('end', () => { + resolve({ + url, + content: rawData.toString('utf8'), + }) + }) + } + ) + }) +} From 9ed7962c30dc90c7e4b307f5dc8cbfe96aba7169 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Mon, 15 Jun 2020 09:26:58 -0700 Subject: [PATCH 02/49] fixing dep --- packages/next/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/package.json b/packages/next/package.json index 12c813828e9e5..95185777600ff 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -78,6 +78,7 @@ "@babel/types": "7.9.6", "@next/react-dev-overlay": "9.4.5-canary.7", "@next/react-refresh-utils": "9.4.5-canary.7", + "ast-types": "0.13.2", "babel-plugin-syntax-jsx": "6.18.0", "babel-plugin-transform-define": "2.0.0", "babel-plugin-transform-react-remove-prop-types": "0.4.24", @@ -157,7 +158,6 @@ "@zeit/ncc": "0.22.0", "amphtml-validator": "1.0.31", "arg": "4.1.0", - "ast-types": "0.13.2", "async-retry": "1.2.3", "async-sema": "3.0.0", "babel-loader": "8.1.0", From 3b85140e29f4f8dafa33ff2aa54051fe56176d6e Mon Sep 17 00:00:00 2001 From: atcastle Date: Thu, 18 Jun 2020 13:39:06 -0700 Subject: [PATCH 03/49] Initial commit with framework for post processing middleware --- packages/next/next-server/lib/post-process.ts | 104 ++++++++++++++++++ packages/next/next-server/server/config.ts | 1 + .../next/next-server/server/next-server.ts | 2 + packages/next/next-server/server/render.tsx | 4 + packages/next/package.json | 2 + 5 files changed, 113 insertions(+) create mode 100644 packages/next/next-server/lib/post-process.ts diff --git a/packages/next/next-server/lib/post-process.ts b/packages/next/next-server/lib/post-process.ts new file mode 100644 index 0000000000000..f4d2dabc60347 --- /dev/null +++ b/packages/next/next-server/lib/post-process.ts @@ -0,0 +1,104 @@ +import { parse, HTMLElement } from 'node-html-parser' + +const MIDDLEWARE_TIME_BUDGET = 10 + +type postProcessOptions = { + preloadImages: boolean +} + +type postProcessData = { + preloads: { + images: Array + } +} + +type postProcessMiddleware = ( + htmlRoot: HTMLElement, + data: postProcessData +) => 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, + options: postProcessOptions +): Promise { + // Don't parse unless there's at least one processor middleware + if (!middlewareRegistry[0]) { + return html + } + const data: postProcessData = { + preloads: { + images: [], + }, + } + const root: HTMLElement = parse(html) + + // Calls the middleware, with some instrumentation and logging + async function callMiddleWare( + middleware: postProcessMiddleware, + name: string + ) { + let timer = Date.now() + await middleware(root, data) + timer = Date.now() - timer + if (timer > MIDDLEWARE_TIME_BUDGET) { + console.warn( + `The postprocess middleware "${name}" took ${timer}ms to complete. This is longer than the ${MIDDLEWARE_TIME_BUDGET} limit.` + ) + } + return + } + + for (let i = 0; i < middlewareRegistry.length; i++) { + let middleware = middlewareRegistry[i] + if (!middleware.condition || middleware.condition(options)) { + await callMiddleWare( + middlewareRegistry[i].middleware, + middlewareRegistry[i].name + ) + } + } + + return root.toString() +} + +// Middleware +const findImages: postProcessMiddleware = async (htmlRoot, data) => { + // TODO: Image preload finding logic here--adds to data + console.log(htmlRoot, data) + return +} + +const renderPreloads: postProcessMiddleware = async (htmlRoot, data) => { + // TODO: Render preload tags from data + console.log(htmlRoot, data) + return +} + +// Initialization +registerPostProcessor( + 'Find-Images', + findImages, + (options) => options.preloadImages +) +registerPostProcessor( + 'Render-Preloads', + renderPreloads, + (options) => options.preloadImages +) + +export default processHTML diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts index 9e0487e29c5ab..43ae8f6b36b0d 100644 --- a/packages/next/next-server/server/config.ts +++ b/packages/next/next-server/server/config.ts @@ -53,6 +53,7 @@ const defaultConfig: { [key: string]: any } = { pageEnv: false, productionBrowserSourceMaps: false, optionalCatchAll: false, + postProcessOptimize: false, }, future: { excludeDefaultMomentLocales: false, diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index babd4aa96b94e..e592ee7beaa71 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -122,6 +122,7 @@ export default class Server { customServer?: boolean ampOptimizerConfig?: { [key: string]: any } basePath: string + postProcess: boolean } private compression?: Middleware private onErrorMiddleware?: ({ err }: { err: Error }) => Promise @@ -174,6 +175,7 @@ export default class Server { customServer: customServer === true ? true : undefined, ampOptimizerConfig: this.nextConfig.experimental.amp?.optimizer, basePath: this.nextConfig.experimental.basePath, + postProcess: this.nextConfig.experimental.postProcessOptimization, } // Only the `publicRuntimeConfig` key is exposed to the client side diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index e0c7329c9ab1c..b9bbcc14a7caf 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -42,6 +42,7 @@ import { LoadComponentsReturnType, ManifestItem } from './load-components' import optimizeAmp from './optimize-amp' import { UnwrapPromise } from '../../lib/coalesced-function' import { GetStaticProps, GetServerSideProps } from '../../types' +import postProcess from '../lib/post-process' function noRouter() { const message = @@ -151,6 +152,7 @@ export type RenderOptsPartial = { ampValidator?: (html: string, pathname: string) => Promise ampSkipValidation?: boolean ampOptimizerConfig?: { [key: string]: any } + postProcess: boolean isDataReq?: boolean params?: ParsedUrlQuery previewProps: __ApiPreviewProps @@ -770,6 +772,8 @@ export async function renderToHTML( } } + html = await postProcess(html, { preloadImages: renderOpts.postProcess }) + if (inAmpMode || hybridAmp) { // fix & being escaped for amphtml rel link html = html.replace(/&amp=1/g, '&=1') diff --git a/packages/next/package.json b/packages/next/package.json index 12c813828e9e5..fa41338c3efe6 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -92,6 +92,7 @@ "mkdirp": "0.5.3", "native-url": "0.3.1", "neo-async": "2.6.1", + "node-html-parser": "^1.2.19", "pnp-webpack-plugin": "1.6.4", "postcss": "7.0.29", "prop-types": "15.7.2", @@ -193,6 +194,7 @@ "lru-cache": "5.1.1", "nanoid": "2.0.3", "node-fetch": "2.6.0", + "node-html-parser": "1.2.19", "ora": "4.0.4", "path-to-regexp": "6.1.0", "postcss-flexbugs-fixes": "4.2.1", From d5ed404747d1190b69a59263ede79666457bd48c Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 23 Jun 2020 14:18:30 -0700 Subject: [PATCH 04/49] generating the style tags --- packages/next/next-server/lib/post-process.ts | 34 +++++++++++++++++-- yarn.lock | 12 +++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/next/next-server/lib/post-process.ts b/packages/next/next-server/lib/post-process.ts index f4d2dabc60347..4a95a3842755c 100644 --- a/packages/next/next-server/lib/post-process.ts +++ b/packages/next/next-server/lib/post-process.ts @@ -15,7 +15,7 @@ type postProcessData = { type postProcessMiddleware = ( htmlRoot: HTMLElement, data: postProcessData -) => Promise +) => Promise type middlewareSignature = { name: string middleware: postProcessMiddleware @@ -45,7 +45,7 @@ async function processHTML( images: [], }, } - const root: HTMLElement = parse(html) + let root: HTMLElement = parse(html) // Calls the middleware, with some instrumentation and logging async function callMiddleWare( @@ -53,7 +53,7 @@ async function processHTML( name: string ) { let timer = Date.now() - await middleware(root, data) + root = await middleware(root, data) timer = Date.now() - timer if (timer > MIDDLEWARE_TIME_BUDGET) { console.warn( @@ -83,6 +83,32 @@ const findImages: postProcessMiddleware = async (htmlRoot, data) => { return } +// Middleware +const inlineFonts: postProcessMiddleware = async (htmlRoot) => { + const links = htmlRoot + .querySelectorAll('link') + .filter( + (tag) => + tag.getAttribute('rel') === 'stylesheet' && + tag.hasAttribute('href') && + tag + .getAttribute('href') + .startsWith('https://fonts.googleapis.com/css2?') + ) + links.forEach((link) => { + link.insertAdjacentHTML( + 'afterend', + `` + ) + /** + * Removing the actual element is not supported in node-html-parser + * so we just remove the href effectively making it inert. + */ + link.removeAttribute('href') + }) + return htmlRoot +} + const renderPreloads: postProcessMiddleware = async (htmlRoot, data) => { // TODO: Render preload tags from data console.log(htmlRoot, data) @@ -95,6 +121,8 @@ registerPostProcessor( findImages, (options) => options.preloadImages ) +// Initialization +registerPostProcessor('Inline-Fonts', inlineFonts, () => true) registerPostProcessor( 'Render-Preloads', renderPreloads, diff --git a/yarn.lock b/yarn.lock index 34d8f3355af81..acc1f5360fb4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7753,6 +7753,11 @@ hawk@~6.0.2: hoek "4.x.x" sntp "2.x.x" +he@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= + header-case@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/header-case/-/header-case-1.0.1.tgz#9535973197c144b09613cd65d317ef19963bd02d" @@ -10777,6 +10782,13 @@ node-gyp@^4.0.0: tar "^4.4.8" which "1" +node-html-parser@^1.2.19: + version "1.2.19" + resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-1.2.19.tgz#2cb14ce7981dfe2c0f5af53cf8654a3d49cded7d" + integrity sha512-MQvBz+qk7SbqNPp0c7hR0F8lRTPXK5n2tww4eFmXf+cXp5hZHtL5rJHlAWlcjzRep+T5Pd5lz3lqFgN7IFYEiw== + dependencies: + he "1.1.1" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" From 39fcfcded646c4b9c904b16e469e8d12b5f94c2c Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Wed, 24 Jun 2020 15:29:35 -0700 Subject: [PATCH 05/49] WIP font optimizations --- packages/next/export/worker.ts | 21 +++++- packages/next/next-server/lib/constants.ts | 1 + packages/next/next-server/lib/post-process.ts | 69 ++++++++++++++----- .../next/next-server/server/next-server.ts | 11 ++- packages/next/next-server/server/render.tsx | 14 +++- packages/next/next-server/server/require.ts | 6 ++ 6 files changed, 101 insertions(+), 21 deletions(-) diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index 96f51ecb6e511..5be1f7bf8d178 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -13,6 +13,7 @@ import 'next/dist/next-server/server/node-polyfill-fetch' import { IncomingMessage, ServerResponse } from 'http' import { ComponentType } from 'react' import { GetStaticProps } from '../types' +import { requireFontManifest } from '../next-server/server/require' const envConfig = require('../next-server/lib/runtime-config') @@ -61,6 +62,7 @@ interface RenderOpts { ampSkipValidation?: boolean hybridAmp?: boolean inAmpMode?: boolean + getFontDefinition?: (url: string) => string } type ComponentModule = ComponentType<{}> & { @@ -254,7 +256,24 @@ export default async function exportPage({ html = components.Component queryWithAutoExportWarn() } else { - curRenderOpts = { ...components, ...renderOpts, ampPath, params } + const getFontDefinition = (fontURL: string) => { + const manifest = requireFontManifest(distDir) + let fontContent = '' + manifest.forEach((font: any) => { + if (font && font.url === fontURL) { + fontContent = font.content + } + }) + + return fontContent + } + curRenderOpts = { + ...components, + ...renderOpts, + ampPath, + params, + getFontDefinition, + } // @ts-ignore html = await renderMethod(req, res, page, query, curRenderOpts) } diff --git a/packages/next/next-server/lib/constants.ts b/packages/next/next-server/lib/constants.ts index 159efa0c30902..a4edd8cd28904 100644 --- a/packages/next/next-server/lib/constants.ts +++ b/packages/next/next-server/lib/constants.ts @@ -9,6 +9,7 @@ export const EXPORT_DETAIL = 'export-detail.json' export const PRERENDER_MANIFEST = 'prerender-manifest.json' export const ROUTES_MANIFEST = 'routes-manifest.json' export const REACT_LOADABLE_MANIFEST = 'react-loadable-manifest.json' +export const FONT_MANIFEST = 'font-manifest.json' export const SERVER_DIRECTORY = 'server' export const SERVERLESS_DIRECTORY = 'serverless' export const CONFIG_FILE = 'next.config.js' diff --git a/packages/next/next-server/lib/post-process.ts b/packages/next/next-server/lib/post-process.ts index 4a95a3842755c..a37e5a1a758a8 100644 --- a/packages/next/next-server/lib/post-process.ts +++ b/packages/next/next-server/lib/post-process.ts @@ -4,6 +4,11 @@ const MIDDLEWARE_TIME_BUDGET = 10 type postProcessOptions = { preloadImages: boolean + optimizeFonts: boolean +} + +type renderOpts = { + getFontDefinition?: (url: string) => string } type postProcessData = { @@ -14,8 +19,11 @@ type postProcessData = { type postProcessMiddleware = ( htmlRoot: HTMLElement, - data: postProcessData -) => Promise + rawString: string, + data: postProcessData, + options: renderOpts +) => Promise + type middlewareSignature = { name: string middleware: postProcessMiddleware @@ -34,26 +42,27 @@ function registerPostProcessor( async function processHTML( html: string, + data: renderOpts, options: postProcessOptions ): Promise { // Don't parse unless there's at least one processor middleware if (!middlewareRegistry[0]) { return html } - const data: postProcessData = { + const postProcessData: postProcessData = { preloads: { images: [], }, } - let root: HTMLElement = parse(html) - + const root: HTMLElement = parse(html) + let document = html // Calls the middleware, with some instrumentation and logging async function callMiddleWare( middleware: postProcessMiddleware, name: string ) { let timer = Date.now() - root = await middleware(root, data) + document = await middleware(root, document, postProcessData, data) timer = Date.now() - timer if (timer > MIDDLEWARE_TIME_BUDGET) { console.warn( @@ -73,18 +82,28 @@ async function processHTML( } } - return root.toString() + return document } // Middleware -const findImages: postProcessMiddleware = async (htmlRoot, data) => { +const findImages: postProcessMiddleware = async (htmlRoot, document, data) => { // TODO: Image preload finding logic here--adds to data console.log(htmlRoot, data) - return + return document } // Middleware -const inlineFonts: postProcessMiddleware = async (htmlRoot) => { +const inlineFonts: postProcessMiddleware = async ( + htmlRoot, + document, + _data, + options +) => { + if (!options.getFontDefinition) { + return htmlRoot + } + + const getFontDefinition = options.getFontDefinition const links = htmlRoot .querySelectorAll('link') .filter( @@ -96,23 +115,33 @@ const inlineFonts: postProcessMiddleware = async (htmlRoot) => { .startsWith('https://fonts.googleapis.com/css2?') ) links.forEach((link) => { - link.insertAdjacentHTML( - 'afterend', - `` + const url = link.getAttribute('href') + console.log(document, '...', `', + `` ) /** * Removing the actual element is not supported in node-html-parser * so we just remove the href effectively making it inert. */ - link.removeAttribute('href') + //link.removeAttribute('href') }) - return htmlRoot + return document } -const renderPreloads: postProcessMiddleware = async (htmlRoot, data) => { +const renderPreloads: postProcessMiddleware = async ( + htmlRoot, + document, + data +) => { // TODO: Render preload tags from data console.log(htmlRoot, data) - return + return document } // Initialization @@ -122,7 +151,11 @@ registerPostProcessor( (options) => options.preloadImages ) // Initialization -registerPostProcessor('Inline-Fonts', inlineFonts, () => true) +registerPostProcessor( + 'Inline-Fonts', + inlineFonts, + (options) => options.optimizeFonts || true +) registerPostProcessor( 'Render-Preloads', renderPreloads, diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index e592ee7beaa71..8dad3d58611b6 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -42,7 +42,7 @@ import { recursiveReadDirSync } from './lib/recursive-readdir-sync' import { loadComponents, LoadComponentsReturnType } from './load-components' import { normalizePagePath } from './normalize-page-path' import { RenderOpts, RenderOptsPartial, renderToHTML } from './render' -import { getPagePath } from './require' +import { getPagePath, requireFontManifest } from './require' import Router, { DynamicRoutes, PageChecker, @@ -123,6 +123,8 @@ export default class Server { ampOptimizerConfig?: { [key: string]: any } basePath: string postProcess: boolean + optimizeFonts: boolean + getFontDefinition: (url: string) => string } private compression?: Middleware private onErrorMiddleware?: ({ err }: { err: Error }) => Promise @@ -176,6 +178,8 @@ export default class Server { ampOptimizerConfig: this.nextConfig.experimental.amp?.optimizer, basePath: this.nextConfig.experimental.basePath, postProcess: this.nextConfig.experimental.postProcessOptimization, + optimizeFonts: this.nextConfig.experimental.fontOptimization, + getFontDefinition: this.getFontDefinition, } // Only the `publicRuntimeConfig` key is exposed to the client side @@ -314,6 +318,11 @@ export default class Server { return (this._cachedPreviewManifest = manifest) } + protected getFontDefinition(url: string): string { + requireFontManifest(this.distDir) + return url + 'server-blah' + } + protected getPreviewProps(): __ApiPreviewProps { return this.getPrerenderManifest().preview } diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index b9bbcc14a7caf..8b72e27a97a7b 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -153,11 +153,13 @@ export type RenderOptsPartial = { ampSkipValidation?: boolean ampOptimizerConfig?: { [key: string]: any } postProcess: boolean + optimizeFonts: boolean isDataReq?: boolean params?: ParsedUrlQuery previewProps: __ApiPreviewProps basePath: string unstable_runtimeJS?: false + getFontDefinition?: (url: string) => string } export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial @@ -293,6 +295,7 @@ export async function renderToHTML( params, previewProps, basePath, + getFontDefinition, } = renderOpts const callMiddleware = async (method: string, args: any[], props = false) => { @@ -772,7 +775,16 @@ export async function renderToHTML( } } - html = await postProcess(html, { preloadImages: renderOpts.postProcess }) + html = await postProcess( + html, + { + getFontDefinition, + }, + { + preloadImages: renderOpts.postProcess, + optimizeFonts: renderOpts.optimizeFonts, + } + ) if (inAmpMode || hybridAmp) { // fix & being escaped for amphtml rel link diff --git a/packages/next/next-server/server/require.ts b/packages/next/next-server/server/require.ts index 5e59f15d0167e..fe34b80f4e146 100644 --- a/packages/next/next-server/server/require.ts +++ b/packages/next/next-server/server/require.ts @@ -4,6 +4,7 @@ import { PAGES_MANIFEST, SERVER_DIRECTORY, SERVERLESS_DIRECTORY, + FONT_MANIFEST, } from '../lib/constants' import { normalizePagePath, denormalizePagePath } from './normalize-page-path' import { PagesManifest } from '../../build/webpack/plugins/pages-manifest-plugin' @@ -54,3 +55,8 @@ export function requirePage( } return require(pagePath) } + +export function requireFontManifest(distDir: string) { + const manifest = require(join(distDir, FONT_MANIFEST)) + return manifest +} From 8a932d1f02e7e4fd55e80812d1fae1235cd9379c Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Thu, 25 Jun 2020 01:17:42 -0700 Subject: [PATCH 06/49] working static pages font replacement --- packages/next/next-server/lib/head.tsx | 8 + packages/next/next-server/lib/post-process.ts | 201 ++++++++++++------ 2 files changed, 144 insertions(+), 65 deletions(-) diff --git a/packages/next/next-server/lib/head.tsx b/packages/next/next-server/lib/head.tsx index 9741566b22e61..c6edbdf71149f 100644 --- a/packages/next/next-server/lib/head.tsx +++ b/packages/next/next-server/lib/head.tsx @@ -136,6 +136,14 @@ function reduceComponents( .reverse() .map((c: React.ReactElement, i: number) => { const key = c.key || i + if ( + c.type === 'link' && + c.props['href'] && + c.props['href'].startsWith('https://fonts.googleapis.com/css2?') + ) { + c.props['data-href'] = c.props['href'] + delete c.props['href'] + } return React.cloneElement(c, { key }) }) } diff --git a/packages/next/next-server/lib/post-process.ts b/packages/next/next-server/lib/post-process.ts index a37e5a1a758a8..9a93790ccdf06 100644 --- a/packages/next/next-server/lib/post-process.ts +++ b/packages/next/next-server/lib/post-process.ts @@ -17,16 +17,22 @@ type postProcessData = { } } -type postProcessMiddleware = ( - htmlRoot: HTMLElement, - rawString: string, - data: postProcessData, - options: renderOpts -) => Promise +interface PostProcessMiddleware { + inspect: ( + originalDom: HTMLElement, + data: postProcessData, + options: renderOpts + ) => void + mutate: ( + markup: string, + data: postProcessData, + options: renderOpts + ) => Promise +} type middlewareSignature = { name: string - middleware: postProcessMiddleware + middleware: PostProcessMiddleware condition: ((options: postProcessOptions) => boolean) | null } @@ -34,7 +40,7 @@ const middlewareRegistry: Array = [] function registerPostProcessor( name: string, - middleware: postProcessMiddleware, + middleware: PostProcessMiddleware, condition?: (options: postProcessOptions) => boolean ) { middlewareRegistry.push({ name, middleware, condition: condition || null }) @@ -58,11 +64,12 @@ async function processHTML( let document = html // Calls the middleware, with some instrumentation and logging async function callMiddleWare( - middleware: postProcessMiddleware, + middleware: PostProcessMiddleware, name: string ) { let timer = Date.now() - document = await middleware(root, document, postProcessData, data) + middleware.inspect(root, postProcessData, data) + document = await middleware.mutate(document, postProcessData, data) timer = Date.now() - timer if (timer > MIDDLEWARE_TIME_BUDGET) { console.warn( @@ -85,80 +92,144 @@ async function processHTML( return document } -// Middleware -const findImages: postProcessMiddleware = async (htmlRoot, document, data) => { - // TODO: Image preload finding logic here--adds to data - console.log(htmlRoot, data) - return document +class FindImages implements PostProcessMiddleware { + inspect( + _originalDom: HTMLElement, + _data: postProcessData, + _options: renderOpts + ) { + return + } + mutate = async ( + markup: string, + _data: postProcessData, + _options: renderOpts + ) => { + return markup + } } -// Middleware -const inlineFonts: postProcessMiddleware = async ( - htmlRoot, - document, - _data, - options -) => { - if (!options.getFontDefinition) { - return htmlRoot - } +class FontOptimizerMiddleware implements PostProcessMiddleware { + fontDefinitions: { + [key: string]: string + } = {} - const getFontDefinition = options.getFontDefinition - const links = htmlRoot - .querySelectorAll('link') - .filter( - (tag) => - tag.getAttribute('rel') === 'stylesheet' && - tag.hasAttribute('href') && - tag - .getAttribute('href') - .startsWith('https://fonts.googleapis.com/css2?') - ) - links.forEach((link) => { - const url = link.getAttribute('href') - console.log(document, '...', `', - `` - ) - /** - * Removing the actual element is not supported in node-html-parser - * so we just remove the href effectively making it inert. - */ - //link.removeAttribute('href') - }) - return document + inspect( + originalDom: HTMLElement, + _data: postProcessData, + options: renderOpts + ) { + if (!options.getFontDefinition) { + return + } + const getFontDefinition = options.getFontDefinition + // collecting all the requested font definitions + originalDom + .querySelectorAll('link') + .filter( + (tag: HTMLElement) => + tag.getAttribute('rel') === 'stylesheet' && + tag.hasAttribute('data-href') && + tag + .getAttribute('data-href') + .startsWith('https://fonts.googleapis.com/css2?') + ) + .forEach((element: HTMLElement) => { + const url = element.getAttribute('data-href') + this.fontDefinitions[url] = getFontDefinition(url) + }) + } + mutate = async ( + markup: string, + _data: postProcessData, + _options: renderOpts + ) => { + let result = markup + for (const key in this.fontDefinitions) { + result = result.replace( + '', + `` + ) + } + return result + } } -const renderPreloads: postProcessMiddleware = async ( - htmlRoot, - document, - data -) => { - // TODO: Render preload tags from data - console.log(htmlRoot, data) - return document +// Middleware +// const inlineFonts: postProcessMiddleware = async ( +// htmlRoot, +// document, +// _data, +// options +// ) => { +// if (!options.getFontDefinition) { +// return htmlRoot +// } + +// const getFontDefinition = options.getFontDefinition +// const links = htmlRoot +// .querySelectorAll('link') +// .filter( +// (tag) => +// tag.getAttribute('rel') === 'stylesheet' && +// tag.hasAttribute('href') && +// tag +// .getAttribute('href') +// .startsWith('https://fonts.googleapis.com/css2?') +// ) +// links.forEach((link) => { +// const url = link.getAttribute('href') +// console.log(document, '...', `', +// `` +// ) +// /** +// * Removing the actual element is not supported in node-html-parser +// * so we just remove the href effectively making it inert. +// */ +// //link.removeAttribute('href') +// }) +// return document +// } + +class RenderPreloads implements PostProcessMiddleware { + inspect = ( + _originalDom: HTMLElement, + _data: postProcessData, + _options: renderOpts + ) => {} + mutate = async ( + markup: string, + _data: postProcessData, + _options: renderOpts + ) => { + return markup + } } // Initialization registerPostProcessor( 'Find-Images', - findImages, + new FindImages(), (options) => options.preloadImages ) // Initialization registerPostProcessor( 'Inline-Fonts', - inlineFonts, + new FontOptimizerMiddleware(), (options) => options.optimizeFonts || true ) registerPostProcessor( 'Render-Preloads', - renderPreloads, + new RenderPreloads(), (options) => options.preloadImages ) From 126f904a22c459338425bc969a90bf456b3ecebe Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Thu, 25 Jun 2020 10:46:08 -0700 Subject: [PATCH 07/49] minifying stylesheets --- packages/next/next-server/lib/post-process.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/next-server/lib/post-process.ts b/packages/next/next-server/lib/post-process.ts index 9a93790ccdf06..ce5cbc3d0241a 100644 --- a/packages/next/next-server/lib/post-process.ts +++ b/packages/next/next-server/lib/post-process.ts @@ -149,7 +149,7 @@ class FontOptimizerMiddleware implements PostProcessMiddleware { result = result.replace( '', `` ) From d5137c39ef521d443b402214cb5134f5df4fed23 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 30 Jun 2020 12:00:47 -0700 Subject: [PATCH 08/49] WIP font-optimization changes --- packages/next/build/webpack-config.ts | 2 +- .../webpack/loaders/next-serverless-loader.ts | 12 ++++++++ .../font-stylesheet-gathering-plugin.ts | 11 +++++++- packages/next/export/worker.ts | 28 +++++++++++-------- packages/next/next-server/lib/head.tsx | 8 ------ packages/next/next-server/lib/post-process.ts | 3 +- .../next/next-server/server/next-server.ts | 2 +- packages/next/next-server/server/require.ts | 10 +++++-- packages/next/pages/_document.tsx | 16 ++++++++++- 9 files changed, 64 insertions(+), 28 deletions(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 998a7e23cfc5f..4d983fbdf3246 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -954,7 +954,7 @@ export default async function getBaseWebpackConfig( inputChunkName.replace(/\.js$/, '.module.js'), }) })(), - !dev && !isServer && new FontStylesheetGatheringPlugin(), + !dev && isServer && new FontStylesheetGatheringPlugin(), config.experimental.conformance && !dev && new WebpackConformancePlugin({ diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index b78825641936b..d9a78c7437810 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -251,6 +251,7 @@ const nextServerlessLoader: loader.Loader = function () { import initServer from 'next-plugin-loader?middleware=on-init-server!' import onError from 'next-plugin-loader?middleware=on-error-server!' import 'next/dist/next-server/server/node-polyfill-fetch' + import { requireFontManifest } from 'next/dist/next-server/server/require' const {isResSent} = require('next/dist/next-server/lib/utils'); ${envLoading} @@ -401,7 +402,18 @@ const nextServerlessLoader: loader.Loader = function () { // make sure to set renderOpts to the correct params e.g. _params // if provided from worker or params if we're parsing them here renderOpts.params = _params || params + renderOpts.getFontDefinition = (fontURL) => { + console.log('will dist dirrrr', "${distDir}", "${canonicalBase}", "${basePath}"); + const manifest = requireFontManifest("${distDir}", true) + let fontContent = '' + manifest.forEach((font) => { + if (font && font.url === fontURL) { + fontContent = font.content + } + }) + return fontContent + } const isFallback = parsedUrl.query.__nextFallback const previewData = tryGetPreviewData(req, res, options.previewProps) diff --git a/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts b/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts index ead032ece1dee..8b50a820e21f6 100644 --- a/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts +++ b/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts @@ -26,6 +26,10 @@ export default class FontStylesheetGatheringPlugin { .tap(this.constructor.name, (parser) => { var that = this parser.hooks.program.tap(this.constructor.name, (ast: any) => { + // We will only optimize fonts from first party code. + if (parser?.state?.module?.resource.includes('node_modules')) { + return + } visit(ast, { visitCallExpression: function (path) { const { node }: { node: namedTypes.CallExpression } = path @@ -50,7 +54,12 @@ export default class FontStylesheetGatheringPlugin { {} ) - if (!props.href) { + if ( + !props.rel || + props.rel !== 'stylesheet' || + !props.href || + !props.href.startsWith('https://') + ) { return false } that.gatheredStylesheets.push(props.href) diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index d33921d16ec55..d8be1d53f961c 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -85,6 +85,18 @@ export default async function exportPage({ ampValidations: [], } + const getFontDefinition = (fontURL: string) => { + const manifest = requireFontManifest(distDir, serverless) + let fontContent = '' + manifest.forEach((font: any) => { + if (font && font.url === fontURL) { + fontContent = font.content + } + }) + + return fontContent + } + try { const { query: originalQuery = {} } = pathMap const { page } = pathMap @@ -215,7 +227,10 @@ export default async function exportPage({ 'export', { ampPath }, // @ts-ignore - params + { + ...params, + getFontDefinition, + } ) curRenderOpts = result.renderOpts || {} html = result.html @@ -248,17 +263,6 @@ export default async function exportPage({ html = components.Component queryWithAutoExportWarn() } else { - const getFontDefinition = (fontURL: string) => { - const manifest = requireFontManifest(distDir) - let fontContent = '' - manifest.forEach((font: any) => { - if (font && font.url === fontURL) { - fontContent = font.content - } - }) - - return fontContent - } curRenderOpts = { ...components, ...renderOpts, diff --git a/packages/next/next-server/lib/head.tsx b/packages/next/next-server/lib/head.tsx index f3e759507108e..4b03b2fc05847 100644 --- a/packages/next/next-server/lib/head.tsx +++ b/packages/next/next-server/lib/head.tsx @@ -136,14 +136,6 @@ function reduceComponents( .reverse() .map((c: React.ReactElement, i: number) => { const key = c.key || i - if ( - c.type === 'link' && - c.props['href'] && - c.props['href'].startsWith('https://fonts.googleapis.com/css2?') - ) { - c.props['data-href'] = c.props['href'] - delete c.props['href'] - } return React.cloneElement(c, { key }) }) } diff --git a/packages/next/next-server/lib/post-process.ts b/packages/next/next-server/lib/post-process.ts index ce5cbc3d0241a..f0287803d7bcb 100644 --- a/packages/next/next-server/lib/post-process.ts +++ b/packages/next/next-server/lib/post-process.ts @@ -120,6 +120,7 @@ class FontOptimizerMiddleware implements PostProcessMiddleware { options: renderOpts ) { if (!options.getFontDefinition) { + console.log('early') return } const getFontDefinition = options.getFontDefinition @@ -132,7 +133,7 @@ class FontOptimizerMiddleware implements PostProcessMiddleware { tag.hasAttribute('data-href') && tag .getAttribute('data-href') - .startsWith('https://fonts.googleapis.com/css2?') + .startsWith('https://fonts.googleapis.com/css') ) .forEach((element: HTMLElement) => { const url = element.getAttribute('data-href') diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index e7ca46be2f14d..009b5eeb5e3aa 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -317,7 +317,7 @@ export default class Server { } protected getFontDefinition(url: string): string { - requireFontManifest(this.distDir) + requireFontManifest(this.distDir, false) return url + 'server-blah' } diff --git a/packages/next/next-server/server/require.ts b/packages/next/next-server/server/require.ts index fe34b80f4e146..28fca96a2cbc0 100644 --- a/packages/next/next-server/server/require.ts +++ b/packages/next/next-server/server/require.ts @@ -56,7 +56,11 @@ export function requirePage( return require(pagePath) } -export function requireFontManifest(distDir: string) { - const manifest = require(join(distDir, FONT_MANIFEST)) - return manifest +export function requireFontManifest(distDir: string, serverless: boolean) { + const serverBuildPath = join( + distDir, + serverless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY + ) + const fontManifest = require(join(serverBuildPath, FONT_MANIFEST)) + return fontManifest } diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index 69019a9bd792e..4a39ba1b89b4b 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -382,7 +382,21 @@ export class Head extends Component< )} - {children} + + { + /// @ts-ignore + children.map((c) => { + if ( + c.type === 'link' && + c.props['href'] && + c.props['href'].startsWith('https://fonts.googleapis.com/css?') + ) { + c.props['data-href'] = c.props['href'] + delete c.props['href'] + } + return c + }) + } {head} Date: Tue, 30 Jun 2020 12:10:03 -0700 Subject: [PATCH 09/49] adding comments --- packages/next/pages/_document.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index ed68f06f863f7..b76940aacd637 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -366,17 +366,17 @@ export class Head extends Component< )} { + // Remove the fonts stylesheet tag here, this content would be inlined by post process. /// @ts-ignore - children.map((c) => { + children.filter((c) => { if ( c.type === 'link' && c.props['href'] && - c.props['href'].startsWith('https://fonts.googleapis.com/css?') + c.props['href'].startsWith('https://fonts.googleapis.com/css') ) { - c.props['data-href'] = c.props['href'] - delete c.props['href'] + return false } - return c + return true }) } {head} From 2f45a7b5aaa6d316a4245d757f0d5e9d6f3cfd1c Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 30 Jun 2020 12:12:06 -0700 Subject: [PATCH 10/49] switching from filter to map --- packages/next/pages/_document.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index b76940aacd637..905d06d5511e3 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -368,15 +368,16 @@ export class Head extends Component< { // Remove the fonts stylesheet tag here, this content would be inlined by post process. /// @ts-ignore - children.filter((c) => { + children.map((c) => { if ( c.type === 'link' && c.props['href'] && - c.props['href'].startsWith('https://fonts.googleapis.com/css') + c.props['href'].startsWith('https://fonts.googleapis.com/css?') ) { - return false + c.props['data-href'] = c.props['href'] + delete c.props['href'] } - return true + return c }) } {head} From 6f9e1e7e37669475dae76dbf0769b604430726c6 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 30 Jun 2020 12:26:43 -0700 Subject: [PATCH 11/49] fixing server path --- packages/next/next-server/server/next-server.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index 932e697b367dc..463ffc676df04 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -312,9 +312,16 @@ export default class Server { return (this._cachedPreviewManifest = manifest) } - protected getFontDefinition(url: string): string { - requireFontManifest(this.distDir, false) - return url + 'server-blah' + protected getFontDefinition(fontURL: string): string { + const manifest = requireFontManifest(this.distDir, false) + let fontContent = '' + manifest.forEach((font: any) => { + if (font && font.url === fontURL) { + fontContent = font.content + } + }) + + return fontContent } protected getPreviewProps(): __ApiPreviewProps { From 210a5cf4531635a3acd4364b1e36d94b8326c10e Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 30 Jun 2020 16:01:13 -0700 Subject: [PATCH 12/49] Fixing serverless builds --- .../webpack/loaders/next-serverless-loader.ts | 13 ------ packages/next/export/worker.ts | 19 ++------- packages/next/next-server/lib/head.tsx | 8 ++++ packages/next/next-server/lib/post-process.ts | 37 ++++++++-------- .../next/next-server/server/font-utils.ts | 42 +++++++++++++++++++ .../next/next-server/server/next-server.ts | 17 ++------ packages/next/next-server/server/render.tsx | 16 ++++++- packages/next/pages/_document.tsx | 24 ++++++----- 8 files changed, 103 insertions(+), 73 deletions(-) create mode 100644 packages/next/next-server/server/font-utils.ts diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index d9a78c7437810..aef77858e7c43 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -251,7 +251,6 @@ const nextServerlessLoader: loader.Loader = function () { import initServer from 'next-plugin-loader?middleware=on-init-server!' import onError from 'next-plugin-loader?middleware=on-error-server!' import 'next/dist/next-server/server/node-polyfill-fetch' - import { requireFontManifest } from 'next/dist/next-server/server/require' const {isResSent} = require('next/dist/next-server/lib/utils'); ${envLoading} @@ -402,18 +401,6 @@ const nextServerlessLoader: loader.Loader = function () { // make sure to set renderOpts to the correct params e.g. _params // if provided from worker or params if we're parsing them here renderOpts.params = _params || params - renderOpts.getFontDefinition = (fontURL) => { - console.log('will dist dirrrr', "${distDir}", "${canonicalBase}", "${basePath}"); - const manifest = requireFontManifest("${distDir}", true) - let fontContent = '' - manifest.forEach((font) => { - if (font && font.url === fontURL) { - fontContent = font.content - } - }) - - return fontContent - } const isFallback = parsedUrl.query.__nextFallback const previewData = tryGetPreviewData(req, res, options.previewProps) diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index d8be1d53f961c..fad9c9ad03552 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -14,6 +14,7 @@ import { IncomingMessage, ServerResponse } from 'http' import { ComponentType } from 'react' import { GetStaticProps } from '../types' import { requireFontManifest } from '../next-server/server/require' +import { FontManifest } from '../next-server/server/font-utils' const envConfig = require('../next-server/lib/runtime-config') @@ -61,7 +62,7 @@ interface RenderOpts { ampSkipValidation?: boolean hybridAmp?: boolean inAmpMode?: boolean - getFontDefinition?: (url: string) => string + fontManifest?: FontManifest } type ComponentModule = ComponentType<{}> & { @@ -85,18 +86,6 @@ export default async function exportPage({ ampValidations: [], } - const getFontDefinition = (fontURL: string) => { - const manifest = requireFontManifest(distDir, serverless) - let fontContent = '' - manifest.forEach((font: any) => { - if (font && font.url === fontURL) { - fontContent = font.content - } - }) - - return fontContent - } - try { const { query: originalQuery = {} } = pathMap const { page } = pathMap @@ -229,7 +218,7 @@ export default async function exportPage({ // @ts-ignore { ...params, - getFontDefinition, + fontManifest: requireFontManifest(distDir, serverless), } ) curRenderOpts = result.renderOpts || {} @@ -268,7 +257,7 @@ export default async function exportPage({ ...renderOpts, ampPath, params, - getFontDefinition, + fontManifest: requireFontManifest(distDir, serverless), } // @ts-ignore html = await renderMethod(req, res, page, query, curRenderOpts) diff --git a/packages/next/next-server/lib/head.tsx b/packages/next/next-server/lib/head.tsx index 4b03b2fc05847..77b10285cd8a6 100644 --- a/packages/next/next-server/lib/head.tsx +++ b/packages/next/next-server/lib/head.tsx @@ -136,6 +136,14 @@ function reduceComponents( .reverse() .map((c: React.ReactElement, i: number) => { const key = c.key || i + if ( + c.type === 'link' && + c.props['href'] && + c.props['href'].startsWith('https://fonts.googleapis.com/css') + ) { + c.props['data-href'] = c.props['href'] + delete c.props['href'] + } return React.cloneElement(c, { key }) }) } diff --git a/packages/next/next-server/lib/post-process.ts b/packages/next/next-server/lib/post-process.ts index f0287803d7bcb..b9d8f6e185f28 100644 --- a/packages/next/next-server/lib/post-process.ts +++ b/packages/next/next-server/lib/post-process.ts @@ -7,8 +7,8 @@ type postProcessOptions = { optimizeFonts: boolean } -type renderOpts = { - getFontDefinition?: (url: string) => string +type renderOptions = { + getFontDefinition?: (url: string) => Promise } type postProcessData = { @@ -21,12 +21,12 @@ interface PostProcessMiddleware { inspect: ( originalDom: HTMLElement, data: postProcessData, - options: renderOpts + options: renderOptions ) => void mutate: ( markup: string, data: postProcessData, - options: renderOpts + options: renderOptions ) => Promise } @@ -48,7 +48,7 @@ function registerPostProcessor( async function processHTML( html: string, - data: renderOpts, + data: renderOptions, options: postProcessOptions ): Promise { // Don't parse unless there's at least one processor middleware @@ -96,34 +96,30 @@ class FindImages implements PostProcessMiddleware { inspect( _originalDom: HTMLElement, _data: postProcessData, - _options: renderOpts + _options: renderOptions ) { return } mutate = async ( markup: string, _data: postProcessData, - _options: renderOpts + _options: renderOptions ) => { return markup } } class FontOptimizerMiddleware implements PostProcessMiddleware { - fontDefinitions: { - [key: string]: string - } = {} + fontDefinitions: Array = [] inspect( originalDom: HTMLElement, _data: postProcessData, - options: renderOpts + options: renderOptions ) { if (!options.getFontDefinition) { - console.log('early') return } - const getFontDefinition = options.getFontDefinition // collecting all the requested font definitions originalDom .querySelectorAll('link') @@ -137,19 +133,24 @@ class FontOptimizerMiddleware implements PostProcessMiddleware { ) .forEach((element: HTMLElement) => { const url = element.getAttribute('data-href') - this.fontDefinitions[url] = getFontDefinition(url) + this.fontDefinitions.push(url) }) } mutate = async ( markup: string, _data: postProcessData, - _options: renderOpts + options: renderOptions ) => { let result = markup + if (!options.getFontDefinition) { + return markup + } for (const key in this.fontDefinitions) { + const url = this.fontDefinitions[key] + const fontContent = await options.getFontDefinition(url) result = result.replace( '', - `` @@ -205,12 +206,12 @@ class RenderPreloads implements PostProcessMiddleware { inspect = ( _originalDom: HTMLElement, _data: postProcessData, - _options: renderOpts + _options: renderOptions ) => {} mutate = async ( markup: string, _data: postProcessData, - _options: renderOpts + _options: renderOptions ) => { return markup } diff --git a/packages/next/next-server/server/font-utils.ts b/packages/next/next-server/server/font-utils.ts new file mode 100644 index 0000000000000..df5673636de9c --- /dev/null +++ b/packages/next/next-server/server/font-utils.ts @@ -0,0 +1,42 @@ +const https = require('https') + +export type FontManifest = Array<{ + url: string + content: string +}> + +export function getFontDefinitionFromNetwork(url: string): Promise { + return new Promise((resolve) => { + let rawData: any = '' + https.get( + url, + { + headers: { + 'user-agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36', + }, + }, + (res: any) => { + res.on('data', (chunk: any) => { + rawData += chunk + }) + res.on('end', () => { + resolve(rawData.toString('utf8')) + }) + } + ) + }) +} + +export function getFontDefinitionFromManifest( + url: string, + manifest: FontManifest +): string { + let fontContent = '' + manifest.forEach((font: any) => { + if (font && font.url === url) { + fontContent = font.content + } + }) + return fontContent +} diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index 463ffc676df04..a8fe9dd98c16d 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -64,6 +64,7 @@ import './node-polyfill-fetch' import { PagesManifest } from '../../build/webpack/plugins/pages-manifest-plugin' import { removePathTrailingSlash } from '../../client/normalize-trailing-slash' import getRouteFromAssetPath from '../lib/router/utils/get-route-from-asset-path' +import { FontManifest } from './font-utils' const getCustomRouteMatcher = pathMatch(true) @@ -122,7 +123,7 @@ export default class Server { basePath: string postProcess: boolean optimizeFonts: boolean - getFontDefinition: (url: string) => string + fontManifest: FontManifest } private compression?: Middleware private onErrorMiddleware?: ({ err }: { err: Error }) => Promise @@ -174,7 +175,7 @@ export default class Server { basePath: this.nextConfig.basePath, postProcess: this.nextConfig.experimental.postProcessOptimization, optimizeFonts: this.nextConfig.experimental.fontOptimization, - getFontDefinition: this.getFontDefinition, + fontManifest: requireFontManifest(this.distDir, false), } // Only the `publicRuntimeConfig` key is exposed to the client side @@ -312,18 +313,6 @@ export default class Server { return (this._cachedPreviewManifest = manifest) } - protected getFontDefinition(fontURL: string): string { - const manifest = requireFontManifest(this.distDir, false) - let fontContent = '' - manifest.forEach((font: any) => { - if (font && font.url === fontURL) { - fontContent = font.content - } - }) - - return fontContent - } - protected getPreviewProps(): __ApiPreviewProps { return this.getPrerenderManifest().preview } diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index 2896a8d02bd00..678925526541d 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -46,6 +46,11 @@ import { getPageFiles } from './get-page-files' import { LoadComponentsReturnType, ManifestItem } from './load-components' import optimizeAmp from './optimize-amp' import postProcess from '../lib/post-process' +import { + FontManifest, + getFontDefinitionFromManifest, + getFontDefinitionFromNetwork, +} from './font-utils' function noRouter() { const message = @@ -146,7 +151,7 @@ export type RenderOptsPartial = { previewProps: __ApiPreviewProps basePath: string unstable_runtimeJS?: false - getFontDefinition?: (url: string) => string + fontManifest?: FontManifest } export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial @@ -273,6 +278,7 @@ export async function renderToHTML( pageConfig = {}, Component, buildManifest, + fontManifest, reactLoadableManifest, ErrorDebug, getStaticProps, @@ -282,9 +288,15 @@ export async function renderToHTML( params, previewProps, basePath, - getFontDefinition, } = renderOpts + const getFontDefinition = (url: string): Promise => { + if (fontManifest) { + return Promise.resolve(getFontDefinitionFromManifest(url, fontManifest)) + } + return getFontDefinitionFromNetwork(url) + } + const callMiddleware = async (method: string, args: any[], props = false) => { let results: any = props ? {} : [] diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index 905d06d5511e3..aff490e9db980 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -368,17 +368,19 @@ export class Head extends Component< { // Remove the fonts stylesheet tag here, this content would be inlined by post process. /// @ts-ignore - children.map((c) => { - if ( - c.type === 'link' && - c.props['href'] && - c.props['href'].startsWith('https://fonts.googleapis.com/css?') - ) { - c.props['data-href'] = c.props['href'] - delete c.props['href'] - } - return c - }) + children && children.map + ? children.map((c) => { + if ( + c.type === 'link' && + c.props['href'] && + c.props['href'].startsWith('https://fonts.googleapis.com/css') + ) { + c.props['data-href'] = c.props['href'] + delete c.props['href'] + } + return c + }) + : children } {head} Date: Tue, 30 Jun 2020 16:02:28 -0700 Subject: [PATCH 13/49] reverting unwated change --- packages/next/build/webpack/loaders/next-serverless-loader.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index aef77858e7c43..b78825641936b 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -401,6 +401,7 @@ const nextServerlessLoader: loader.Loader = function () { // make sure to set renderOpts to the correct params e.g. _params // if provided from worker or params if we're parsing them here renderOpts.params = _params || params + const isFallback = parsedUrl.query.__nextFallback const previewData = tryGetPreviewData(req, res, options.previewProps) From 0bad5cbbf11549c7deee68925ce94868944eb84d Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 30 Jun 2020 16:28:48 -0700 Subject: [PATCH 14/49] removing commented code --- packages/next/next-server/lib/post-process.ts | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/packages/next/next-server/lib/post-process.ts b/packages/next/next-server/lib/post-process.ts index b9d8f6e185f28..6a8b455081e2c 100644 --- a/packages/next/next-server/lib/post-process.ts +++ b/packages/next/next-server/lib/post-process.ts @@ -160,48 +160,6 @@ class FontOptimizerMiddleware implements PostProcessMiddleware { } } -// Middleware -// const inlineFonts: postProcessMiddleware = async ( -// htmlRoot, -// document, -// _data, -// options -// ) => { -// if (!options.getFontDefinition) { -// return htmlRoot -// } - -// const getFontDefinition = options.getFontDefinition -// const links = htmlRoot -// .querySelectorAll('link') -// .filter( -// (tag) => -// tag.getAttribute('rel') === 'stylesheet' && -// tag.hasAttribute('href') && -// tag -// .getAttribute('href') -// .startsWith('https://fonts.googleapis.com/css2?') -// ) -// links.forEach((link) => { -// const url = link.getAttribute('href') -// console.log(document, '...', `', -// `` -// ) -// /** -// * Removing the actual element is not supported in node-html-parser -// * so we just remove the href effectively making it inert. -// */ -// //link.removeAttribute('href') -// }) -// return document -// } - class RenderPreloads implements PostProcessMiddleware { inspect = ( _originalDom: HTMLElement, From 463ca8546fbbd02f4a61b018c0e6d8d01fa02728 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 30 Jun 2020 16:36:38 -0700 Subject: [PATCH 15/49] adding experimental flag --- packages/next/build/webpack-config.ts | 5 ++++- packages/next/next-server/server/config.ts | 3 ++- packages/next/next-server/server/next-server.ts | 8 +++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index c237a03a0b65a..4507de991d559 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -961,7 +961,10 @@ export default async function getBaseWebpackConfig( inputChunkName.replace(/\.js$/, '.module.js'), }) })(), - !dev && isServer && new FontStylesheetGatheringPlugin(), + config.experimental.optimizeFonts && + !dev && + isServer && + new FontStylesheetGatheringPlugin(), config.experimental.conformance && !dev && new WebpackConformancePlugin({ diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts index 2a7b378856568..7b20b2a13f50e 100644 --- a/packages/next/next-server/server/config.ts +++ b/packages/next/next-server/server/config.ts @@ -53,7 +53,8 @@ const defaultConfig: { [key: string]: any } = { workerThreads: false, pageEnv: false, productionBrowserSourceMaps: false, - postProcessOptimize: false, + optimizeImages: false, + optimizeFonts: false, }, future: { excludeDefaultMomentLocales: false, diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index a8fe9dd98c16d..1846538ac20a5 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -173,9 +173,11 @@ export default class Server { customServer: customServer === true ? true : undefined, ampOptimizerConfig: this.nextConfig.experimental.amp?.optimizer, basePath: this.nextConfig.basePath, - postProcess: this.nextConfig.experimental.postProcessOptimization, - optimizeFonts: this.nextConfig.experimental.fontOptimization, - fontManifest: requireFontManifest(this.distDir, false), + postProcess: this.nextConfig.experimental.optimizeImages, + optimizeFonts: this.nextConfig.experimental.optimizeFonts, + fontManifest: this.nextConfig.experimental.optimizeFonts + ? requireFontManifest(this.distDir, false) + : undefined, } // Only the `publicRuntimeConfig` key is exposed to the client side From 004ebcb8025e6114573b3d3dc8633f07da8ddbef Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 30 Jun 2020 16:38:37 -0700 Subject: [PATCH 16/49] code resuability --- .../font-stylesheet-gathering-plugin.ts | 33 +++---------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts b/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts index 8b50a820e21f6..dd0e920427a26 100644 --- a/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts +++ b/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts @@ -4,8 +4,7 @@ import { visit } from 'next/dist/compiled/recast' import { compilation as CompilationType, Compiler } from 'webpack' import { namedTypes } from 'ast-types' import { RawSource } from 'webpack-sources' - -const https = require('https') +import { getFontDefinitionFromNetwork } from '../../../next-server/server/font-utils' interface VisitorMap { [key: string]: (path: NodePath) => void @@ -83,7 +82,9 @@ export default class FontStylesheetGatheringPlugin { compilation.hooks.finishModules.tapAsync( this.constructor.name, async (_, modulesFinished) => { - const allContent = this.gatheredStylesheets.map((url) => getFile(url)) + const allContent = this.gatheredStylesheets.map((url) => + getFontDefinitionFromNetwork(url) + ) const manifestContent = (await Promise.allSettled(allContent)).map( (promise) => promise.value ) @@ -110,29 +111,3 @@ function isNodeCreatingLinkElement(node: namedTypes.CallExpression) { // Next has pragma: __jsx. return callee.name === '__jsx' && componentNode.value === 'link' } - -function getFile(url: string): Promise { - return new Promise((resolve) => { - let rawData: any = '' - https.get( - url, - { - headers: { - 'user-agent': - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36', - }, - }, - (res: any) => { - res.on('data', (chunk: any) => { - rawData += chunk - }) - res.on('end', () => { - resolve({ - url, - content: rawData.toString('utf8'), - }) - }) - } - ) - }) -} From 6acdce59912e8fa0ac5d03c23285c912174b5128 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 30 Jun 2020 16:40:29 -0700 Subject: [PATCH 17/49] renaming flags --- packages/next/next-server/server/render.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index 678925526541d..d02ba8718d7c0 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -144,7 +144,7 @@ export type RenderOptsPartial = { ampValidator?: (html: string, pathname: string) => Promise ampSkipValidation?: boolean ampOptimizerConfig?: { [key: string]: any } - postProcess: boolean + optimizeImages: boolean optimizeFonts: boolean isDataReq?: boolean params?: ParsedUrlQuery From 2289980174846686ca3d509f2dd5ec7401ca5ac2 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 30 Jun 2020 16:41:01 -0700 Subject: [PATCH 18/49] renaming flags --- packages/next/next-server/server/render.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index d02ba8718d7c0..089aa2b345379 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -800,7 +800,7 @@ export async function renderToHTML( getFontDefinition, }, { - preloadImages: renderOpts.postProcess, + preloadImages: renderOpts.optimizeImages, optimizeFonts: renderOpts.optimizeFonts, } ) From 0d0c96dde3488bbc258ac604dd5e4a4c1bf6197c Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Tue, 30 Jun 2020 23:58:05 -0700 Subject: [PATCH 19/49] guarding behind a flag --- packages/next/export/index.ts | 1 + packages/next/export/worker.ts | 10 ++++++++-- packages/next/next-server/lib/post-process.ts | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index 23d40b0ba0bb7..93cbac6986dfd 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -388,6 +388,7 @@ export default async function exportApp( subFolders, buildExport: options.buildExport, serverless: isTargetLikeServerless(nextConfig.target), + optimizeFonts: nextConfig.experimental.optimizeFonts, }) for (const validation of result.ampValidations || []) { diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index fad9c9ad03552..3cb67d36073a2 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -46,6 +46,7 @@ interface ExportPageInput { serverRuntimeConfig: string subFolders: string serverless: boolean + optimizeFonts: boolean } interface ExportPageResults { @@ -81,6 +82,7 @@ export default async function exportPage({ serverRuntimeConfig, subFolders, serverless, + optimizeFonts, }: ExportPageInput): Promise { let results: ExportPageResults = { ampValidations: [], @@ -218,7 +220,9 @@ export default async function exportPage({ // @ts-ignore { ...params, - fontManifest: requireFontManifest(distDir, serverless), + fontManifest: optimizeFonts + ? requireFontManifest(distDir, serverless) + : undefined, } ) curRenderOpts = result.renderOpts || {} @@ -257,7 +261,9 @@ export default async function exportPage({ ...renderOpts, ampPath, params, - fontManifest: requireFontManifest(distDir, serverless), + fontManifest: optimizeFonts + ? requireFontManifest(distDir, serverless) + : undefined, } // @ts-ignore html = await renderMethod(req, res, page, query, curRenderOpts) diff --git a/packages/next/next-server/lib/post-process.ts b/packages/next/next-server/lib/post-process.ts index 6a8b455081e2c..c54e1d39aebe9 100644 --- a/packages/next/next-server/lib/post-process.ts +++ b/packages/next/next-server/lib/post-process.ts @@ -185,7 +185,7 @@ registerPostProcessor( registerPostProcessor( 'Inline-Fonts', new FontOptimizerMiddleware(), - (options) => options.optimizeFonts || true + (options) => options.optimizeFonts ) registerPostProcessor( 'Render-Preloads', From 45e792959b001fd2eba093d718173d23c25cd869 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Wed, 1 Jul 2020 00:18:30 -0700 Subject: [PATCH 20/49] removing Promise.allSettled --- .../plugins/font-stylesheet-gathering-plugin.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts b/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts index dd0e920427a26..d4ad8f575fe4b 100644 --- a/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts +++ b/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts @@ -4,7 +4,10 @@ import { visit } from 'next/dist/compiled/recast' import { compilation as CompilationType, Compiler } from 'webpack' import { namedTypes } from 'ast-types' import { RawSource } from 'webpack-sources' -import { getFontDefinitionFromNetwork } from '../../../next-server/server/font-utils' +import { + getFontDefinitionFromNetwork, + FontManifest, +} from '../../../next-server/server/font-utils' interface VisitorMap { [key: string]: (path: NodePath) => void @@ -85,9 +88,14 @@ export default class FontStylesheetGatheringPlugin { const allContent = this.gatheredStylesheets.map((url) => getFontDefinitionFromNetwork(url) ) - const manifestContent = (await Promise.allSettled(allContent)).map( - (promise) => promise.value - ) + let manifestContent: FontManifest = [] + + for (let promiseIndex in allContent) { + manifestContent.push({ + url: this.gatheredStylesheets[promiseIndex], + content: await allContent[promiseIndex], + }) + } compilation.assets['font-manifest.json'] = new RawSource( JSON.stringify(manifestContent, null, ' ') ) From 2602b31ba2e3351dafee9faa43ce57025fd47fb9 Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Thu, 2 Jul 2020 16:53:23 -0700 Subject: [PATCH 21/49] Bug fixes and adding tests --- packages/next/next-server/lib/post-process.ts | 16 ++++--- packages/next/pages/_document.tsx | 18 +------- .../font-optimization/next.config.js | 5 +++ .../font-optimization/pages/_document.js | 30 +++++++++++++ .../font-optimization/pages/index.js | 15 +++++++ .../font-optimization/test/index.test.js | 44 +++++++++++++++++++ 6 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 test/integration/font-optimization/next.config.js create mode 100644 test/integration/font-optimization/pages/_document.js create mode 100644 test/integration/font-optimization/pages/index.js create mode 100644 test/integration/font-optimization/test/index.test.js diff --git a/packages/next/next-server/lib/post-process.ts b/packages/next/next-server/lib/post-process.ts index c54e1d39aebe9..3c105e02b21b1 100644 --- a/packages/next/next-server/lib/post-process.ts +++ b/packages/next/next-server/lib/post-process.ts @@ -126,13 +126,18 @@ class FontOptimizerMiddleware implements PostProcessMiddleware { .filter( (tag: HTMLElement) => tag.getAttribute('rel') === 'stylesheet' && - tag.hasAttribute('data-href') && - tag - .getAttribute('data-href') - .startsWith('https://fonts.googleapis.com/css') + ((tag.hasAttribute('data-href') && + tag + .getAttribute('data-href') + .startsWith('https://fonts.googleapis.com/css')) || + (tag.hasAttribute('href') && + tag + .getAttribute('href') + .startsWith('https://fonts.googleapis.com/css'))) ) .forEach((element: HTMLElement) => { - const url = element.getAttribute('data-href') + const url = + element.getAttribute('data-href') || element.getAttribute('href') this.fontDefinitions.push(url) }) } @@ -147,6 +152,7 @@ class FontOptimizerMiddleware implements PostProcessMiddleware { } for (const key in this.fontDefinitions) { const url = this.fontDefinitions[key] + result = result.replace(`href="${url}"`, `data-href="${url}"`) const fontContent = await options.getFontDefinition(url) result = result.replace( '', diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index aff490e9db980..189a3ca2968cf 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -365,23 +365,7 @@ export class Head extends Component< )} - { - // Remove the fonts stylesheet tag here, this content would be inlined by post process. - /// @ts-ignore - children && children.map - ? children.map((c) => { - if ( - c.type === 'link' && - c.props['href'] && - c.props['href'].startsWith('https://fonts.googleapis.com/css') - ) { - c.props['data-href'] = c.props['href'] - delete c.props['href'] - } - return c - }) - : children - } + {children} {head} + + + + +
+ + + + ) + } +} diff --git a/test/integration/font-optimization/pages/index.js b/test/integration/font-optimization/pages/index.js new file mode 100644 index 0000000000000..f20eca5d2a972 --- /dev/null +++ b/test/integration/font-optimization/pages/index.js @@ -0,0 +1,15 @@ +import React from 'react' + +const Idk = React.createContext(null) + +const Page = () => { + return ( +
+ + {(idk) =>

Value: {idk}

}
+
+
+ ) +} + +export default Page diff --git a/test/integration/font-optimization/test/index.test.js b/test/integration/font-optimization/test/index.test.js new file mode 100644 index 0000000000000..2049efef1d7ec --- /dev/null +++ b/test/integration/font-optimization/test/index.test.js @@ -0,0 +1,44 @@ +/* eslint-env jest */ + +import { join } from 'path' +import { + killApp, + findPort, + nextStart, + nextBuild, + renderViaHTTP, +} from 'next-test-utils' +import fs from 'fs-extra' + +jest.setTimeout(1000 * 30) + +const appDir = join(__dirname, '../') +let builtServerPagesDir +let builtPage +let appPort +let app + +const fsExists = (file) => + fs + .access(file) + .then(() => true) + .catch(() => false) + +describe('Font optimization', () => { + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + builtServerPagesDir = join(appDir, '.next/server') + builtPage = (file) => join(builtServerPagesDir, file) + }) + afterAll(() => killApp(app)) + + it('should inline the google fonts', async () => { + const html = await renderViaHTTP(appPort, '/') + expect(await fsExists(builtPage('font-manifest.json'))).toBe(true) + expect(html).toContain( + '' + ) + }) +}) From 5cde0b27ddcf4a81cf44157723ee2acb571bd53c Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Thu, 2 Jul 2020 17:13:01 -0700 Subject: [PATCH 22/49] sending experimental flags --- packages/next/export/worker.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index 3cb67d36073a2..2da46f7749064 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -63,6 +63,7 @@ interface RenderOpts { ampSkipValidation?: boolean hybridAmp?: boolean inAmpMode?: boolean + optimizeFonts?: boolean fontManifest?: FontManifest } @@ -220,6 +221,7 @@ export default async function exportPage({ // @ts-ignore { ...params, + optimizeFonts, fontManifest: optimizeFonts ? requireFontManifest(distDir, serverless) : undefined, @@ -261,6 +263,7 @@ export default async function exportPage({ ...renderOpts, ampPath, params, + optimizeFonts, fontManifest: optimizeFonts ? requireFontManifest(distDir, serverless) : undefined, From fcf567cc18566ee244d0aa5b532abd36bc7d1e9b Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Fri, 3 Jul 2020 01:39:56 -0700 Subject: [PATCH 23/49] bug fixes and adding tests --- packages/next/next-server/lib/post-process.ts | 4 +-- .../font-optimization/pages/stars.js | 26 +++++++++++++++++++ .../font-optimization/test/index.test.js | 16 +++++++++++- 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 test/integration/font-optimization/pages/stars.js diff --git a/packages/next/next-server/lib/post-process.ts b/packages/next/next-server/lib/post-process.ts index 3c105e02b21b1..388e416974b0b 100644 --- a/packages/next/next-server/lib/post-process.ts +++ b/packages/next/next-server/lib/post-process.ts @@ -152,14 +152,14 @@ class FontOptimizerMiddleware implements PostProcessMiddleware { } for (const key in this.fontDefinitions) { const url = this.fontDefinitions[key] - result = result.replace(`href="${url}"`, `data-href="${url}"`) + result = result.replace(` href="${url}"`, ` data-href="${url}"`) const fontContent = await options.getFontDefinition(url) result = result.replace( '', `` + )}` ) } return result diff --git a/test/integration/font-optimization/pages/stars.js b/test/integration/font-optimization/pages/stars.js new file mode 100644 index 0000000000000..ce1982e3647e4 --- /dev/null +++ b/test/integration/font-optimization/pages/stars.js @@ -0,0 +1,26 @@ +import Head from 'next/head' + +function Home({ stars }) { + return ( +
+ + Create Next App + + + + +
+
Next stars: {stars}
+
+
+ ) +} + +Home.getInitialProps = async () => { + return { stars: Math.random() * 1000 } +} + +export default Home diff --git a/test/integration/font-optimization/test/index.test.js b/test/integration/font-optimization/test/index.test.js index 2049efef1d7ec..f4fc747ee8ec8 100644 --- a/test/integration/font-optimization/test/index.test.js +++ b/test/integration/font-optimization/test/index.test.js @@ -34,11 +34,25 @@ describe('Font optimization', () => { }) afterAll(() => killApp(app)) - it('should inline the google fonts', async () => { + it('should inline the google fonts for static pages', async () => { const html = await renderViaHTTP(appPort, '/') expect(await fsExists(builtPage('font-manifest.json'))).toBe(true) expect(html).toContain( '' ) + expect(html).toMatch( + /