diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 2fc0253ba59a6..6b0a6e0a39025 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -204,6 +204,7 @@ export function getEdgeServerEntry(opts: { pagesType: opts.pagesType, appDirLoader: Buffer.from(opts.appDirLoader || '').toString('base64'), sriEnabled: !opts.isDev && !!opts.config.experimental.sri?.algorithm, + hasFontLoaders: !!opts.config.experimental.fontLoaders, } return { diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 29f82ca095d84..775d726bec4c4 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -59,6 +59,7 @@ import { APP_BUILD_MANIFEST, FLIGHT_SERVER_CSS_MANIFEST, RSC_MODULE_TYPES, + FONT_LOADER_MANIFEST, } from '../shared/lib/constants' import { getSortedRoutes, isDynamicRoute } from '../shared/lib/router/utils' import { __ApiPreviewProps } from '../server/api-utils' @@ -828,6 +829,9 @@ export default async function build( config.optimizeFonts ? path.join(serverDir, FONT_MANIFEST) : null, BUILD_ID_FILE, appDir ? path.join(serverDir, APP_PATHS_MANIFEST) : null, + config.experimental.fontLoaders + ? path.join(serverDir, FONT_LOADER_MANIFEST) + : null, ] .filter(nonNullable) .map((file) => path.join(config.distDir, file)), diff --git a/packages/next/build/swc/options.js b/packages/next/build/swc/options.js index 9cfc10bd9bfc5..bb8208b6dd6b4 100644 --- a/packages/next/build/swc/options.js +++ b/packages/next/build/swc/options.js @@ -123,6 +123,9 @@ function getBaseSWCOptions({ isServer: !!isServerLayer, } : false, + fontLoaders: + nextConfig?.experimental?.fontLoaders && + Object.keys(nextConfig.experimental.fontLoaders), } } diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index db71cbc61ea5c..b4260973ab2fa 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -61,6 +61,7 @@ import loadJsConfig from './load-jsconfig' import { loadBindings } from './swc' import { AppBuildManifestPlugin } from './webpack/plugins/app-build-manifest-plugin' import { SubresourceIntegrityPlugin } from './webpack/plugins/subresource-integrity-plugin' +import { FontLoaderManifestPlugin } from './webpack/plugins/font-loader-manifest-plugin' const NEXT_PROJECT_ROOT = pathJoin(__dirname, '..', '..') const NEXT_PROJECT_ROOT_DIST = pathJoin(NEXT_PROJECT_ROOT, 'dist') @@ -1508,6 +1509,7 @@ export default async function getBaseWebpackConfig( 'next-middleware-asset-loader', 'next-middleware-wasm-loader', 'next-app-loader', + 'next-font-loader', ].reduce((alias, loader) => { // using multiple aliases to replace `resolveLoader.modules` alias[loader] = path.join(__dirname, 'webpack', 'loaders', loader) @@ -1838,6 +1840,7 @@ export default async function getBaseWebpackConfig( new MiddlewarePlugin({ dev, sriEnabled: !dev && !!config.experimental.sri?.algorithm, + hasFontLoaders: !!config.experimental.fontLoaders, }), isClient && new BuildManifestPlugin({ @@ -1890,6 +1893,9 @@ export default async function getBaseWebpackConfig( isClient && !!config.experimental.sri?.algorithm && new SubresourceIntegrityPlugin(config.experimental.sri.algorithm), + isClient && + config.experimental.fontLoaders && + new FontLoaderManifestPlugin(), !dev && isClient && new (require('./webpack/plugins/telemetry-plugin').TelemetryPlugin)( diff --git a/packages/next/build/webpack/config/blocks/css/index.ts b/packages/next/build/webpack/config/blocks/css/index.ts index 5b81ffc7da103..3ebc34d9e74bd 100644 --- a/packages/next/build/webpack/config/blocks/css/index.ts +++ b/packages/next/build/webpack/config/blocks/css/index.ts @@ -3,11 +3,13 @@ import { webpack } from 'next/dist/compiled/webpack/webpack' import { loader, plugin } from '../../helpers' import { ConfigurationContext, ConfigurationFn, pipe } from '../../utils' import { getCssModuleLoader, getGlobalCssLoader } from './loaders' +import { getFontLoader } from './loaders/font-loader' import { getCustomDocumentError, getGlobalImportError, getGlobalModuleImportError, getLocalModuleImportError, + getFontLoaderDocumentImportError, } from './messages' import { getPostCssPlugins } from './plugins' @@ -199,6 +201,59 @@ export const css = curry(async function css( }) ) + // Resolve the configured font loaders, the resolved files are noop files that next-font-loader will match + let fontLoaders: [string, string][] | undefined = ctx.experimental.fontLoaders + ? Object.entries(ctx.experimental.fontLoaders).map( + ([fontLoader, fontLoaderOptions]: any) => [ + require.resolve(fontLoader), + fontLoaderOptions, + ] + ) + : undefined + + // Font loaders cannot be imported in _document. + fontLoaders?.forEach(([fontLoaderPath, fontLoaderOptions]) => { + fns.push( + loader({ + oneOf: [ + markRemovable({ + test: fontLoaderPath, + // Use a loose regex so we don't have to crawl the file system to + // find the real file name (if present). + issuer: /pages[\\/]_document\./, + use: { + loader: 'error-loader', + options: { + reason: getFontLoaderDocumentImportError(), + }, + }, + }), + ], + }) + ) + + // Matches the resolved font loaders noop files to run next-font-loader + fns.push( + loader({ + oneOf: [ + markRemovable({ + sideEffects: false, + test: fontLoaderPath, + issuer: { + and: [ + { + or: [ctx.rootDirectory, regexClientEntry], + }, + ], + not: [/node_modules/], + }, + use: getFontLoader(ctx, lazyPostCSSInitializer, fontLoaderOptions), + }), + ], + }) + ) + }) + // CSS Modules support must be enabled on the server and client so the class // names are available for SSR or Prerendering. if (ctx.experimental.appDir && !ctx.isProduction) { diff --git a/packages/next/build/webpack/config/blocks/css/loaders/font-loader.ts b/packages/next/build/webpack/config/blocks/css/loaders/font-loader.ts new file mode 100644 index 0000000000000..c48efd1d75916 --- /dev/null +++ b/packages/next/build/webpack/config/blocks/css/loaders/font-loader.ts @@ -0,0 +1,70 @@ +import { webpack } from 'next/dist/compiled/webpack/webpack' +import { ConfigurationContext } from '../../../utils' +import { getClientStyleLoader } from './client' +import { cssFileResolve } from './file-resolve' + +export function getFontLoader( + ctx: ConfigurationContext, + postcss: any, + fontLoaderOptions: any +): webpack.RuleSetUseItem[] { + const loaders: webpack.RuleSetUseItem[] = [] + + if (ctx.isClient) { + // Add appropriate development mode or production mode style + // loader + loaders.push( + getClientStyleLoader({ + isAppDir: !!ctx.experimental.appDir, + isDevelopment: ctx.isDevelopment, + assetPrefix: ctx.assetPrefix, + }) + ) + } + + loaders.push({ + loader: require.resolve('../../../../loaders/css-loader/src'), + options: { + postcss, + importLoaders: 1, + // Use CJS mode for backwards compatibility: + esModule: false, + url: (url: string, resourcePath: string) => + cssFileResolve(url, resourcePath, ctx.experimental.urlImports), + import: (url: string, _: any, resourcePath: string) => + cssFileResolve(url, resourcePath, ctx.experimental.urlImports), + modules: { + // Do not transform class names (CJS mode backwards compatibility): + exportLocalsConvention: 'asIs', + // Server-side (Node.js) rendering support: + exportOnlyLocals: ctx.isServer, + // Disallow global style exports so we can code-split CSS and + // not worry about loading order. + mode: 'pure', + getLocalIdent: ( + _context: any, + _localIdentName: any, + exportName: string, + _options: any, + meta: any + ) => { + // hash from next-font-loader + return `__${exportName}_${meta.fontFamilyHash}` + }, + }, + fontLoader: true, + }, + }) + + loaders.push({ + loader: 'next-font-loader', + options: { + isServer: ctx.isServer, + assetPrefix: ctx.assetPrefix, + fontLoaderOptions, + postcss, + }, + }) + + return loaders +} diff --git a/packages/next/build/webpack/config/blocks/css/messages.ts b/packages/next/build/webpack/config/blocks/css/messages.ts index 61c62c994fc34..42b5c720e54ba 100644 --- a/packages/next/build/webpack/config/blocks/css/messages.ts +++ b/packages/next/build/webpack/config/blocks/css/messages.ts @@ -31,3 +31,9 @@ export function getCustomDocumentError() { 'pages/_document.js' )}. Please move global styles to ${chalk.cyan('pages/_app.js')}.` } + +export function getFontLoaderDocumentImportError() { + return `Font loaders ${chalk.bold('cannot')} be used within ${chalk.cyan( + 'pages/_document.js' + )}.` +} diff --git a/packages/next/build/webpack/loaders/css-loader/src/index.js b/packages/next/build/webpack/loaders/css-loader/src/index.js index eae004f556ea7..bc259cdfefee3 100644 --- a/packages/next/build/webpack/loaders/css-loader/src/index.js +++ b/packages/next/build/webpack/loaders/css-loader/src/index.js @@ -4,7 +4,6 @@ */ import CssSyntaxError from './CssSyntaxError' import Warning from '../../postcss-loader/src/Warning' -// import { icssParser, importParser, urlParser } from './plugins' import { stringifyRequest } from '../../../stringify-request' const moduleRegExp = /\.module\.\w+$/i @@ -128,6 +127,7 @@ function normalizeOptions(rawOptions, loaderContext) { : rawOptions.importLoaders, esModule: typeof rawOptions.esModule === 'undefined' ? true : rawOptions.esModule, + fontLoader: rawOptions.fontLoader, } } @@ -169,10 +169,11 @@ export default async function loader(content, map, meta) { const { icssParser, importParser, urlParser } = require('./plugins') const replacements = [] - const exports = [] + // if it's a font loader next-font-loader will have exports that should be exported as is + const exports = options.fontLoader ? meta.exports : [] if (shouldUseModulesPlugins(options)) { - plugins.push(...getModulesPlugins(options, this)) + plugins.push(...getModulesPlugins(options, this, meta)) } const importPluginImports = [] diff --git a/packages/next/build/webpack/loaders/css-loader/src/utils.js b/packages/next/build/webpack/loaders/css-loader/src/utils.js index 0ab83b2fd9ec6..328800a9317c8 100644 --- a/packages/next/build/webpack/loaders/css-loader/src/utils.js +++ b/packages/next/build/webpack/loaders/css-loader/src/utils.js @@ -135,7 +135,7 @@ function shouldUseIcssPlugin(options) { return options.icss === true || Boolean(options.modules) } -function getModulesPlugins(options, loaderContext) { +function getModulesPlugins(options, loaderContext, meta) { const { mode, getLocalIdent, @@ -154,11 +154,17 @@ function getModulesPlugins(options, loaderContext) { extractImports(), modulesScope({ generateScopedName(exportName) { - return getLocalIdent(loaderContext, localIdentName, exportName, { - context: localIdentContext, - hashPrefix: localIdentHashPrefix, - regExp: localIdentRegExp, - }) + return getLocalIdent( + loaderContext, + localIdentName, + exportName, + { + context: localIdentContext, + hashPrefix: localIdentHashPrefix, + regExp: localIdentRegExp, + }, + meta + ) }, exportGlobals: options.modules.exportGlobals, }), diff --git a/packages/next/build/webpack/loaders/next-edge-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-edge-ssr-loader/index.ts index 889a9ebe8bdff..a04f5c9979b9c 100644 --- a/packages/next/build/webpack/loaders/next-edge-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-edge-ssr-loader/index.ts @@ -15,6 +15,7 @@ export type EdgeSSRLoaderQuery = { appDirLoader?: string pagesType?: 'app' | 'pages' | 'root' sriEnabled: boolean + hasFontLoaders: boolean } export default async function edgeSSRLoader(this: any) { @@ -32,6 +33,7 @@ export default async function edgeSSRLoader(this: any) { appDirLoader: appDirLoaderBase64, pagesType, sriEnabled, + hasFontLoaders, } = this.getOptions() const appDirLoader = Buffer.from( @@ -103,6 +105,9 @@ export default async function edgeSSRLoader(this: any) { const subresourceIntegrityManifest = ${ sriEnabled ? 'self.__SUBRESOURCE_INTEGRITY_MANIFEST' : 'undefined' } + const fontLoaderManifest = ${ + hasFontLoaders ? 'self.__FONT_LOADER_MANIFEST' : 'undefined' + } const render = getRender({ pageType, @@ -122,6 +127,7 @@ export default async function edgeSSRLoader(this: any) { subresourceIntegrityManifest, config: ${stringifiedConfig}, buildId: ${JSON.stringify(buildId)}, + fontLoaderManifest, }) export const ComponentMod = pageMod diff --git a/packages/next/build/webpack/loaders/next-edge-ssr-loader/render.ts b/packages/next/build/webpack/loaders/next-edge-ssr-loader/render.ts index 128ccbe478250..23c175a5a77cd 100644 --- a/packages/next/build/webpack/loaders/next-edge-ssr-loader/render.ts +++ b/packages/next/build/webpack/loaders/next-edge-ssr-loader/render.ts @@ -2,6 +2,7 @@ import type { NextConfig } from '../../../../server/config-shared' import type { DocumentType, AppType } from '../../../../shared/lib/utils' import type { BuildManifest } from '../../../../server/get-page-files' import type { ReactLoadableManifest } from '../../../../server/load-components' +import type { FontLoaderManifest } from '../../plugins/font-loader-manifest-plugin' import WebServer from '../../../../server/web-server' import { @@ -28,6 +29,7 @@ export function getRender({ serverCSSManifest, config, buildId, + fontLoaderManifest, }: { pagesType?: 'app' | 'pages' | 'root' dev: boolean @@ -47,6 +49,7 @@ export function getRender({ appServerMod: any config: NextConfig buildId: string + fontLoaderManifest: FontLoaderManifest }) { const isAppPath = pagesType === 'app' const baseLoadComponentResult = { @@ -54,6 +57,7 @@ export function getRender({ buildManifest, reactLoadableManifest, subresourceIntegrityManifest, + fontLoaderManifest, Document, App: appMod?.default as AppType, } diff --git a/packages/next/build/webpack/loaders/next-font-loader/index.ts b/packages/next/build/webpack/loaders/next-font-loader/index.ts new file mode 100644 index 0000000000000..196ac837ceb11 --- /dev/null +++ b/packages/next/build/webpack/loaders/next-font-loader/index.ts @@ -0,0 +1,83 @@ +import path from 'path' +import loaderUtils from 'next/dist/compiled/loader-utils3' +import postcssFontLoaderPlugn from './postcss-font-loader' + +type FontLoader = (options: { + functionName: string + data: any[] + config: any + emitFontFile: (content: Buffer, ext: string, preload: boolean) => string +}) => Promise<{ css: string; fallbackFonts: string[] }> + +export default async function nextFontLoader(this: any) { + const fontLoaderSpan = this.currentTraceSpan.traceChild('next-font-loader') + return fontLoaderSpan.traceAsyncFn(async () => { + const callback = this.async() + const { + isServer, + assetPrefix, + fontLoaderOptions, + postcss: getPostcss, + } = this.getOptions() + + const emitFontFile = (content: Buffer, ext: string, preload: boolean) => { + const opts = { context: this.rootContext, content } + const interpolatedName = loaderUtils.interpolateName( + this, + // Font files ending with .p.(woff|woff2|eot|ttf|otf) are preloaded + `static/fonts/[hash]${preload ? '.p' : ''}.${ext}`, + opts + ) + const outputPath = `${assetPrefix}/_next/${interpolatedName}` + if (!isServer) { + this.emitFile(interpolatedName, content, null) + } + return outputPath + } + + // next-swc next_font_loaders turns each function call argument into JSON seperated by semicolons + let [functionName, ...data] = this.resourceQuery.slice(1).split(';') + data = data.map((value: string) => JSON.parse(value)) + + try { + const fontLoader: FontLoader = require(path.join( + this.resourcePath, + '../loader.js' + )).default + let { css, fallbackFonts } = await fontLoader({ + functionName, + data, + config: fontLoaderOptions, + emitFontFile, + }) + + const { postcss } = await getPostcss() + + // Exports will be exported as is from css-loader instead of a CSS module export + const exports: { name: any; value: any }[] = [] + const fontFamilyHash = loaderUtils.getHashDigest( + Buffer.from(css), + 'md5', + 'hex', + 6 + ) + // Add CSS classes, exports and make the font-family localy scoped by turning it unguessable + const result = await postcss( + postcssFontLoaderPlugn(exports, fontFamilyHash, fallbackFonts) + ).process(css, { + from: undefined, + }) + + // Reuse ast in css-loader + const ast = { + type: 'postcss', + version: result.processor.version, + root: result.root, + } + callback(null, result.css, null, { exports, ast, fontFamilyHash }) + } catch (err: any) { + err.stack = false + callback(err) + } + }) +} diff --git a/packages/next/build/webpack/loaders/next-font-loader/postcss-font-loader.ts b/packages/next/build/webpack/loaders/next-font-loader/postcss-font-loader.ts new file mode 100644 index 0000000000000..08313dd565df3 --- /dev/null +++ b/packages/next/build/webpack/loaders/next-font-loader/postcss-font-loader.ts @@ -0,0 +1,135 @@ +import postcss, { Declaration } from 'postcss' + +const postcssFontLoaderPlugn = ( + exports: { name: any; value: any }[], + fontFamilyHash: string, + fallbackFonts: string[] = [] +) => { + return { + postcssPlugin: 'postcss-font-loader', + Once(root: any) { + const fontFamilies: string[] = [] + let rawFamily: string | undefined + let fontWeight: string | undefined + let fontStyle: string | undefined + + const formatFamily = (family: string) => { + if (family[0] === "'" || family[0] === '"') { + family = family.slice(1, family.length - 1) + } + // Turn the font family unguessable to make it localy scoped + return `'__${family.replace(/ /g, '_')}_${fontFamilyHash}'` + } + + for (const node of root.nodes) { + if (node.type === 'atrule' && node.name === 'font-face') { + const familyNode = node.nodes.find( + (decl: Declaration) => decl.prop === 'font-family' + ) + if (!familyNode) { + continue + } + + if (!rawFamily) { + let family: string = familyNode.value + if (family[0] === "'" || family[0] === '"') { + family = family.slice(1, family.length - 1) + } + rawFamily = family + } + const formattedFamily = formatFamily(familyNode.value) + familyNode.value = formattedFamily + + if (fontFamilies.includes(formattedFamily)) { + continue + } + fontFamilies.push(formattedFamily) + + // Only extract weight and style from first encountered family, the rest will treated as fallbacks + if (fontFamilies.length > 1) { + continue + } + + // Extract weight and style from first encountered @font-face + const weight = node.nodes.find( + (decl: Declaration) => decl.prop === 'font-weight' + ) + + // Skip if the value includes ' ', then it's a range of possible values + if (weight && !weight.value.includes(' ')) { + fontWeight = weight.value + } + + const style = node.nodes.find( + (decl: Declaration) => decl.prop === 'font-style' + ) + // Skip if the value includes ' ', then it's a range of possible values + if (style && !style.value.includes(' ')) { + fontStyle = style.value + } + } + } + + const [mainFontFamily, ...adjustFontFallbacks] = fontFamilies + // If fallback fonts were provided from the font loader, they should be used before the adjustFontFallbacks + const formattedFontFamilies = [ + mainFontFamily, + ...fallbackFonts, + ...adjustFontFallbacks, + ].join(', ') + // Add class with family, weight and style + const classRule = new postcss.Rule({ selector: '.className' }) + classRule.nodes = [ + new postcss.Declaration({ + prop: 'font-family', + value: formattedFontFamilies, + }), + ...(fontWeight + ? [ + new postcss.Declaration({ + prop: 'font-weight', + value: fontWeight, + }), + ] + : []), + ...(fontStyle + ? [ + new postcss.Declaration({ + prop: 'font-style', + value: fontStyle, + }), + ] + : []), + ] + root.nodes.push(classRule) + + // Add class that defines a variable with the font family + const varialbeRule = new postcss.Rule({ selector: '.variable' }) + varialbeRule.nodes = [ + new postcss.Declaration({ + prop: rawFamily + ? `--next-font-${rawFamily.toLowerCase().replace(/ /g, '-')}${ + fontWeight ? `-${fontWeight}` : '' + }${fontStyle === 'italic' ? `-${fontStyle}` : ''}` + : '', + value: formattedFontFamilies, + }), + ] + root.nodes.push(varialbeRule) + + // Export @font-face values as is + exports.push({ + name: 'style', + value: { + fontFamily: formattedFontFamilies, + fontWeight: fontWeight && Number(fontWeight), + fontStyle, + }, + }) + }, + } +} + +postcssFontLoaderPlugn.postcss = true + +export default postcssFontLoaderPlugn diff --git a/packages/next/build/webpack/plugins/font-loader-manifest-plugin.ts b/packages/next/build/webpack/plugins/font-loader-manifest-plugin.ts new file mode 100644 index 0000000000000..c68992af80e14 --- /dev/null +++ b/packages/next/build/webpack/plugins/font-loader-manifest-plugin.ts @@ -0,0 +1,62 @@ +import { webpack, sources } from 'next/dist/compiled/webpack/webpack' +import getRouteFromEntrypoint from '../../../server/get-route-from-entrypoint' +import { FONT_LOADER_MANIFEST } from '../../../shared/lib/constants' + +export type FontLoaderManifest = { + pages: { + [path: string]: string[] + } +} +const PLUGIN_NAME = 'FontLoaderManifestPlugin' + +// Creates a manifest of all fonts that should be preloaded given a route +export class FontLoaderManifestPlugin { + apply(compiler: webpack.Compiler) { + compiler.hooks.make.tap(PLUGIN_NAME, (compilation) => { + compilation.hooks.processAssets.tap( + { + name: PLUGIN_NAME, + stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, + }, + (assets: any) => { + const fontLoaderManifest: FontLoaderManifest = { + pages: {}, + } + + for (const entrypoint of compilation.entrypoints.values()) { + const pagePath = getRouteFromEntrypoint(entrypoint.name!) + + if (!pagePath) { + continue + } + + const fontFiles: string[] = entrypoint.chunks + .flatMap((chunk: any) => [...chunk.auxiliaryFiles]) + .filter((file: string) => + /\.(woff|woff2|eot|ttf|otf)$/.test(file) + ) + + // Font files ending with .p.(woff|woff2|eot|ttf|otf) are preloaded + const preloadedFontFiles: string[] = fontFiles.filter( + (file: string) => /\.p.(woff|woff2|eot|ttf|otf)$/.test(file) + ) + + // Create an entry for the path even if no files should preload. If that's the case a preconnect tag is added. + if (fontFiles.length > 0) { + fontLoaderManifest.pages[pagePath] = preloadedFontFiles + } + } + + const manifest = JSON.stringify(fontLoaderManifest, null, 2) + assets[`server/${FONT_LOADER_MANIFEST}.js`] = new sources.RawSource( + `self.__FONT_LOADER_MANIFEST=${manifest}` + ) + assets[`server/${FONT_LOADER_MANIFEST}.json`] = new sources.RawSource( + manifest + ) + } + ) + }) + return + } +} diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index cee3db5e520e0..d87d9301b586a 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -19,6 +19,7 @@ import { NEXT_CLIENT_SSR_ENTRY_SUFFIX, FLIGHT_SERVER_CSS_MANIFEST, SUBRESOURCE_INTEGRITY_MANIFEST, + FONT_LOADER_MANIFEST, } from '../../../shared/lib/constants' import { getPageStaticInfo, @@ -85,7 +86,7 @@ function isUsingIndirectEvalAndUsedByExports(args: { function getEntryFiles( entryFiles: string[], meta: EntryMetadata, - opts: { sriEnabled: boolean } + opts: { sriEnabled: boolean; hasFontLoaders: boolean } ) { const files: string[] = [] if (meta.edgeSSR) { @@ -114,6 +115,10 @@ function getEntryFiles( `server/${MIDDLEWARE_BUILD_MANIFEST}.js`, `server/${MIDDLEWARE_REACT_LOADABLE_MANIFEST}.js` ) + + if (opts.hasFontLoaders) { + files.push(`server/${FONT_LOADER_MANIFEST}.js`) + } } files.push( @@ -127,7 +132,7 @@ function getEntryFiles( function getCreateAssets(params: { compilation: webpack.Compilation metadataByEntry: Map - opts: { sriEnabled: boolean } + opts: { sriEnabled: boolean; hasFontLoaders: boolean } }) { const { compilation, metadataByEntry, opts } = params return (assets: any) => { @@ -790,10 +795,20 @@ function getExtractMetadata(params: { export default class MiddlewarePlugin { private readonly dev: boolean private readonly sriEnabled: boolean - - constructor({ dev, sriEnabled }: { dev: boolean; sriEnabled: boolean }) { + private readonly hasFontLoaders: boolean + + constructor({ + dev, + sriEnabled, + hasFontLoaders, + }: { + dev: boolean + sriEnabled: boolean + hasFontLoaders: boolean + }) { this.dev = dev this.sriEnabled = sriEnabled + this.hasFontLoaders = hasFontLoaders } public apply(compiler: webpack.Compiler) { @@ -836,7 +851,10 @@ export default class MiddlewarePlugin { getCreateAssets({ compilation, metadataByEntry, - opts: { sriEnabled: this.sriEnabled }, + opts: { + sriEnabled: this.sriEnabled, + hasFontLoaders: this.hasFontLoaders, + }, }) ) }) diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index 8a5d48ab17a69..0106566a967cf 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -24,6 +24,7 @@ import { EXPORT_MARKER, FLIGHT_MANIFEST, FLIGHT_SERVER_CSS_MANIFEST, + FONT_LOADER_MANIFEST, PAGES_MANIFEST, PHASE_EXPORT, PRERENDER_MANIFEST, @@ -389,6 +390,9 @@ export default async function exportApp( optimizeFonts: nextConfig.optimizeFonts as FontConfig, largePageDataBytes: nextConfig.experimental.largePageDataBytes, serverComponents: !!nextConfig.experimental.appDir, + fontLoaderManifest: nextConfig.experimental.fontLoaders + ? require(join(distDir, 'server', `${FONT_LOADER_MANIFEST}.json`)) + : undefined, } const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index 26564e04cc50b..0733e5e077a17 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -11,6 +11,7 @@ import type { NEXT_DATA, } from '../shared/lib/utils' import type { ScriptProps } from '../client/script' +import type { FontLoaderManifest } from '../build/webpack/plugins/font-loader-manifest-plugin' import { BuildManifest, getPageFiles } from '../server/get-page-files' import { htmlEscapeJsonString } from '../server/htmlescape' @@ -353,6 +354,54 @@ function getAmpPath(ampPath: string, asPath: string): string { return ampPath || `${asPath}${asPath.includes('?') ? '&' : '?'}amp=1` } +function getFontLoaderLinks( + fontLoaderManifest: FontLoaderManifest | undefined, + dangerousAsPath: string, + assetPrefix: string = '' +) { + if (!fontLoaderManifest) { + return { + preconnect: null, + preload: null, + } + } + + const appFontsEntry = fontLoaderManifest.pages['/_app'] + const pageFontsEntry = fontLoaderManifest.pages[dangerousAsPath] + + const preloadedFontFiles = [ + ...(appFontsEntry ?? []), + ...(pageFontsEntry ?? []), + ] + + // If no font files should preload but there's an entry for the path, add a preconnect tag. + const preconnectToSelf = !!( + preloadedFontFiles.length === 0 && + (appFontsEntry || pageFontsEntry) + ) + + return { + preconnect: preconnectToSelf ? ( + + ) : null, + preload: preloadedFontFiles + ? preloadedFontFiles.map((fontFile) => { + const ext = /\.(woff|woff2|eot|ttf|otf)$/.exec(fontFile)![1] + return ( + + ) + }) + : null, + } +} + // Use `React.Component` to avoid errors from the RSC checks because // it can't be imported directly in Server Components: // @@ -608,6 +657,8 @@ export class Head extends React.Component { disableOptimizedLoading, optimizeCss, optimizeFonts, + assetPrefix, + fontLoaderManifest, } = this.context const disableRuntimeJS = unstable_runtimeJS === false @@ -722,6 +773,12 @@ export class Head extends React.Component { process.env.NEXT_RUNTIME !== 'edge' && inAmpMode ) + const fontLoaderLinks = getFontLoaderLinks( + fontLoaderManifest, + dangerousAsPath, + assetPrefix + ) + return ( {this.context.isDevelopment && ( @@ -762,6 +819,9 @@ export class Head extends React.Component { {children} {optimizeFonts && } + {fontLoaderLinks.preconnect} + {fontLoaderLinks.preload} + {process.env.NEXT_RUNTIME !== 'edge' && inAmpMode && ( <> { supportsDynamicHTML?: boolean serverComponentManifest?: any serverCSSManifest?: any + fontLoaderManifest?: FontLoaderManifest renderServerComponentData?: boolean serverComponentProps?: any largePageDataBytes?: number @@ -230,6 +232,7 @@ export default abstract class Server { protected customRoutes: CustomRoutes protected serverComponentManifest?: any protected serverCSSManifest?: any + protected fontLoaderManifest?: FontLoaderManifest public readonly hostname?: string public readonly port?: number @@ -252,6 +255,7 @@ export default abstract class Server { protected abstract getPrerenderManifest(): PrerenderManifest protected abstract getServerComponentManifest(): any protected abstract getServerCSSManifest(): any + protected abstract getFontLoaderManifest(): FontLoaderManifest | undefined protected abstract attachRequestMeta( req: BaseNextRequest, parsedUrl: NextUrlWithParsedQuery @@ -370,6 +374,9 @@ export default abstract class Server { this.serverCSSManifest = serverComponents ? this.getServerCSSManifest() : undefined + this.fontLoaderManifest = this.nextConfig.experimental.fontLoaders + ? this.getFontLoaderManifest() + : undefined this.renderOpts = { poweredByHeader: this.nextConfig.poweredByHeader, diff --git a/packages/next/server/config-schema.ts b/packages/next/server/config-schema.ts index 1592d0624e8a8..f330036276f68 100644 --- a/packages/next/server/config-schema.ts +++ b/packages/next/server/config-schema.ts @@ -387,6 +387,9 @@ const configSchema = { workerThreads: { type: 'boolean', }, + fontLoaders: { + type: 'object', + }, }, type: 'object', }, diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts index d0612a7d4fb35..0ae07ad1ecdcf 100644 --- a/packages/next/server/config-shared.ts +++ b/packages/next/server/config-shared.ts @@ -150,6 +150,7 @@ export interface ExperimentalConfig { algorithm?: SubresourceIntegrityAlgorithm } adjustFontFallbacks?: boolean + fontLoaders?: { [fontLoader: string]: any } } export type ExportPathMap = { diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 9128481ca70b2..cea2e139c78ba 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -1126,6 +1126,10 @@ export default class DevServer extends Server { return undefined } + protected getFontLoaderManifest() { + return undefined + } + protected async hasMiddleware(): Promise { return this.hasPage(this.actualMiddlewareFile!) } @@ -1353,6 +1357,7 @@ export default class DevServer extends Server { this.serverComponentManifest = super.getServerComponentManifest() this.serverCSSManifest = super.getServerCSSManifest() } + this.fontLoaderManifest = super.getFontLoaderManifest() return super.findPageComponents({ pathname, query, params, isAppPath }) } catch (err) { diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 3a077b90aa16e..e04669a71dab9 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -45,6 +45,7 @@ import { FLIGHT_SERVER_CSS_MANIFEST, SERVERLESS_DIRECTORY, SERVER_DIRECTORY, + FONT_LOADER_MANIFEST, } from '../shared/lib/constants' import { recursiveReadDirSync } from './lib/recursive-readdir-sync' import { format as formatUrl, UrlWithParsedQuery } from 'url' @@ -524,7 +525,8 @@ export default class NextNodeServer extends BaseServer { params.path[0] === 'media' || params.path[0] === this.buildId || params.path[0] === 'pages' || - params.path[1] === 'pages' + params.path[1] === 'pages' || + params.path[0] === 'fonts' ) { this.setImmutableAssetCacheControl(res) } @@ -822,6 +824,7 @@ export default class NextNodeServer extends BaseServer { // https://github.com/vercel/next.js/blob/df7cbd904c3bd85f399d1ce90680c0ecf92d2752/packages/next/server/render.tsx#L947-L952 renderOpts.serverComponentManifest = this.serverComponentManifest renderOpts.serverCSSManifest = this.serverCSSManifest + renderOpts.fontLoaderManifest = this.fontLoaderManifest if ( this.nextConfig.experimental.appDir && @@ -1016,6 +1019,11 @@ export default class NextNodeServer extends BaseServer { )) } + protected getFontLoaderManifest() { + if (!this.nextConfig.experimental.fontLoaders) return undefined + return require(join(this.distDir, 'server', `${FONT_LOADER_MANIFEST}.json`)) + } + protected getFallback(page: string): Promise { page = normalizePagePath(page) const cacheFs = this.getCacheFilesystem() diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 304167ef843d8..5e5a0a8aed862 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -26,6 +26,7 @@ import type { } from 'next/types' import type { UnwrapPromise } from '../lib/coalesced-function' import type { ReactReadableStream } from './node-web-streams-helper' +import type { FontLoaderManifest } from '../build/webpack/plugins/font-loader-manifest-plugin' import React from 'react' import { StyleRegistry, createStyleRegistry } from 'styled-jsx' @@ -232,6 +233,7 @@ export type RenderOptsPartial = { resolvedAsPath?: string serverComponentManifest?: any serverCSSManifest?: any + fontLoaderManifest?: FontLoaderManifest distDir?: string locale?: string locales?: string[] @@ -1446,6 +1448,7 @@ export async function renderToHTML( nextScriptWorkers: renderOpts.nextScriptWorkers, runtime: globalRuntime, largePageDataBytes: renderOpts.largePageDataBytes, + fontLoaderManifest: renderOpts.fontLoaderManifest, } const document = ( diff --git a/packages/next/server/web-server.ts b/packages/next/server/web-server.ts index 2326ec5bf8136..77fe205d393d0 100644 --- a/packages/next/server/web-server.ts +++ b/packages/next/server/web-server.ts @@ -131,6 +131,11 @@ export default class NextWebServer extends BaseServer { return this.serverOptions.webServerConfig.extendRenderOpts.serverCSSManifest } + protected getFontLoaderManifest() { + return this.serverOptions.webServerConfig.extendRenderOpts + .fontLoaderManifest + } + protected generateRoutes(): { headers: Route[] rewrites: { diff --git a/packages/next/shared/lib/constants.ts b/packages/next/shared/lib/constants.ts index fe527c83685e7..e9c43bdd4cf36 100644 --- a/packages/next/shared/lib/constants.ts +++ b/packages/next/shared/lib/constants.ts @@ -27,6 +27,7 @@ export const APP_PATH_ROUTES_MANIFEST = 'app-path-routes-manifest.json' export const BUILD_MANIFEST = 'build-manifest.json' export const APP_BUILD_MANIFEST = 'app-build-manifest.json' export const SUBRESOURCE_INTEGRITY_MANIFEST = 'subresource-integrity-manifest' +export const FONT_LOADER_MANIFEST = 'font-loader-manifest' export const EXPORT_MARKER = 'export-marker.json' export const EXPORT_DETAIL = 'export-detail.json' export const PRERENDER_MANIFEST = 'prerender-manifest.json' diff --git a/packages/next/shared/lib/html-context.ts b/packages/next/shared/lib/html-context.ts index 57b08a6972533..254db0636cf4b 100644 --- a/packages/next/shared/lib/html-context.ts +++ b/packages/next/shared/lib/html-context.ts @@ -2,6 +2,7 @@ import type { BuildManifest } from '../../server/get-page-files' import type { ServerRuntime } from 'next/types' import type { NEXT_DATA } from './utils' import type { FontConfig } from '../../server/font-utils' +import type { FontLoaderManifest } from '../../build/webpack/plugins/font-loader-manifest-plugin' import { createContext } from 'react' @@ -42,6 +43,7 @@ export type HtmlProps = { runtime?: ServerRuntime hasConcurrentFeatures?: boolean largePageDataBytes?: number + fontLoaderManifest?: FontLoaderManifest } export const HtmlContext = createContext(null as any) diff --git a/test/development/next-font/font-loader-in-document-error.test.ts b/test/development/next-font/font-loader-in-document-error.test.ts new file mode 100644 index 0000000000000..403da95bd1d58 --- /dev/null +++ b/test/development/next-font/font-loader-in-document-error.test.ts @@ -0,0 +1,32 @@ +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { check, getRedboxSource } from 'next-test-utils' +import webdriver from 'next-webdriver' +import { join } from 'path' + +describe('font-loader-in-document-error', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: { + pages: new FileRef(join(__dirname, 'font-loader-in-document/pages')), + 'next.config.js': new FileRef( + join(__dirname, 'font-loader-in-document/next.config.js') + ), + }, + dependencies: { + '@next/font': 'canary', + }, + }) + }) + afterAll(() => next.destroy()) + + test('font loader inside _document', async () => { + const browser = await webdriver(next.appPort, '/') + await check(() => getRedboxSource(browser), /Font loaders/) + expect(await getRedboxSource(browser)).toInclude( + 'Font loaders cannot be used within pages/_document.js' + ) + }) +}) diff --git a/test/development/next-font/font-loader-in-document/next.config.js b/test/development/next-font/font-loader-in-document/next.config.js new file mode 100644 index 0000000000000..6cd855478a746 --- /dev/null +++ b/test/development/next-font/font-loader-in-document/next.config.js @@ -0,0 +1,9 @@ +module.exports = { + experimental: { + fontLoaders: { + '@next/font/google': { + subsets: ['latin'], + }, + }, + }, +} diff --git a/test/development/next-font/font-loader-in-document/pages/_document.js b/test/development/next-font/font-loader-in-document/pages/_document.js new file mode 100644 index 0000000000000..4e50cbee8c11e --- /dev/null +++ b/test/development/next-font/font-loader-in-document/pages/_document.js @@ -0,0 +1,17 @@ +import { Html, Head, Main, NextScript } from 'next/document' +import { Abel } from '@next/font/google' + +// eslint-disable-next-line no-unused-vars +const abel = Abel({ variant: '400' }) + +export default function Document() { + return ( + + + +
+ + + + ) +} diff --git a/test/development/next-font/font-loader-in-document/pages/index.js b/test/development/next-font/font-loader-in-document/pages/index.js new file mode 100644 index 0000000000000..71c4bddbe5455 --- /dev/null +++ b/test/development/next-font/font-loader-in-document/pages/index.js @@ -0,0 +1,3 @@ +export default function Index() { + return

Hello world

+} diff --git a/test/e2e/next-font/app/components/CompWithFonts.js b/test/e2e/next-font/app/components/CompWithFonts.js new file mode 100644 index 0000000000000..f82d59672c9a2 --- /dev/null +++ b/test/e2e/next-font/app/components/CompWithFonts.js @@ -0,0 +1,20 @@ +import { Inter, Roboto } from '@next/font/google' +const inter = Inter({ variant: '900', display: 'swap', preload: false }) +const roboto = Roboto({ + variant: '100-italic', + display: 'swap', + preload: true, +}) + +export default function Component() { + return ( + <> +
+ {JSON.stringify(inter)} +
+
+ {JSON.stringify(roboto)} +
+ + ) +} diff --git a/test/e2e/next-font/app/next.config.js b/test/e2e/next-font/app/next.config.js new file mode 100644 index 0000000000000..6cd855478a746 --- /dev/null +++ b/test/e2e/next-font/app/next.config.js @@ -0,0 +1,9 @@ +module.exports = { + experimental: { + fontLoaders: { + '@next/font/google': { + subsets: ['latin'], + }, + }, + }, +} diff --git a/test/e2e/next-font/app/pages/_app.js b/test/e2e/next-font/app/pages/_app.js new file mode 100644 index 0000000000000..1deef62049a1c --- /dev/null +++ b/test/e2e/next-font/app/pages/_app.js @@ -0,0 +1,16 @@ +import { Open_Sans } from '@next/font/google' +const openSans = Open_Sans() + +function MyApp({ Component, pageProps }) { + return ( + <> +
+ {JSON.stringify(openSans)} +
+ + + ) +} + +export { openSans } +export default MyApp diff --git a/test/e2e/next-font/app/pages/variables.js b/test/e2e/next-font/app/pages/variables.js new file mode 100644 index 0000000000000..ce3649496d1e3 --- /dev/null +++ b/test/e2e/next-font/app/pages/variables.js @@ -0,0 +1,78 @@ +import { Fira_Code, Albert_Sans, Inter, Roboto } from '@next/font/google' +const firaCode = Fira_Code() +const albertSans = Albert_Sans({ + variant: 'variable-italic', + adjustFontFallback: false, +}) +const inter = Inter({ variant: '900', display: 'swap' }) // Don't preload by default when swap +const roboto = Roboto({ + variant: '100-italic', + display: 'swap', + preload: true, +}) + +export default function WithFonts() { + return ( + <> + {/* Fira Code Variable */} +
+ With variables +
+
+ Without variables +
+ + {/* Albert Sant Variable Italic */} +
+ With variables +
+
+ Without variables +
+ + {/* Inter 900 */} +
+ With variables +
+
+ Without variables +
+ + {/* Roboto 100 Italic */} +
+ With variables +
+
+ Without variables +
+ + ) +} diff --git a/test/e2e/next-font/app/pages/with-fallback.js b/test/e2e/next-font/app/pages/with-fallback.js new file mode 100644 index 0000000000000..587452fb45fd8 --- /dev/null +++ b/test/e2e/next-font/app/pages/with-fallback.js @@ -0,0 +1,22 @@ +import { Open_Sans } from '@next/font/google' +const openSans = Open_Sans({ fallback: ['system-ui', 'Arial'] }) + +export default function WithFonts() { + return ( + <> +
+ {JSON.stringify(openSans)} +
+
+ {JSON.stringify(openSans)} +
+
+ {JSON.stringify(openSans)} +
+ + ) +} diff --git a/test/e2e/next-font/app/pages/with-fonts.js b/test/e2e/next-font/app/pages/with-fonts.js new file mode 100644 index 0000000000000..13dbe3e46bf9f --- /dev/null +++ b/test/e2e/next-font/app/pages/with-fonts.js @@ -0,0 +1,14 @@ +import CompWithFonts from '../components/CompWithFonts' +import { openSans } from './_app' + +export default function WithFonts() { + return ( + <> + +
+ {JSON.stringify(openSans)} +
+
+ + ) +} diff --git a/test/e2e/next-font/app/pages/without-fonts.js b/test/e2e/next-font/app/pages/without-fonts.js new file mode 100644 index 0000000000000..0f649722e671d --- /dev/null +++ b/test/e2e/next-font/app/pages/without-fonts.js @@ -0,0 +1,3 @@ +export default function WithoutFonts() { + return

Hello world

+} diff --git a/test/e2e/next-font/basepath.test.ts b/test/e2e/next-font/basepath.test.ts new file mode 100644 index 0000000000000..0b6d242fc138a --- /dev/null +++ b/test/e2e/next-font/basepath.test.ts @@ -0,0 +1,54 @@ +import cheerio from 'cheerio' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { renderViaHTTP } from 'next-test-utils' +import { join } from 'path' + +const mockedGoogleFontResponses = require.resolve( + './google-font-mocked-responses.js' +) + +describe('@next/font/google basepath', () => { + let next: NextInstance + + if ((global as any).isNextDeploy) { + it('should skip next deploy for now', () => {}) + return + } + + beforeAll(async () => { + next = await createNext({ + files: { + pages: new FileRef(join(__dirname, 'basepath/pages')), + 'next.config.js': new FileRef( + join(__dirname, 'basepath/next.config.js') + ), + }, + dependencies: { + '@next/font': 'canary', + }, + env: { + NEXT_FONT_GOOGLE_MOCKED_RESPONSES: mockedGoogleFontResponses, + }, + }) + }) + afterAll(() => next.destroy()) + + test('preload correct files', async () => { + const html = await renderViaHTTP(next.url, '/dashboard') + const $ = cheerio.load(html) + + // Preconnect + expect($('link[rel="preconnect"]').length).toBe(0) + + // Preload + expect($('link[as="font"]').length).toBe(1) + expect($('link[as="font"]').get(0).attribs).toEqual({ + as: 'font', + crossorigin: 'anonymous', + href: '/dashboard/_next/static/fonts/0812efcfaefec5ea.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) + }) +}) diff --git a/test/e2e/next-font/basepath/next.config.js b/test/e2e/next-font/basepath/next.config.js new file mode 100644 index 0000000000000..8509832e4b888 --- /dev/null +++ b/test/e2e/next-font/basepath/next.config.js @@ -0,0 +1,10 @@ +module.exports = { + basePath: '/dashboard', + experimental: { + fontLoaders: { + '@next/font/google': { + subsets: ['latin'], + }, + }, + }, +} diff --git a/test/e2e/next-font/basepath/pages/index.js b/test/e2e/next-font/basepath/pages/index.js new file mode 100644 index 0000000000000..02056b6f303d3 --- /dev/null +++ b/test/e2e/next-font/basepath/pages/index.js @@ -0,0 +1,6 @@ +import { Open_Sans } from '@next/font/google' +const openSans = Open_Sans() + +export default function Inter() { + return

Hello world

+} diff --git a/test/e2e/next-font/google-font-mocked-responses.js b/test/e2e/next-font/google-font-mocked-responses.js new file mode 100644 index 0000000000000..732813f2401c2 --- /dev/null +++ b/test/e2e/next-font/google-font-mocked-responses.js @@ -0,0 +1,496 @@ +module.exports = { + 'https://fonts.googleapis.com/css2?family=Open+Sans:wght@300..800&display=optional': ` +/* cyrillic-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300 800; + font-stretch: 100%; + font-display: optional; + src: url(https://fonts.gstatic.com/s/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu0SC55K5gw.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300 800; + font-stretch: 100%; + font-display: optional; + src: url(https://fonts.gstatic.com/s/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu0SC55K5gw.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300 800; + font-stretch: 100%; + font-display: optional; + src: url(https://fonts.gstatic.com/s/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu0SC55K5gw.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300 800; + font-stretch: 100%; + font-display: optional; + src: url(https://fonts.gstatic.com/s/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu0SC55K5gw.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* hebrew */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300 800; + font-stretch: 100%; + font-display: optional; + src: url(https://fonts.gstatic.com/s/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu0SC55K5gw.woff2) format('woff2'); + unicode-range: U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F; +} +/* vietnamese */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300 800; + font-stretch: 100%; + font-display: optional; + src: url(https://fonts.gstatic.com/s/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu0SC55K5gw.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300 800; + font-stretch: 100%; + font-display: optional; + src: url(https://fonts.gstatic.com/s/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu0SC55K5gw.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300 800; + font-stretch: 100%; + font-display: optional; + src: url(https://fonts.gstatic.com/s/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-mu0SC55I.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + `, + 'https://fonts.googleapis.com/css2?family=Inter:wght@900&display=swap': ` + /* cyrillic-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url(https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuBWYAZJhiJ-Ek-_EeAmM.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url(https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuBWYAZthiJ-Ek-_EeAmM.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url(https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuBWYAZNhiJ-Ek-_EeAmM.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url(https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuBWYAZxhiJ-Ek-_EeAmM.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url(https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuBWYAZBhiJ-Ek-_EeAmM.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url(https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuBWYAZFhiJ-Ek-_EeAmM.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url(https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuBWYAZ9hiJ-Ek-_EeA.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + `, + 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@1,100&display=swap': ` + /* cyrillic-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 100; + font-display: swap; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEz0dL-vwnYh2eg.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 100; + font-display: swap; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzQdL-vwnYh2eg.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 100; + font-display: swap; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzwdL-vwnYh2eg.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 100; + font-display: swap; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzMdL-vwnYh2eg.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 100; + font-display: swap; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEz8dL-vwnYh2eg.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 100; + font-display: swap; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEz4dL-vwnYh2eg.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 100; + font-display: swap; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzAdL-vwnYg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + `, + 'https://fonts.googleapis.com/css2?family=Roboto:wght@400&display=optional': ` + /* cyrillic-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu5mxKKTU1Kvnz.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu7mxKKTU1Kvnz.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4WxKKTU1Kvnz.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu7WxKKTU1Kvnz.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu7GxKKTU1Kvnz.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + `, + 'https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=block': ` + /* cyrillic-ext */ + @font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: block; + src: url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; + } + /* cyrillic */ + @font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: block; + src: url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; + } + /* greek-ext */ + @font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: block; + src: url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; + } + /* greek */ + @font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: block; + src: url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0370-03FF; + } + /* vietnamese */ + @font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: block; + src: url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; + } + /* latin-ext */ + @font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: block; + src: url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; + } + /* latin */ + @font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: block; + src: url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + } + `, + 'https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@200..900&display=swap': ` + /* cyrillic-ext */ +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 200 900; + font-display: swap; + src: url(https://fonts.gstatic.com/s/sourcecodepro/v22/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMOvWnsUnxlC9.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 200 900; + font-display: swap; + src: url(https://fonts.gstatic.com/s/sourcecodepro/v22/HI_SiYsKILxRpg3hIP6sJ7fM7PqlOevWnsUnxlC9.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 200 900; + font-display: swap; + src: url(https://fonts.gstatic.com/s/sourcecodepro/v22/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMevWnsUnxlC9.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 200 900; + font-display: swap; + src: url(https://fonts.gstatic.com/s/sourcecodepro/v22/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPuvWnsUnxlC9.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 200 900; + font-display: swap; + src: url(https://fonts.gstatic.com/s/sourcecodepro/v22/HI_SiYsKILxRpg3hIP6sJ7fM7PqlMuvWnsUnxlC9.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 200 900; + font-display: swap; + src: url(https://fonts.gstatic.com/s/sourcecodepro/v22/HI_SiYsKILxRpg3hIP6sJ7fM7PqlM-vWnsUnxlC9.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 200 900; + font-display: swap; + src: url(https://fonts.gstatic.com/s/sourcecodepro/v22/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevWnsUnxg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + `, + 'https://fonts.googleapis.com/css2?family=Abel:wght@400&display=optional': ` + /* latin */ +@font-face { + font-family: 'Abel'; + font-style: normal; + font-weight: 400; + font-display: optional; + src: url(https://fonts.gstatic.com/s/abel/v18/MwQ5bhbm2POE2V9BPbh5uGM.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + `, + 'https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&display=optional': ` + /* cyrillic-ext */ +@font-face { + font-family: 'Fira Code'; + font-style: normal; + font-weight: 300 700; + font-display: optional; + src: url(https://fonts.gstatic.com/s/firacode/v21/uU9NCBsR6Z2vfE9aq3bh0NSDqFGedCMX.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Fira Code'; + font-style: normal; + font-weight: 300 700; + font-display: optional; + src: url(https://fonts.gstatic.com/s/firacode/v21/uU9NCBsR6Z2vfE9aq3bh2dSDqFGedCMX.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Fira Code'; + font-style: normal; + font-weight: 300 700; + font-display: optional; + src: url(https://fonts.gstatic.com/s/firacode/v21/uU9NCBsR6Z2vfE9aq3bh0dSDqFGedCMX.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Fira Code'; + font-style: normal; + font-weight: 300 700; + font-display: optional; + src: url(https://fonts.gstatic.com/s/firacode/v21/uU9NCBsR6Z2vfE9aq3bh3tSDqFGedCMX.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* latin-ext */ +@font-face { + font-family: 'Fira Code'; + font-style: normal; + font-weight: 300 700; + font-display: optional; + src: url(https://fonts.gstatic.com/s/firacode/v21/uU9NCBsR6Z2vfE9aq3bh09SDqFGedCMX.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Fira Code'; + font-style: normal; + font-weight: 300 700; + font-display: optional; + src: url(https://fonts.gstatic.com/s/firacode/v21/uU9NCBsR6Z2vfE9aq3bh3dSDqFGedA.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + `, + 'https://fonts.googleapis.com/css2?family=Albert+Sans:ital,wght@1,100..900&display=optional': ` + /* latin-ext */ +@font-face { + font-family: 'Albert Sans'; + font-style: italic; + font-weight: 100 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/albertsans/v1/i7dMIFdwYjGaAMFtZd_QA1ZeUFuaHi6WZ3S_Yg.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Albert Sans'; + font-style: italic; + font-weight: 100 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/albertsans/v1/i7dMIFdwYjGaAMFtZd_QA1ZeUFWaHi6WZ3Q.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + `, +} diff --git a/test/e2e/next-font/index.test.ts b/test/e2e/next-font/index.test.ts new file mode 100644 index 0000000000000..aed14be9eb00e --- /dev/null +++ b/test/e2e/next-font/index.test.ts @@ -0,0 +1,294 @@ +import cheerio from 'cheerio' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { renderViaHTTP } from 'next-test-utils' +import { join } from 'path' +import webdriver from 'next-webdriver' + +const mockedGoogleFontResponses = require.resolve( + './google-font-mocked-responses.js' +) + +describe('@next/font/google', () => { + let next: NextInstance + + if ((global as any).isNextDeploy) { + it('should skip next deploy for now', () => {}) + return + } + + beforeAll(async () => { + next = await createNext({ + files: { + pages: new FileRef(join(__dirname, 'app/pages')), + components: new FileRef(join(__dirname, 'app/components')), + 'next.config.js': new FileRef(join(__dirname, 'app/next.config.js')), + }, + dependencies: { + '@next/font': 'canary', + }, + env: { + NEXT_FONT_GOOGLE_MOCKED_RESPONSES: mockedGoogleFontResponses, + }, + }) + }) + afterAll(() => next.destroy()) + + describe('import values', () => { + test('page with font', async () => { + const html = await renderViaHTTP(next.url, '/with-fonts') + const $ = cheerio.load(html) + + // _app.js + expect(JSON.parse($('#app-open-sans').text())).toEqual({ + className: '__className_bbc724', + variable: '__variable_bbc724', + style: { + fontFamily: "'__Open_Sans_bbc724', '__open-sans-fallback_bbc724'", + fontStyle: 'normal', + }, + }) + + // with-fonts.js + expect(JSON.parse($('#with-fonts-open-sans').text())).toEqual({ + className: '__className_bbc724', + variable: '__variable_bbc724', + style: { + fontFamily: "'__Open_Sans_bbc724', '__open-sans-fallback_bbc724'", + fontStyle: 'normal', + }, + }) + + // CompWithFonts.js + expect(JSON.parse($('#comp-with-fonts-inter').text())).toEqual({ + className: '__className_17e98a', + variable: '__variable_17e98a', + style: { + fontFamily: "'__Inter_17e98a', '__inter-fallback_17e98a'", + fontStyle: 'normal', + fontWeight: 900, + }, + }) + expect(JSON.parse($('#comp-with-fonts-roboto').text())).toEqual({ + className: '__className_72084b', + variable: '__variable_72084b', + style: { + fontFamily: "'__Roboto_72084b', '__roboto-fallback_72084b'", + fontStyle: 'italic', + fontWeight: 100, + }, + }) + }) + }) + + describe('computed styles', () => { + test('page with fonts', async () => { + const browser = await webdriver(next.url, '/with-fonts') + + // _app.js + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#app-open-sans")).fontFamily' + ) + ).toBe('__Open_Sans_bbc724, __open-sans-fallback_bbc724') + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#app-open-sans")).fontWeight' + ) + ).toBe('400') + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#app-open-sans")).fontStyle' + ) + ).toBe('normal') + + // with-fonts.js + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#with-fonts-open-sans")).fontFamily' + ) + ).toBe('__Open_Sans_bbc724, __open-sans-fallback_bbc724') + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#with-fonts-open-sans")).fontWeight' + ) + ).toBe('400') + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#with-fonts-open-sans")).fontStyle' + ) + ).toBe('normal') + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#with-fonts-open-sans-style")).fontWeight' + ) + ).toBe('400') + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#with-fonts-open-sans-style")).fontStyle' + ) + ).toBe('normal') + + // CompWithFonts.js + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#comp-with-fonts-inter")).fontFamily' + ) + ).toBe('__Inter_17e98a, __inter-fallback_17e98a') + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#comp-with-fonts-inter")).fontWeight' + ) + ).toBe('900') + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#comp-with-fonts-inter")).fontStyle' + ) + ).toBe('normal') + + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#comp-with-fonts-roboto")).fontFamily' + ) + ).toBe('__Roboto_72084b, __roboto-fallback_72084b') + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#comp-with-fonts-roboto")).fontWeight' + ) + ).toBe('100') + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#comp-with-fonts-roboto")).fontStyle' + ) + ).toBe('italic') + }) + + test('page using variables', async () => { + const browser = await webdriver(next.url, '/variables') + + // Fira Code Variable + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#variables-fira-code")).fontFamily' + ) + ).toBe('__Fira_Code_a1dc08, __fira-code-fallback_a1dc08') + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#without-variables-fira-code")).fontFamily' + ) + ).not.toBe('__Fira_Code_a1dc08, __fira-code-fallback_a1dc08') + + // Albert Sant Variable Italic + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#variables-albert-sans-italic")).fontFamily' + ) + ).toBe('__Albert_Sans_2b85d2') + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#without-variables-albert-sans-italic")).fontFamily' + ) + ).not.toBe('__Albert_Sans_2b85d2') + + // Inter 900 + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#variables-inter-900")).fontFamily' + ) + ).toBe('__Inter_ea3712, __inter-fallback_ea3712') + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#without-variables-inter-900")).fontFamily' + ) + ).not.toBe('__Inter_ea3712, __inter-fallback_ea3712') + + // Roboto 100 Italic + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#variables-roboto-100-italic")).fontFamily' + ) + ).toBe('__Roboto_72084b, __roboto-fallback_72084b') + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#without-variables-roboto-100-italic")).fontFamily' + ) + ).not.toBe('__Roboto_72084b') + }) + + test('page using fallback fonts', async () => { + const browser = await webdriver(next.url, '/with-fallback') + + // .className + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#with-fallback-fonts-classname")).fontFamily' + ) + ).toBe( + '__Open_Sans_bbc724, system-ui, Arial, __open-sans-fallback_bbc724' + ) + + // .style + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#with-fallback-fonts-style")).fontFamily' + ) + ).toBe( + '__Open_Sans_bbc724, system-ui, Arial, __open-sans-fallback_bbc724' + ) + + // .variable + expect( + await browser.eval( + 'getComputedStyle(document.querySelector("#with-fallback-fonts-variable")).fontFamily' + ) + ).toBe( + '__Open_Sans_bbc724, system-ui, Arial, __open-sans-fallback_bbc724' + ) + }) + }) + + describe('preload', () => { + test('page with fonts', async () => { + const html = await renderViaHTTP(next.url, '/with-fonts') + const $ = cheerio.load(html) + + // Preconnect + expect($('link[rel="preconnect"]').length).toBe(0) + + expect($('link[as="font"]').length).toBe(2) + // From /_app + expect($('link[as="font"]').get(0).attribs).toEqual({ + as: 'font', + crossorigin: 'anonymous', + href: '/_next/static/fonts/0812efcfaefec5ea.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) + expect($('link[as="font"]').get(1).attribs).toEqual({ + as: 'font', + crossorigin: 'anonymous', + href: '/_next/static/fonts/4f3dcdf40b3ca86d.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) + }) + + test('page without fonts', async () => { + const html = await renderViaHTTP(next.url, '/without-fonts') + const $ = cheerio.load(html) + + // Preconnect + expect($('link[rel="preconnect"]').length).toBe(0) + + // From _app + expect($('link[as="font"]').length).toBe(1) + expect($('link[as="font"]').get(0).attribs).toEqual({ + as: 'font', + crossorigin: 'anonymous', + href: '/_next/static/fonts/0812efcfaefec5ea.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) + }) + }) +}) diff --git a/test/e2e/next-font/with-font-declarations-file.test.ts b/test/e2e/next-font/with-font-declarations-file.test.ts new file mode 100644 index 0000000000000..c6d5bca36edc5 --- /dev/null +++ b/test/e2e/next-font/with-font-declarations-file.test.ts @@ -0,0 +1,110 @@ +import cheerio from 'cheerio' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { renderViaHTTP } from 'next-test-utils' +import { join } from 'path' + +const mockedGoogleFontResponses = require.resolve( + './google-font-mocked-responses.js' +) + +const isDev = (global as any).isNextDev + +describe('@next/font/google with-font-declarations-file', () => { + let next: NextInstance + + if ((global as any).isNextDeploy) { + it('should skip next deploy for now', () => {}) + return + } + + beforeAll(async () => { + next = await createNext({ + files: { + pages: new FileRef( + join(__dirname, 'with-font-declarations-file/pages') + ), + components: new FileRef( + join(__dirname, 'with-font-declarations-file/components') + ), + 'fonts.js': new FileRef( + join(__dirname, 'with-font-declarations-file/fonts.js') + ), + 'next.config.js': new FileRef( + join(__dirname, 'with-font-declarations-file/next.config.js') + ), + }, + dependencies: { + '@next/font': 'canary', + }, + env: { + NEXT_FONT_GOOGLE_MOCKED_RESPONSES: mockedGoogleFontResponses, + }, + }) + }) + afterAll(() => next.destroy()) + + test('preload correct files at /inter', async () => { + const html = await renderViaHTTP(next.url, '/inter') + const $ = cheerio.load(html) + + // Preconnect + expect($('link[rel="preconnect"]').length).toBe(0) + + if (isDev) { + // In dev all fonts will be preloaded since it's before DCE + expect($('link[as="font"]').length).toBe(3) + } else { + // Preload + expect($('link[as="font"]').length).toBe(2) + // From /_app + expect($('link[as="font"]').get(0).attribs).toEqual({ + as: 'font', + crossorigin: 'anonymous', + href: '/_next/static/fonts/0812efcfaefec5ea.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) + // From /inter + expect($('link[as="font"]').get(1).attribs).toEqual({ + as: 'font', + crossorigin: 'anonymous', + href: '/_next/static/fonts/4a7f86e553ee7e51.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) + } + }) + + test('preload correct files at /roboto', async () => { + const html = await renderViaHTTP(next.url, '/roboto') + const $ = cheerio.load(html) + + // Preconnect + expect($('link[rel="preconnect"]').length).toBe(0) + + if (isDev) { + // In dev all fonts will be preloaded since it's before DCE + expect($('link[as="font"]').length).toBe(3) + } else { + // Preload + expect($('link[as="font"]').length).toBe(2) + // From /_app + expect($('link[as="font"]').get(0).attribs).toEqual({ + as: 'font', + crossorigin: 'anonymous', + href: '/_next/static/fonts/0812efcfaefec5ea.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) + // From /roboto + expect($('link[as="font"]').get(1).attribs).toEqual({ + as: 'font', + crossorigin: 'anonymous', + href: '/_next/static/fonts/9a7e84b4dd095b33.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) + } + }) +}) diff --git a/test/e2e/next-font/with-font-declarations-file/components/roboto-comp.js b/test/e2e/next-font/with-font-declarations-file/components/roboto-comp.js new file mode 100644 index 0000000000000..4c58fcd0c139d --- /dev/null +++ b/test/e2e/next-font/with-font-declarations-file/components/roboto-comp.js @@ -0,0 +1,10 @@ +import Link from 'next/link' +import { roboto } from '../fonts' + +export default function Roboto() { + return ( + + To inter + + ) +} diff --git a/test/e2e/next-font/with-font-declarations-file/fonts.js b/test/e2e/next-font/with-font-declarations-file/fonts.js new file mode 100644 index 0000000000000..9e82f26c76719 --- /dev/null +++ b/test/e2e/next-font/with-font-declarations-file/fonts.js @@ -0,0 +1,16 @@ +import { + Open_Sans, + Source_Code_Pro, + Abel, + Inter, + Roboto, +} from '@next/font/google' + +const openSans = Open_Sans() +const sourceCodePro = Source_Code_Pro({ display: 'swap', preload: false }) +const abel = Abel({ variant: '400', display: 'optional', preload: false }) + +const inter = Inter({ display: 'block', preload: true }) +const roboto = Roboto({ variant: '400' }) + +export { openSans, sourceCodePro, abel, inter, roboto } diff --git a/test/e2e/next-font/with-font-declarations-file/next.config.js b/test/e2e/next-font/with-font-declarations-file/next.config.js new file mode 100644 index 0000000000000..6cd855478a746 --- /dev/null +++ b/test/e2e/next-font/with-font-declarations-file/next.config.js @@ -0,0 +1,9 @@ +module.exports = { + experimental: { + fontLoaders: { + '@next/font/google': { + subsets: ['latin'], + }, + }, + }, +} diff --git a/test/e2e/next-font/with-font-declarations-file/pages/_app.js b/test/e2e/next-font/with-font-declarations-file/pages/_app.js new file mode 100644 index 0000000000000..8aa19db97f4dd --- /dev/null +++ b/test/e2e/next-font/with-font-declarations-file/pages/_app.js @@ -0,0 +1,15 @@ +import { openSans, sourceCodePro, abel } from '../fonts' + +function MyApp({ Component, pageProps }) { + return ( +
+
+
+ +
+
+
+ ) +} + +export default MyApp diff --git a/test/e2e/next-font/with-font-declarations-file/pages/inter.js b/test/e2e/next-font/with-font-declarations-file/pages/inter.js new file mode 100644 index 0000000000000..7eafc740bd028 --- /dev/null +++ b/test/e2e/next-font/with-font-declarations-file/pages/inter.js @@ -0,0 +1,10 @@ +import Link from 'next/link' +import { inter } from '../fonts' + +export default function Inter() { + return ( + + To roboto + + ) +} diff --git a/test/e2e/next-font/with-font-declarations-file/pages/roboto.js b/test/e2e/next-font/with-font-declarations-file/pages/roboto.js new file mode 100644 index 0000000000000..85b22d621b1b2 --- /dev/null +++ b/test/e2e/next-font/with-font-declarations-file/pages/roboto.js @@ -0,0 +1,5 @@ +import RobotoComp from '../components/roboto-comp' + +export default function Roboto() { + return +} diff --git a/test/e2e/next-font/without-preloaded-fonts.test.ts b/test/e2e/next-font/without-preloaded-fonts.test.ts new file mode 100644 index 0000000000000..2c7cf366c5a2b --- /dev/null +++ b/test/e2e/next-font/without-preloaded-fonts.test.ts @@ -0,0 +1,132 @@ +import cheerio from 'cheerio' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { renderViaHTTP } from 'next-test-utils' +import { join } from 'path' + +const mockedGoogleFontResponses = require.resolve( + './google-font-mocked-responses.js' +) + +describe('@next/font/google without-preloaded-fonts without _app', () => { + let next: NextInstance + + if ((global as any).isNextDeploy) { + it('should skip next deploy for now', () => {}) + return + } + + beforeAll(async () => { + next = await createNext({ + files: { + 'pages/no-preload.js': new FileRef( + join(__dirname, 'without-preloaded-fonts/pages/no-preload.js') + ), + 'pages/without-fonts.js': new FileRef( + join(__dirname, 'without-preloaded-fonts/pages/without-fonts.js') + ), + 'next.config.js': new FileRef( + join(__dirname, 'without-preloaded-fonts/next.config.js') + ), + }, + dependencies: { + '@next/font': 'canary', + }, + env: { + NEXT_FONT_GOOGLE_MOCKED_RESPONSES: mockedGoogleFontResponses, + }, + }) + }) + afterAll(() => next.destroy()) + + test('without preload', async () => { + const html = await renderViaHTTP(next.url, '/no-preload') + const $ = cheerio.load(html) + + // Preconnect + expect($('link[rel="preconnect"]').length).toBe(1) + expect($('link[rel="preconnect"]').get(0).attribs).toEqual({ + crossorigin: 'anonymous', + href: '/', + rel: 'preconnect', + }) + + // Preload + expect($('link[as="font"]').length).toBe(0) + }) + + test('without fonts', async () => { + const html = await renderViaHTTP(next.url, '/without-fonts') + const $ = cheerio.load(html) + + expect($('link[rel="preconnect"]').length).toBe(0) + expect($('link[as="font"]').length).toBe(0) + }) +}) + +describe('@next/font/google no preloads with _app', () => { + let next: NextInstance + + if ((global as any).isNextDeploy) { + it('should skip next deploy for now', () => {}) + return + } + + beforeAll(async () => { + next = await createNext({ + files: { + 'pages/_app.js': new FileRef( + join(__dirname, 'without-preloaded-fonts/pages/_app.js') + ), + 'pages/no-preload.js': new FileRef( + join(__dirname, 'without-preloaded-fonts/pages/no-preload.js') + ), + 'pages/without-fonts.js': new FileRef( + join(__dirname, 'without-preloaded-fonts/pages/without-fonts.js') + ), + 'next.config.js': new FileRef( + join(__dirname, 'without-preloaded-fonts/next.config.js') + ), + }, + dependencies: { + '@next/font': 'canary', + }, + env: { + NEXT_FONT_GOOGLE_MOCKED_RESPONSES: mockedGoogleFontResponses, + }, + }) + }) + afterAll(() => next.destroy()) + + test('without preload', async () => { + const html = await renderViaHTTP(next.url, '/no-preload') + const $ = cheerio.load(html) + + // Preconnect + expect($('link[rel="preconnect"]').length).toBe(1) + expect($('link[rel="preconnect"]').get(0).attribs).toEqual({ + crossorigin: 'anonymous', + href: '/', + rel: 'preconnect', + }) + + // Preload + expect($('link[as="font"]').length).toBe(0) + }) + + test('without fonts', async () => { + const html = await renderViaHTTP(next.url, '/without-fonts') + const $ = cheerio.load(html) + + // Preconnect + expect($('link[rel="preconnect"]').length).toBe(1) + expect($('link[rel="preconnect"]').get(0).attribs).toEqual({ + crossorigin: 'anonymous', + href: '/', + rel: 'preconnect', + }) + + // Preload + expect($('link[as="font"]').length).toBe(0) + }) +}) diff --git a/test/e2e/next-font/without-preloaded-fonts/next.config.js b/test/e2e/next-font/without-preloaded-fonts/next.config.js new file mode 100644 index 0000000000000..6cd855478a746 --- /dev/null +++ b/test/e2e/next-font/without-preloaded-fonts/next.config.js @@ -0,0 +1,9 @@ +module.exports = { + experimental: { + fontLoaders: { + '@next/font/google': { + subsets: ['latin'], + }, + }, + }, +} diff --git a/test/e2e/next-font/without-preloaded-fonts/pages/_app.js b/test/e2e/next-font/without-preloaded-fonts/pages/_app.js new file mode 100644 index 0000000000000..86f596bdb327c --- /dev/null +++ b/test/e2e/next-font/without-preloaded-fonts/pages/_app.js @@ -0,0 +1,12 @@ +import { Abel } from '@next/font/google' +const abel = Abel({ variant: '400', display: 'optional', preload: false }) + +function MyApp({ Component, pageProps }) { + return ( +
+ +
+ ) +} + +export default MyApp diff --git a/test/e2e/next-font/without-preloaded-fonts/pages/no-preload.js b/test/e2e/next-font/without-preloaded-fonts/pages/no-preload.js new file mode 100644 index 0000000000000..accacef22204c --- /dev/null +++ b/test/e2e/next-font/without-preloaded-fonts/pages/no-preload.js @@ -0,0 +1,6 @@ +import { Abel } from '@next/font/google' +const abel = Abel({ variant: '400', display: 'optional', preload: false }) + +export default function NoPreload() { + return

Hello world

+} diff --git a/test/e2e/next-font/without-preloaded-fonts/pages/without-fonts.js b/test/e2e/next-font/without-preloaded-fonts/pages/without-fonts.js new file mode 100644 index 0000000000000..4e9a1aaafe12c --- /dev/null +++ b/test/e2e/next-font/without-preloaded-fonts/pages/without-fonts.js @@ -0,0 +1,3 @@ +export default function WithoutFonts() { + return

Hello world

+}