From 397894b133f11432b80c18f41fd4bc555c85044e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= Date: Thu, 17 Nov 2022 17:52:19 +0100 Subject: [PATCH] @next/font/google variable fonts without weight range (#43036) A few variable fonts don't have a weight range, in those cases a `wght` axis shouldn't be set at all. Just add optional additional axes. Some fonts set a CSS variable on a `body` tag, ignore it to keep the CSS module pure. Fixes #43040 ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- packages/font/src/google/loader.ts | 5 +- packages/font/src/google/utils.ts | 62 ++++++++++++------- .../variable-font-without-weight-range.js | 11 ++++ .../next-font/google-font-mocked-responses.js | 50 +++++++++++++++ test/e2e/next-font/index.test.ts | 16 +++++ test/unit/google-font-loader.test.ts | 16 ++++- 6 files changed, 135 insertions(+), 25 deletions(-) create mode 100644 test/e2e/next-font/app/pages/variable-font-without-weight-range.js diff --git a/packages/font/src/google/loader.ts b/packages/font/src/google/loader.ts index 8633a00c83f6..09ea0729212b 100644 --- a/packages/font/src/google/loader.ts +++ b/packages/font/src/google/loader.ts @@ -95,7 +95,7 @@ const downloadGoogleFonts: FontLoader = async ({ try { const hasCachedCSS = cssCache.has(url) - const fontFaceDeclarations = hasCachedCSS + let fontFaceDeclarations = hasCachedCSS ? cssCache.get(url) : await fetchCSSFromGoogleFonts(url, fontFamily).catch(() => null) if (!hasCachedCSS) { @@ -107,6 +107,9 @@ const downloadGoogleFonts: FontLoader = async ({ throw new Error(`Failed to fetch \`${fontFamily}\` from Google Fonts.`) } + // CSS Variables may be set on a body tag, ignore them to keep the CSS module pure + fontFaceDeclarations = fontFaceDeclarations.split('body {')[0] + // Find font files to download const fontFiles: Array<{ googleFontFileUrl: string diff --git a/packages/font/src/google/utils.ts b/packages/font/src/google/utils.ts index 16baec887f90..03cf1c28382a 100644 --- a/packages/font/src/google/utils.ts +++ b/packages/font/src/google/utils.ts @@ -127,26 +127,31 @@ export function validateData(functionName: string, data: any): FontOptions { export function getUrl( fontFamily: string, axes: { - wght: string[] - ital: string[] + wght?: string[] + ital?: string[] variableAxes?: [string, string][] }, display: string ) { // Variants are all combinations of weight and style, each variant will result in a separate font file const variants: Array<[string, string][]> = [] - for (const wgth of axes.wght) { - if (axes.ital.length === 0) { - variants.push([['wght', wgth], ...(axes.variableAxes ?? [])]) - } else { - for (const ital of axes.ital) { - variants.push([ - ['ital', ital], - ['wght', wgth], - ...(axes.variableAxes ?? []), - ]) + if (axes.wght) { + for (const wgth of axes.wght) { + if (!axes.ital) { + variants.push([['wght', wgth], ...(axes.variableAxes ?? [])]) + } else { + for (const ital of axes.ital) { + variants.push([ + ['ital', ital], + ['wght', wgth], + ...(axes.variableAxes ?? []), + ]) + } } } + } else if (axes.variableAxes) { + // Variable fonts might not have a range of weights, just add optional variable axes in that case + variants.push([...axes.variableAxes]) } // Google api requires the axes to be sorted, starting with lowercase words @@ -163,13 +168,21 @@ export function getUrl( }) } - return `https://fonts.googleapis.com/css2?family=${fontFamily.replace( + let url = `https://fonts.googleapis.com/css2?family=${fontFamily.replace( / /g, '+' - )}:${variants[0].map(([key]) => key).join(',')}@${variants - .map((variant) => variant.map(([, val]) => val).join(',')) - .sort() - .join(';')}&display=${display}` + )}` + + if (variants.length > 0) { + url = `${url}:${variants[0].map(([key]) => key).join(',')}@${variants + .map((variant) => variant.map(([, val]) => val).join(',')) + .sort() + .join(';')}` + } + + url = `${url}&display=${display}` + + return url } export async function fetchCSSFromGoogleFonts(url: string, fontFamily: string) { @@ -221,8 +234,8 @@ export function getFontAxes( styles: string[], selectedVariableAxes?: string[] ): { - wght: string[] - ital: string[] + wght?: string[] + ital?: string[] variableAxes?: [string, string][] } { const allAxes: Array<{ tag: string; min: number; max: number }> = ( @@ -230,7 +243,7 @@ export function getFontAxes( )[fontFamily].axes const hasItalic = styles.includes('italic') const hasNormal = styles.includes('normal') - const ital = hasItalic ? [...(hasNormal ? ['0'] : []), '1'] : [] + const ital = hasItalic ? [...(hasNormal ? ['0'] : []), '1'] : undefined // Weights will always contain one element if it's a variable font if (weights[0] === 'variable') { @@ -259,18 +272,21 @@ export function getFontAxes( }) } - let weightAxis: string - const variableAxes: [string, string][] = [] + let weightAxis: string | undefined + let variableAxes: [string, string][] | undefined for (const { tag, min, max } of allAxes) { if (tag === 'wght') { weightAxis = `${min}..${max}` } else if (selectedVariableAxes?.includes(tag)) { + if (!variableAxes) { + variableAxes = [] + } variableAxes.push([tag, `${min}..${max}`]) } } return { - wght: [weightAxis!], + wght: weightAxis ? [weightAxis] : undefined, ital, variableAxes, } diff --git a/test/e2e/next-font/app/pages/variable-font-without-weight-range.js b/test/e2e/next-font/app/pages/variable-font-without-weight-range.js new file mode 100644 index 000000000000..bb78339d8d40 --- /dev/null +++ b/test/e2e/next-font/app/pages/variable-font-without-weight-range.js @@ -0,0 +1,11 @@ +import { Nabla } from '@next/font/google' + +const nabla = Nabla() + +export default function VariableFontWithoutWeightRange() { + return ( +

+ {JSON.stringify(nabla)} +

+ ) +} diff --git a/test/e2e/next-font/google-font-mocked-responses.js b/test/e2e/next-font/google-font-mocked-responses.js index 5499d96f4ff3..467e74c94474 100644 --- a/test/e2e/next-font/google-font-mocked-responses.js +++ b/test/e2e/next-font/google-font-mocked-responses.js @@ -938,4 +938,54 @@ module.exports = { `, 'https://fonts.googleapis.com/css2?family=Inter:wght@400&display=optional': null, + 'https://fonts.googleapis.com/css2?family=Nabla&display=optional': ` + /* cyrillic-ext */ +@font-face { + font-family: 'Nabla'; + font-style: normal; + font-weight: 400; + font-display: optional; + src: url(https://fonts.gstatic.com/s/nabla/v9/j8_D6-LI0Lvpe7Makz5UhJt9C3uqg_X_75gyGS4jAxsNIjrRBRpeFXZ6x96OvGloBgM.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* math */ +@font-face { + font-family: 'Nabla'; + font-style: normal; + font-weight: 400; + font-display: optional; + src: url(https://fonts.gstatic.com/s/nabla/v9/j8_D6-LI0Lvpe7Makz5UhJt9C3uqg_X_75gyGS4jAxsNIjrRBWteFXZ6x96OvGloBgM.woff2) format('woff2'); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0330, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2034-2037, U+2057, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2102, U+210A-210E, U+2110-2112, U+2115, U+2119-211D, U+2124, U+2128, U+212C-212D, U+212F-2131, U+2133-2138, U+213C-2140, U+2145-2149, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B6, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* vietnamese */ +@font-face { + font-family: 'Nabla'; + font-style: normal; + font-weight: 400; + font-display: optional; + src: url(https://fonts.gstatic.com/s/nabla/v9/j8_D6-LI0Lvpe7Makz5UhJt9C3uqg_X_75gyGS4jAxsNIjrRBRheFXZ6x96OvGloBgM.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: 'Nabla'; + font-style: normal; + font-weight: 400; + font-display: optional; + src: url(https://fonts.gstatic.com/s/nabla/v9/j8_D6-LI0Lvpe7Makz5UhJt9C3uqg_X_75gyGS4jAxsNIjrRBRleFXZ6x96OvGloBgM.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: 'Nabla'; + font-style: normal; + font-weight: 400; + font-display: optional; + src: url(https://fonts.gstatic.com/s/nabla/v9/j8_D6-LI0Lvpe7Makz5UhJt9C3uqg_X_75gyGS4jAxsNIjrRBRdeFXZ6x96OvAFr.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; +} +body { + --google-font-color-nabla:colrv1; +} + `, } diff --git a/test/e2e/next-font/index.test.ts b/test/e2e/next-font/index.test.ts index 47eb37573288..2298bcb13344 100644 --- a/test/e2e/next-font/index.test.ts +++ b/test/e2e/next-font/index.test.ts @@ -124,6 +124,22 @@ describe('@next/font/google', () => { }, }) }) + + test('Variable font without weight range', async () => { + const html = await renderViaHTTP( + next.url, + '/variable-font-without-weight-range' + ) + const $ = cheerio.load(html) + + expect(JSON.parse($('#nabla').text())).toEqual({ + className: expect.stringMatching(/__className_.{6}/), + style: { + fontFamily: expect.stringMatching(/^'__Nabla_.{6}'$/), + fontStyle: 'normal', + }, + }) + }) }) describe('computed styles', () => { diff --git a/test/unit/google-font-loader.test.ts b/test/unit/google-font-loader.test.ts index 80096c884535..5e7b4efb5ed3 100644 --- a/test/unit/google-font-loader.test.ts +++ b/test/unit/google-font-loader.test.ts @@ -91,12 +91,26 @@ describe('@next/font/google loader', () => { }, 'https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght,SOFT,WONK@0,9..144,100..900,0..100,0..1;1,9..144,100..900,0..100,0..1&display=optional', ], - [ 'Poppins', { weight: ['900', '400', '100'] }, 'https://fonts.googleapis.com/css2?family=Poppins:wght@100;400;900&display=optional', ], + [ + 'Nabla', + {}, + 'https://fonts.googleapis.com/css2?family=Nabla&display=optional', + ], + [ + 'Nabla', + { axes: ['EDPT', 'EHLT'] }, + 'https://fonts.googleapis.com/css2?family=Nabla:EDPT,EHLT@0..200,0..24&display=optional', + ], + [ + 'Ballet', + {}, + 'https://fonts.googleapis.com/css2?family=Ballet&display=optional', + ], ])('%s', async (functionName: string, data: any, url: string) => { fetch.mockResolvedValue({ ok: true,