Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@next/font/google fetch error dev #42637

Merged
merged 6 commits into from Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
217 changes: 133 additions & 84 deletions packages/font/src/google/loader.ts
Expand Up @@ -34,7 +34,9 @@ const downloadGoogleFonts: FontLoader = async ({
data,
config,
emitFontFile,
isDev,
isServer,
loaderContext,
}) => {
const subsets = config?.subsets || []

Expand Down Expand Up @@ -69,80 +71,7 @@ const downloadGoogleFonts: FontLoader = async ({
)
const url = getUrl(fontFamily, fontAxes, display)

let cachedCssRequest = cssCache.get(url)
const fontFaceDeclarations =
cachedCssRequest ?? (await fetchCSSFromGoogleFonts(url, fontFamily))
if (!cachedCssRequest) {
cssCache.set(url, fontFaceDeclarations)
} else {
cssCache.delete(url)
}

// Find font files to download
const fontFiles: Array<{
googleFontFileUrl: string
preloadFontFile: boolean
}> = []
let currentSubset = ''
for (const line of fontFaceDeclarations.split('\n')) {
// Each @font-face has the subset above it in a comment
const newSubset = /\/\* (.+?) \*\//.exec(line)?.[1]
if (newSubset) {
currentSubset = newSubset
} else {
const googleFontFileUrl = /src: url\((.+?)\)/.exec(line)?.[1]
if (
googleFontFileUrl &&
!fontFiles.some(
(foundFile) => foundFile.googleFontFileUrl === googleFontFileUrl
)
) {
fontFiles.push({
googleFontFileUrl,
preloadFontFile:
!!preload && (callSubsets ?? subsets).includes(currentSubset),
})
}
}
}

// Download font files
const downloadedFiles = await Promise.all(
fontFiles.map(async ({ googleFontFileUrl, preloadFontFile }) => {
let cachedFontRequest = fontCache.get(googleFontFileUrl)
const fontFileBuffer =
cachedFontRequest ?? (await fetchFontFile(googleFontFileUrl))
if (!cachedFontRequest) {
fontCache.set(googleFontFileUrl, fontFileBuffer)
} else {
fontCache.delete(googleFontFileUrl)
}

const ext = /\.(woff|woff2|eot|ttf|otf)$/.exec(googleFontFileUrl)![1]
// Emit font file to .next/static/fonts
const selfHostedFileUrl = emitFontFile(
fontFileBuffer,
ext,
preloadFontFile
)

return {
googleFontFileUrl,
selfHostedFileUrl,
}
})
)

// Replace @font-face sources with self-hosted files
let updatedCssResponse = fontFaceDeclarations
for (const { googleFontFileUrl, selfHostedFileUrl } of downloadedFiles) {
updatedCssResponse = updatedCssResponse.replace(
new RegExp(escapeStringRegexp(googleFontFileUrl), 'g'),
selfHostedFileUrl
)
}

// Add fallback font
// Find fallback font metrics
let adjustFontFallbackMetrics: AdjustFontFallback | undefined
if (adjustFontFallback) {
try {
Expand All @@ -164,16 +93,136 @@ const downloadGoogleFonts: FontLoader = async ({
}
}

return {
css: updatedCssResponse,
fallbackFonts: fallback,
weight:
weights.length === 1 && weights[0] !== 'variable'
? weights[0]
: undefined,
style: styles.length === 1 ? styles[0] : undefined,
variable,
adjustFontFallback: adjustFontFallbackMetrics,
try {
const hasCachedCSS = cssCache.has(url)
const fontFaceDeclarations = hasCachedCSS
? cssCache.get(url)
: await fetchCSSFromGoogleFonts(url, fontFamily).catch(() => null)
if (!hasCachedCSS) {
cssCache.set(url, fontFaceDeclarations)
} else {
cssCache.delete(url)
}
if (fontFaceDeclarations === null) {
throw new Error(`Failed to fetch \`${fontFamily}\` from Google Fonts.`)
}

// Find font files to download
const fontFiles: Array<{
googleFontFileUrl: string
preloadFontFile: boolean
}> = []
let currentSubset = ''
for (const line of fontFaceDeclarations.split('\n')) {
// Each @font-face has the subset above it in a comment
const newSubset = /\/\* (.+?) \*\//.exec(line)?.[1]
if (newSubset) {
currentSubset = newSubset
} else {
const googleFontFileUrl = /src: url\((.+?)\)/.exec(line)?.[1]
if (
googleFontFileUrl &&
!fontFiles.some(
(foundFile) => foundFile.googleFontFileUrl === googleFontFileUrl
)
) {
fontFiles.push({
googleFontFileUrl,
preloadFontFile:
!!preload && (callSubsets ?? subsets).includes(currentSubset),
})
}
}
}

// Download font files
const downloadedFiles = await Promise.all(
fontFiles.map(async ({ googleFontFileUrl, preloadFontFile }) => {
const hasCachedFont = fontCache.has(googleFontFileUrl)
const fontFileBuffer = hasCachedFont
? fontCache.get(googleFontFileUrl)
: await fetchFontFile(googleFontFileUrl).catch(() => null)
if (!hasCachedFont) {
fontCache.set(googleFontFileUrl, fontFileBuffer)
} else {
fontCache.delete(googleFontFileUrl)
}
if (fontFileBuffer === null) {
throw new Error(
`Failed to fetch \`${fontFamily}\` from Google Fonts.`
)
}

const ext = /\.(woff|woff2|eot|ttf|otf)$/.exec(googleFontFileUrl)![1]
// Emit font file to .next/static/fonts
const selfHostedFileUrl = emitFontFile(
fontFileBuffer,
ext,
preloadFontFile
)

return {
googleFontFileUrl,
selfHostedFileUrl,
}
})
)

// Replace @font-face sources with self-hosted files
let updatedCssResponse = fontFaceDeclarations
for (const { googleFontFileUrl, selfHostedFileUrl } of downloadedFiles) {
updatedCssResponse = updatedCssResponse.replace(
new RegExp(escapeStringRegexp(googleFontFileUrl), 'g'),
selfHostedFileUrl
)
}

return {
css: updatedCssResponse,
fallbackFonts: fallback,
weight:
weights.length === 1 && weights[0] !== 'variable'
? weights[0]
: undefined,
style: styles.length === 1 ? styles[0] : undefined,
variable,
adjustFontFallback: adjustFontFallbackMetrics,
}
} catch (err) {
loaderContext.cacheable(false)
if (isDev) {
if (isServer) {
Log.error(
`Failed to download \`${fontFamily}\` from Google Fonts. Using fallback font instead.`
)
}

// In dev we should return the fallback font instead of throwing an error
let css = `@font-face {
font-family: '${fontFamily} Fallback';
src: local("${adjustFontFallbackMetrics?.fallbackFont ?? 'Arial'}");`
if (adjustFontFallbackMetrics) {
css += `
ascent-override:${adjustFontFallbackMetrics.ascentOverride};
descent-override:${adjustFontFallbackMetrics.descentOverride};
line-gap-override:${adjustFontFallbackMetrics.lineGapOverride};
size-adjust:${adjustFontFallbackMetrics.sizeAdjust};`
}
css += '\n}'

return {
css,
fallbackFonts: fallback,
weight:
weights.length === 1 && weights[0] !== 'variable'
? weights[0]
: undefined,
style: styles.length === 1 ? styles[0] : undefined,
variable,
}
} else {
throw err
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/font/src/local/loader.ts
Expand Up @@ -57,7 +57,7 @@ const fetchFonts: FontLoader = async ({
data,
emitFontFile,
resolve,
fs,
loaderContext,
}) => {
const {
src,
Expand All @@ -74,7 +74,7 @@ const fetchFonts: FontLoader = async ({
const fontFiles = await Promise.all(
src.map(async ({ path, style, weight, ext, format }) => {
const resolved = await resolve(path)
const fileBuffer = await promisify(fs.readFile)(resolved)
const fileBuffer = await promisify(loaderContext.fs.readFile)(resolved)
const fontUrl = emitFontFile(fileBuffer, ext, preload)

let fontMetadata: any
Expand Down
Expand Up @@ -59,6 +59,7 @@ export function getFontLoader(
loaders.push({
loader: 'next-font-loader',
options: {
isDev: ctx.isDevelopment,
isServer: ctx.isServer,
assetPrefix: ctx.assetPrefix,
fontLoaderOptions,
Expand Down
Expand Up @@ -13,6 +13,7 @@ export default async function nextFontLoader(this: any) {
return fontLoaderSpan.traceAsyncFn(async () => {
const callback = this.async()
const {
isDev,
isServer,
assetPrefix,
fontLoaderOptions,
Expand Down Expand Up @@ -79,8 +80,9 @@ export default async function nextFontLoader(this: any) {
),
src.startsWith('.') ? src : `./${src}`
),
fs: this.fs,
isDev,
isServer,
loaderContext: this,
})

const { postcss } = await getPostcss()
Expand Down
Expand Up @@ -51,7 +51,7 @@ const postcssFontLoaderPlugn = ({
}

if (!fontFamily) {
throw new Error('Font loaders must have exactly one font family')
throw new Error("Font loaders must return one or more @font-face's")
}

// Add fallback font with override values
Expand Down
3 changes: 2 additions & 1 deletion packages/next/font/index.d.ts
Expand Up @@ -19,8 +19,9 @@ export type FontLoader = (options: {
config: any
emitFontFile: (content: Buffer, ext: string, preload: boolean) => string
resolve: (src: string) => string
fs: any
isDev: boolean
isServer: boolean
loaderContext: any
}) => Promise<{
css: string
fallbackFonts?: string[]
Expand Down
76 changes: 76 additions & 0 deletions test/e2e/next-font/google-fetch-error.test.ts
@@ -0,0 +1,76 @@
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import { join } from 'path'
import webdriver from 'next-webdriver'

const mockedGoogleFontResponses = require.resolve(
'./google-font-mocked-responses.js'
)

describe('@next/font/google fetch error', () => {
const isDev = (global as any).isNextDev
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, 'google-fetch-error/pages')),
'next.config.js': new FileRef(
join(__dirname, 'google-fetch-error/next.config.js')
),
},
dependencies: {
'@next/font': 'canary',
},
env: {
NEXT_FONT_GOOGLE_MOCKED_RESPONSES: mockedGoogleFontResponses,
},
skipStart: true,
})
})
afterAll(() => next.destroy())

if (isDev) {
it('should use a fallback font in dev', async () => {
await next.start()
const outputIndex = next.cliOutput.length
const browser = await webdriver(next.url, '/')

const ascentOverride = await browser.eval(
'Array.from(document.fonts.values()).find(font => font.family.includes("Inter_Fallback")).ascentOverride'
)
expect(ascentOverride).toBe('90%')

const descentOverride = await browser.eval(
'Array.from(document.fonts.values()).find(font => font.family.includes("Inter_Fallback")).descentOverride'
)
expect(descentOverride).toBe('22.43%')

const lineGapOverride = await browser.eval(
'Array.from(document.fonts.values()).find(font => font.family.includes("Inter_Fallback")).lineGapOverride'
)
expect(lineGapOverride).toBe('0%')

const sizeAdjust = await browser.eval(
'Array.from(document.fonts.values()).find(font => font.family.includes("Inter_Fallback")).sizeAdjust'
)
expect(sizeAdjust).toBe('107.64%')

expect(next.cliOutput.slice(outputIndex)).toInclude(
'Failed to download `Inter` from Google Fonts. Using fallback font instead.'
)
})
} else {
it('should error when not in dev', async () => {
await expect(next.start()).rejects.toThrow('next build failed')
expect(next.cliOutput).toInclude(
'Failed to fetch `Inter` from Google Fonts.'
)
})
}
})
9 changes: 9 additions & 0 deletions test/e2e/next-font/google-fetch-error/next.config.js
@@ -0,0 +1,9 @@
module.exports = {
experimental: {
fontLoaders: [
{
loader: '@next/font/google',
},
],
},
}