未验证 提交 fb81ecb2 编写于 作者: P Prateek Bhatnagar 提交者: GitHub

Font optimizations (#14746)

Co-authored-by: Natcastle <atcastle@gmail.com>
上级 27c207da
......@@ -53,7 +53,7 @@ import WebpackConformancePlugin, {
ReactSyncScriptsConformanceCheck,
} from './webpack/plugins/webpack-conformance-plugin'
import { WellKnownErrorsPlugin } from './webpack/plugins/wellknown-errors-plugin'
import FontStylesheetGatheringPlugin from './webpack/plugins/font-stylesheet-gathering-plugin'
type ExcludesFalse = <T>(x: T | false) => x is T
const isWebpack5 = parseInt(webpack.version!) === 5
......@@ -873,6 +873,9 @@ export default async function getBaseWebpackConfig(
'process.env.__NEXT_REACT_MODE': JSON.stringify(
config.experimental.reactMode
),
'process.env.__NEXT_OPTIMIZE_FONTS': JSON.stringify(
config.experimental.optimizeFonts
),
'process.env.__NEXT_SCROLL_RESTORATION': JSON.stringify(
config.experimental.scrollRestoration
),
......@@ -978,6 +981,10 @@ export default async function getBaseWebpackConfig(
inputChunkName.replace(/\.js$/, '.module.js'),
})
})(),
config.experimental.optimizeFonts &&
!dev &&
isServer &&
new FontStylesheetGatheringPlugin(),
config.experimental.conformance &&
!isWebpack5 &&
!dev &&
......
......@@ -6,6 +6,7 @@ import { loader } from 'webpack'
import { API_ROUTE } from '../../../lib/constants'
import {
BUILD_MANIFEST,
FONT_MANIFEST,
REACT_LOADABLE_MANIFEST,
ROUTES_MANIFEST,
} from '../../../next-server/lib/constants'
......@@ -58,6 +59,10 @@ const nextServerlessLoader: loader.Loader = function () {
'/'
)
const routesManifest = join(distDir, ROUTES_MANIFEST).replace(/\\/g, '/')
const fontManifest = join(distDir, 'serverless', FONT_MANIFEST).replace(
/\\/g,
'/'
)
const escapedBuildId = escapeRegexp(buildId)
const pageIsDynamicRoute = isDynamicRoute(page)
......@@ -266,7 +271,7 @@ const nextServerlessLoader: loader.Loader = function () {
}
const {parse} = require('url')
const {parse: parseQs} = require('querystring')
const {renderToHTML} = require('next/dist/next-server/server/render');
const { renderToHTML } = require('next/dist/next-server/server/render');
const { tryGetPreviewData } = require('next/dist/next-server/server/api-utils');
const {sendPayload} = require('next/dist/next-server/server/send-payload');
const buildManifest = require('${buildManifest}');
......@@ -274,6 +279,7 @@ const nextServerlessLoader: loader.Loader = function () {
const Document = require('${absoluteDocumentPath}').default;
const Error = require('${absoluteErrorPath}').default;
const App = require('${absoluteAppPath}').default;
${dynamicRouteImports}
${rewriteImports}
......@@ -418,6 +424,11 @@ const nextServerlessLoader: loader.Loader = function () {
const previewData = tryGetPreviewData(req, res, options.previewProps)
const isPreviewMode = previewData !== false
if (process.env.__NEXT_OPTIMIZE_FONTS) {
renderOpts.optimizeFonts = true
renderOpts.fontManifest = require('${fontManifest}')
process.env['__NEXT_OPTIMIZE_FONT'+'S'] = true
}
let result = await renderToHTML(req, res, "${page}", Object.assign({}, getStaticProps ? { ...(parsedUrl.query.amp ? { amp: '1' } : {}) } : parsedUrl.query, nowParams ? nowParams : params, _params, isFallback ? { __nextFallback: 'true' } : {}), renderOpts)
if (!renderMode) {
......
// eslint-disable-next-line import/no-extraneous-dependencies
import { NodePath } from 'ast-types/lib/node-path'
import { compilation as CompilationType, Compiler } from 'webpack'
import { namedTypes } from 'ast-types'
import { RawSource } from 'webpack-sources'
import {
getFontDefinitionFromNetwork,
FontManifest,
} from '../../../next-server/server/font-utils'
// @ts-ignore
import BasicEvaluatedExpression from 'webpack/lib/BasicEvaluatedExpression'
import { OPTIMIZED_FONT_PROVIDERS } from '../../../next-server/lib/constants'
interface VisitorMap {
[key: string]: (path: NodePath) => void
}
export default class FontStylesheetGatheringPlugin {
compiler?: Compiler
gatheredStylesheets: Array<string> = []
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: any) => {
/**
* Webpack fun facts:
* `parser.hooks.call.for` cannot catch calls for user defined identifiers like `__jsx`
* it can only detect calls for native objects like `window`, `this`, `eval` etc.
* In order to be able to catch calls of variables like `__jsx`, first we need to catch them as
* Identifier and then return `BasicEvaluatedExpression` whose `id` and `type` webpack matches to
* invoke hook for call.
* See: https://github.com/webpack/webpack/blob/webpack-4/lib/Parser.js#L1931-L1932.
*/
parser.hooks.evaluate
.for('Identifier')
.tap(this.constructor.name, (node: namedTypes.Identifier) => {
// We will only optimize fonts from first party code.
if (parser?.state?.module?.resource.includes('node_modules')) {
return
}
return node.name === '__jsx'
? new BasicEvaluatedExpression()
//@ts-ignore
.setRange(node.range)
.setExpression(node)
.setIdentifier('__jsx')
: undefined
})
parser.hooks.call
.for('__jsx')
.tap(this.constructor.name, (node: namedTypes.CallExpression) => {
if (node.arguments.length !== 2) {
// A font link tag has only two arguments rel=stylesheet and href='...'
return
}
if (!isNodeCreatingLinkElement(node)) {
return
}
// node.arguments[0] is the name of the tag and [1] are the props.
const propsNode = node.arguments[1] as namedTypes.ObjectExpression
const props: { [key: string]: string } = {}
propsNode.properties.forEach((prop) => {
if (prop.type !== 'Property') {
return
}
if (
prop.key.type === 'Identifier' &&
prop.value.type === 'Literal'
) {
props[prop.key.name] = prop.value.value as string
}
})
if (
!props.rel ||
props.rel !== 'stylesheet' ||
!props.href ||
!OPTIMIZED_FONT_PROVIDERS.some((url) =>
props.href.startsWith(url)
)
) {
return false
}
this.gatheredStylesheets.push(props.href)
})
})
}
}
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 (_: any, modulesFinished: Function) => {
const fontDefinitionPromises = this.gatheredStylesheets.map((url) =>
getFontDefinitionFromNetwork(url)
)
let manifestContent: FontManifest = []
for (let promiseIndex in fontDefinitionPromises) {
manifestContent.push({
url: this.gatheredStylesheets[promiseIndex],
content: await fontDefinitionPromises[promiseIndex],
})
}
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'
}
......@@ -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 || []) {
......
......@@ -13,6 +13,8 @@ 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'
import { FontManifest } from '../next-server/server/font-utils'
const envConfig = require('../next-server/lib/runtime-config')
......@@ -44,6 +46,7 @@ interface ExportPageInput {
serverRuntimeConfig: string
subFolders: string
serverless: boolean
optimizeFonts: boolean
}
interface ExportPageResults {
......@@ -60,6 +63,8 @@ interface RenderOpts {
ampSkipValidation?: boolean
hybridAmp?: boolean
inAmpMode?: boolean
optimizeFonts?: boolean
fontManifest?: FontManifest
}
type ComponentModule = ComponentType<{}> & {
......@@ -78,6 +83,7 @@ export default async function exportPage({
serverRuntimeConfig,
subFolders,
serverless,
optimizeFonts,
}: ExportPageInput): Promise<ExportPageResults> {
let results: ExportPageResults = {
ampValidations: [],
......@@ -211,7 +217,14 @@ export default async function exportPage({
req,
res,
'export',
{ ampPath },
{
ampPath,
/// @ts-ignore
optimizeFonts,
fontManifest: optimizeFonts
? requireFontManifest(distDir, serverless)
: null,
},
// @ts-ignore
params
)
......@@ -246,7 +259,25 @@ export default async function exportPage({
html = components.Component
queryWithAutoExportWarn()
} else {
curRenderOpts = { ...components, ...renderOpts, ampPath, params }
/**
* This sets environment variable to be used at the time of static export by head.tsx.
* Using this from process.env allows targetting both serverless and SSR by calling
* `process.env.__NEXT_OPTIMIZE_FONTS`.
* TODO(prateekbh@): Remove this when experimental.optimizeFonts are being clened up.
*/
if (optimizeFonts) {
process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true)
}
curRenderOpts = {
...components,
...renderOpts,
ampPath,
params,
optimizeFonts,
fontManifest: optimizeFonts
? requireFontManifest(distDir, serverless)
: null,
}
// @ts-ignore
html = await renderMethod(req, res, page, query, curRenderOpts)
}
......
......@@ -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'
......@@ -33,3 +34,4 @@ export const TEMPORARY_REDIRECT_STATUS = 307
export const PERMANENT_REDIRECT_STATUS = 308
export const STATIC_PROPS_ID = '__N_SSG'
export const SERVER_PROPS_ID = '__N_SSP'
export const OPTIMIZED_FONT_PROVIDERS = ['https://fonts.googleapis.com/css']
......@@ -136,6 +136,21 @@ function reduceComponents(
.reverse()
.map((c: React.ReactElement<any>, i: number) => {
const key = c.key || i
if (process.env.__NEXT_OPTIMIZE_FONTS) {
if (
c.type === 'link' &&
c.props['href'] &&
// TODO(prateekbh@): Replace this with const from `constants` when the tree shaking works.
['https://fonts.googleapis.com/css'].some((url) =>
c.props['href'].startsWith(url)
)
) {
const newProps = { ...(c.props || {}) }
newProps['data-href'] = newProps['href']
newProps['href'] = undefined
return React.cloneElement(c, newProps)
}
}
return React.cloneElement(c, { key })
})
}
......
import { parse, HTMLElement } from 'node-html-parser'
import { OPTIMIZED_FONT_PROVIDERS } from './constants'
const MIDDLEWARE_TIME_BUDGET = 10
type postProcessOptions = {
optimizeFonts: boolean
}
type renderOptions = {
getFontDefinition?: (url: string) => string
}
type postProcessData = {
preloads: {
images: Array<string>
}
}
interface PostProcessMiddleware {
inspect: (
originalDom: HTMLElement,
data: postProcessData,
options: renderOptions
) => void
mutate: (
markup: string,
data: postProcessData,
options: renderOptions
) => Promise<string>
}
type middlewareSignature = {
name: string
middleware: PostProcessMiddleware
condition: ((options: postProcessOptions) => boolean) | null
}
const middlewareRegistry: Array<middlewareSignature> = []
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<string> {
// Don't parse unless there's at least one processor middleware
if (!middlewareRegistry[0]) {
return html
}
const postProcessData: postProcessData = {
preloads: {
images: [],
},
}
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()
middleware.inspect(root, postProcessData, data)
const inspectTime = Date.now() - timer
document = await middleware.mutate(document, postProcessData, data)
timer = Date.now() - timer
if (timer > MIDDLEWARE_TIME_BUDGET) {
console.warn(
`The postprocess middleware "${name}" took ${timer}ms(${inspectTime}, ${
timer - inspectTime
}) 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 document
}
class FontOptimizerMiddleware implements PostProcessMiddleware {
fontDefinitions: Array<string> = []
inspect(
originalDom: HTMLElement,
_data: postProcessData,
options: renderOptions
) {
if (!options.getFontDefinition) {
return
}
// 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) =>
tag.getAttribute('data-href').startsWith(url)
)
)
.forEach((element: HTMLElement) => {
const url = element.getAttribute('data-href')
this.fontDefinitions.push(url)
})
}
mutate = async (
markup: string,
_data: postProcessData,
options: renderOptions
) => {
let result = markup
if (!options.getFontDefinition) {
return markup
}
for (const key in this.fontDefinitions) {
const url = this.fontDefinitions[key]
if (result.indexOf(`<style data-href="${url}">`) > -1) {
// The font is already optimized and probably the response is cached
continue
}
const fontContent = options.getFontDefinition(url)
result = result.replace(
'</head>',
`<style data-href="${url}">${fontContent.replace(
/(\n|\s)/g,
''
)}</style></head>`
)
}
return result
}
}
// 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 default processHTML
......@@ -52,6 +52,7 @@ const defaultConfig: { [key: string]: any } = {
workerThreads: false,
pageEnv: false,
productionBrowserSourceMaps: false,
optimizeFonts: false,
scrollRestoration: false,
},
future: {
......
const https = require('https')
const CHROME_UA =
'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'
const IE_UA = 'Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko'
export type FontManifest = Array<{
url: string
content: string
}>
function getFontForUA(url: string, UA: string): Promise<String> {
return new Promise((resolve) => {
let rawData: any = ''
https.get(
url,
{
headers: {
'user-agent': UA,
},
},
(res: any) => {
res.on('data', (chunk: any) => {
rawData += chunk
})
res.on('end', () => {
resolve(rawData.toString('utf8'))
})
}
)
})
}
export async function getFontDefinitionFromNetwork(
url: string
): Promise<string> {
let result = ''
/**
* The order of IE -> Chrome is important, other wise chrome starts loading woff1.
* CSS cascading 🤷‍♂️.
*/
result += await getFontForUA(url, IE_UA)
result += await getFontForUA(url, CHROME_UA)
return result
}
export function getFontDefinitionFromManifest(
url: string,
manifest: FontManifest
): string {
return (
manifest.find((font) => {
if (font && font.url === url) {
return true
}
return false
})?.content || ''
)
}
......@@ -43,7 +43,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,
......@@ -63,6 +63,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)
......@@ -119,6 +120,8 @@ export default class Server {
customServer?: boolean
ampOptimizerConfig?: { [key: string]: any }
basePath: string
optimizeFonts: boolean
fontManifest: FontManifest
}
private compression?: Middleware
private onErrorMiddleware?: ({ err }: { err: Error }) => Promise<void>
......@@ -165,6 +168,10 @@ export default class Server {
customServer: customServer === true ? true : undefined,
ampOptimizerConfig: this.nextConfig.experimental.amp?.optimizer,
basePath: this.nextConfig.basePath,
optimizeFonts: this.nextConfig.experimental.optimizeFonts,
fontManifest: this.nextConfig.experimental.optimizeFonts
? requireFontManifest(this.distDir, this._isLikeServerless)
: null,
}
// Only the `publicRuntimeConfig` key is exposed to the client side
......@@ -219,6 +226,16 @@ export default class Server {
),
flushToDisk: this.nextConfig.experimental.sprFlushToDisk,
})
/**
* This sets environment variable to be used at the time of SSR by head.tsx.
* Using this from process.env allows targetting both serverless and SSR by calling
* `process.env.__NEXT_OPTIMIZE_FONTS`.
* TODO(prateekbh@): Remove this when experimental.optimizeFonts are being clened up.
*/
if (this.renderOpts.optimizeFonts) {
process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true)
}
}
protected currentPhase(): string {
......@@ -1044,7 +1061,10 @@ export default class Server {
renderResult = await (components.Component as any).renderReqToHTML(
req,
res,
'passthrough'
'passthrough',
{
fontManifest: this.renderOpts.fontManifest,
}
)
html = renderResult.html
......
......@@ -45,6 +45,8 @@ import { tryGetPreviewData, __ApiPreviewProps } from './api-utils'
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 } from './font-utils'
function noRouter() {
const message =
......@@ -143,6 +145,8 @@ export type RenderOptsPartial = {
previewProps: __ApiPreviewProps
basePath: string
unstable_runtimeJS?: false
optimizeFonts: boolean
fontManifest?: FontManifest
}
export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
......@@ -269,6 +273,7 @@ export async function renderToHTML(
pageConfig = {},
Component,
buildManifest,
fontManifest,
reactLoadableManifest,
ErrorDebug,
getStaticProps,
......@@ -280,6 +285,13 @@ export async function renderToHTML(
basePath,
} = renderOpts
const getFontDefinition = (url: string): string => {
if (fontManifest) {
return getFontDefinitionFromManifest(url, fontManifest)
}
return ''
}
const callMiddleware = async (method: string, args: any[], props = false) => {
let results: any = props ? {} : []
......@@ -781,6 +793,16 @@ export async function renderToHTML(
}
}
html = await postProcess(
html,
{
getFontDefinition,
},
{
optimizeFonts: renderOpts.optimizeFonts,
}
)
if (inAmpMode || hybridAmp) {
// fix &amp being escaped for amphtml rel link
html = html.replace(/&amp;amp=1/g, '&amp=1')
......
......@@ -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,12 @@ export function requirePage(
}
return require(pagePath)
}
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
}
......@@ -78,6 +78,7 @@
"@babel/types": "7.9.6",
"@next/react-dev-overlay": "9.5.0",
"@next/react-refresh-utils": "9.5.0",
"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",
......@@ -94,6 +95,7 @@
"mkdirp": "0.5.3",
"native-url": "0.3.4",
"neo-async": "2.6.1",
"node-html-parser": "^1.2.19",
"pnp-webpack-plugin": "1.6.4",
"postcss": "7.0.32",
"process": "0.11.10",
......@@ -155,7 +157,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",
......
import PropTypes from 'prop-types'
import React, { useContext, Component } from 'react'
import React, { useContext, Component, ReactNode } from 'react'
import flush from 'styled-jsx/server'
import { AMP_RENDER_TARGET } from '../next-server/lib/constants'
import {
AMP_RENDER_TARGET,
OPTIMIZED_FONT_PROVIDERS,
} from '../next-server/lib/constants'
import { DocumentContext as DocumentComponentContext } from '../next-server/lib/document-context'
import {
DocumentContext,
......@@ -236,6 +239,24 @@ export class Head extends Component<
))
}
makeStylesheetInert(node: ReactNode): ReactNode {
return React.Children.map(node, (c: any) => {
if (
c.type === 'link' &&
c.props['href'] &&
OPTIMIZED_FONT_PROVIDERS.some((url) => c.props['href'].startsWith(url))
) {
const newProps = { ...(c.props || {}) }
newProps['data-href'] = newProps['href']
newProps['href'] = undefined
return React.cloneElement(c, newProps)
} else if (c.props && c.props['children']) {
c.props['children'] = this.makeStylesheetInert(c.props['children'])
}
return c
})
}
render() {
const {
styles,
......@@ -278,6 +299,10 @@ export class Head extends Component<
)
}
if (process.env.__NEXT_OPTIMIZE_FONTS) {
children = this.makeStylesheetInert(children)
}
let hasAmphtmlRel = false
let hasCanonicalRel = false
......@@ -285,7 +310,6 @@ export class Head extends Component<
head = React.Children.map(head || [], (child) => {
if (!child) return child
const { type, props } = child
if (inAmpMode) {
let badProp: string = ''
......@@ -435,7 +459,9 @@ export class Head extends Component<
href={canonicalBase + getAmpPath(ampPath, dangerousAsPath)}
/>
)}
{this.getCssLinks()}
{process.env.__NEXT_OPTIMIZE_FONTS
? this.makeStylesheetInert(this.getCssLinks())
: this.getCssLinks()}
{!disableRuntimeJS && this.getPreloadDynamicChunks()}
{!disableRuntimeJS && this.getPreloadMainLinks()}
{this.context._documentProps.isDevelopment && (
......
......@@ -98,7 +98,7 @@ describe('Build Output', () => {
expect(parseFloat(indexFirstLoad) - 60).toBeLessThanOrEqual(0)
expect(indexFirstLoad.endsWith('kB')).toBe(true)
expect(parseFloat(err404Size) - 3.4).toBeLessThanOrEqual(0)
expect(parseFloat(err404Size) - 3.6).toBeLessThanOrEqual(0)
expect(err404Size.endsWith('kB')).toBe(true)
expect(parseFloat(err404FirstLoad) - 63).toBeLessThanOrEqual(0)
......
import * as React from 'react'
/// @ts-ignore
import Document, { Main, NextScript, Head } from 'next/document'
export default class MyDocument extends Document {
constructor(props) {
super(props)
const { __NEXT_DATA__, ids } = props
if (ids) {
__NEXT_DATA__.ids = ids
}
}
render() {
return (
<html>
<Head>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Voces"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</html>
)
}
}
import React from 'react'
const Page = () => {
return <div>Hi!</div>
}
export default Page
import Head from 'next/head'
function Home({ stars }) {
return (
<div className="container">
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Roboto:wght@700"
></link>
</Head>
<main>
<div>Next stars: {stars}</div>
</main>
</div>
)
}
Home.getInitialProps = async () => {
return { stars: Math.random() * 1000 }
}
export default Home
import React from 'react'
import Head from 'next/head'
const Page = () => {
return (
<>
<Head>
<link
href="https://fonts.googleapis.com/css2?family=Modak"
rel="stylesheet"
/>
</Head>
<div>Hi!</div>
</>
)
}
export default Page
const http = require('http')
const url = require('url')
const fs = require('fs')
const path = require('path')
const server = http.createServer(async (req, res) => {
let { pathname } = url.parse(req.url)
pathname = pathname.replace(/\/$/, '')
let isDataReq = false
if (pathname.startsWith('/_next/data')) {
isDataReq = true
pathname = pathname
.replace(`/_next/data/${process.env.BUILD_ID}/`, '/')
.replace(/\.json$/, '')
}
console.log('serving', pathname)
if (pathname === '/favicon.ico') {
res.statusCode = 404
return res.end()
}
if (pathname.startsWith('/_next/static/')) {
res.write(
fs.readFileSync(
path.join(
__dirname,
'./.next/static/',
decodeURI(pathname.slice('/_next/static/'.length))
),
'utf8'
)
)
return res.end()
} else {
const ext = isDataReq ? 'json' : 'html'
if (
fs.existsSync(
path.join(__dirname, `./.next/serverless/pages${pathname}.${ext}`)
)
) {
res.write(
fs.readFileSync(
path.join(__dirname, `./.next/serverless/pages${pathname}.${ext}`),
'utf8'
)
)
return res.end()
}
let re
try {
re = require(`./.next/serverless/pages${pathname}`)
} catch {
const d = decodeURI(pathname)
if (
fs.existsSync(
path.join(__dirname, `./.next/serverless/pages${d}.${ext}`)
)
) {
res.write(
fs.readFileSync(
path.join(__dirname, `./.next/serverless/pages${d}.${ext}`),
'utf8'
)
)
return res.end()
}
const routesManifest = require('./.next/routes-manifest.json')
const { dynamicRoutes } = routesManifest
dynamicRoutes.some(({ page, regex }) => {
if (new RegExp(regex).test(pathname)) {
if (
fs.existsSync(
path.join(__dirname, `./.next/serverless/pages${page}.${ext}`)
)
) {
res.write(
fs.readFileSync(
path.join(__dirname, `./.next/serverless/pages${page}.${ext}`),
'utf8'
)
)
res.end()
return true
}
re = require(`./.next/serverless/pages${page}`)
return true
}
return false
})
}
if (!res.finished) {
try {
return await (typeof re.render === 'function'
? re.render(req, res)
: re.default(req, res))
} catch (e) {
console.log('FAIL_FUNCTION', e)
res.statusCode = 500
res.write('FAIL_FUNCTION')
res.end()
}
}
}
})
server.listen(process.env.PORT, () => {
console.log('ready on', process.env.PORT)
})
/* eslint-env jest */
import { join } from 'path'
import {
killApp,
findPort,
nextStart,
nextBuild,
renderViaHTTP,
initNextServerScript,
} from 'next-test-utils'
import fs from 'fs-extra'
jest.setTimeout(1000 * 30)
const appDir = join(__dirname, '../')
const nextConfig = join(appDir, 'next.config.js')
let builtServerPagesDir
let builtPage
let appPort
let app
const fsExists = (file) =>
fs
.access(file)
.then(() => true)
.catch(() => false)
async function getBuildId() {
return fs.readFile(join(appDir, '.next', 'BUILD_ID'), 'utf8')
}
const startServerlessEmulator = async (dir, port) => {
const scriptPath = join(dir, 'server.js')
const env = Object.assign(
{},
{ ...process.env },
{ PORT: port, BUILD_ID: await getBuildId() }
)
return initNextServerScript(scriptPath, /ready on/i, env)
}
function runTests() {
it('should inline the google fonts for static pages', async () => {
const html = await renderViaHTTP(appPort, '/index')
expect(await fsExists(builtPage('font-manifest.json'))).toBe(true)
expect(html).toContain(
'<link rel="stylesheet" data-href="https://fonts.googleapis.com/css?family=Voces"/>'
)
expect(html).toMatch(
/<style data-href="https:\/\/fonts\.googleapis\.com\/css\?family=Voces">.*<\/style>/
)
})
it('should inline the google fonts for static pages with Next/Head', async () => {
const html = await renderViaHTTP(appPort, '/static-head')
expect(await fsExists(builtPage('font-manifest.json'))).toBe(true)
expect(html).toContain(
'<link rel="stylesheet" data-href="https://fonts.googleapis.com/css2?family=Modak"/>'
)
expect(html).toMatch(
/<style data-href="https:\/\/fonts\.googleapis\.com\/css2\?family=Modak">.*<\/style>/
)
})
it('should inline the google fonts for SSR pages', async () => {
const html = await renderViaHTTP(appPort, '/stars')
expect(await fsExists(builtPage('font-manifest.json'))).toBe(true)
expect(html).toContain(
'<link rel="stylesheet" data-href="https://fonts.googleapis.com/css2?family=Roboto:wght@700"/>'
)
expect(html).toMatch(
/<style data-href="https:\/\/fonts\.googleapis\.com\/css2\?family=Roboto:wght@700">.*<\/style>/
)
})
}
describe('Font optimization for SSR apps', () => {
beforeAll(async () => {
await fs.writeFile(
nextConfig,
`module.exports = { experimental: {optimizeFonts: true} }`,
'utf8'
)
await nextBuild(appDir)
appPort = await findPort()
app = await nextStart(appDir, appPort)
builtServerPagesDir = join(appDir, '.next', 'server')
builtPage = (file) => join(builtServerPagesDir, file)
})
afterAll(() => killApp(app))
runTests()
})
describe('Font optimization for serverless apps', () => {
beforeAll(async () => {
await fs.writeFile(
nextConfig,
`module.exports = { target: 'serverless', experimental: {optimizeFonts: true} }`,
'utf8'
)
await nextBuild(appDir)
appPort = await findPort()
app = await nextStart(appDir, appPort)
builtServerPagesDir = join(appDir, '.next', 'serverless')
builtPage = (file) => join(builtServerPagesDir, file)
})
afterAll(() => killApp(app))
runTests()
})
describe('Font optimization for emulated serverless apps', () => {
beforeAll(async () => {
await fs.writeFile(
nextConfig,
`module.exports = { target: 'experimental-serverless-trace', experimental: {optimizeFonts: true} }`,
'utf8'
)
await nextBuild(appDir)
appPort = await findPort()
await startServerlessEmulator(appDir, appPort)
builtServerPagesDir = join(appDir, '.next', 'serverless')
builtPage = (file) => join(builtServerPagesDir, file)
})
afterAll(async () => {
await fs.remove(nextConfig)
})
runTests()
})
......@@ -7797,6 +7797,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"
......@@ -10826,6 +10831,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"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册