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

Improve @next/font error handling #43298

Merged
merged 16 commits into from Nov 24, 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
7 changes: 3 additions & 4 deletions packages/font/src/google/loader.ts
Expand Up @@ -13,6 +13,7 @@ import {
getUrl,
validateData,
} from './utils'
import { nextFontError } from '../utils'

const cssCache = new Map<string, Promise<string>>()
const fontCache = new Map<string, any>()
Expand Down Expand Up @@ -104,7 +105,7 @@ const downloadGoogleFonts: FontLoader = async ({
cssCache.delete(url)
}
if (fontFaceDeclarations === null) {
throw new Error(`Failed to fetch \`${fontFamily}\` from Google Fonts.`)
nextFontError(`Failed to fetch \`${fontFamily}\` from Google Fonts.`)
}

// CSS Variables may be set on a body tag, ignore them to keep the CSS module pure
Expand Down Expand Up @@ -151,9 +152,7 @@ const downloadGoogleFonts: FontLoader = async ({
fontCache.delete(googleFontFileUrl)
}
if (fontFileBuffer === null) {
throw new Error(
`Failed to fetch \`${fontFamily}\` from Google Fonts.`
)
nextFontError(`Failed to fetch \`${fontFamily}\` from Google Fonts.`)
}

const ext = /\.(woff|woff2|eot|ttf|otf)$/.exec(googleFontFileUrl)![1]
Expand Down
27 changes: 14 additions & 13 deletions packages/font/src/google/utils.ts
@@ -1,6 +1,7 @@
import fs from 'fs'
// @ts-ignore
import fetch from 'next/dist/compiled/node-fetch'
import { nextFontError } from '../utils'
import fontData from './font-data.json'
const allowedDisplayValues = ['auto', 'block', 'swap', 'fallback', 'optional']

Expand Down Expand Up @@ -32,15 +33,15 @@ export function validateData(functionName: string, data: any): FontOptions {
subsets,
} = data[0] || ({} as any)
if (functionName === '') {
throw new Error(`@next/font/google has no default export`)
nextFontError(`@next/font/google has no default export`)
}

const fontFamily = functionName.replace(/_/g, ' ')

const fontFamilyData = (fontData as any)[fontFamily]
const fontWeights = fontFamilyData?.weights
if (!fontWeights) {
throw new Error(`Unknown font \`${fontFamily}\``)
nextFontError(`Unknown font \`${fontFamily}\``)
}
const fontStyles = fontFamilyData.styles

Expand All @@ -56,7 +57,7 @@ export function validateData(functionName: string, data: any): FontOptions {
if (fontWeights.includes('variable')) {
weights.push('variable')
} else {
throw new Error(
nextFontError(
`Missing weight for font \`${fontFamily}\`.\nAvailable weights: ${formatValues(
fontWeights
)}`
Expand All @@ -65,14 +66,14 @@ export function validateData(functionName: string, data: any): FontOptions {
}

if (weights.length > 1 && weights.includes('variable')) {
throw new Error(
nextFontError(
`Unexpected \`variable\` in weight array for font \`${fontFamily}\`. You only need \`variable\`, it includes all available weights.`
)
}

weights.forEach((selectedWeight) => {
if (!fontWeights.includes(selectedWeight)) {
throw new Error(
nextFontError(
`Unknown weight \`${selectedWeight}\` for font \`${fontFamily}\`.\nAvailable weights: ${formatValues(
fontWeights
)}`
Expand All @@ -90,7 +91,7 @@ export function validateData(functionName: string, data: any): FontOptions {

styles.forEach((selectedStyle) => {
if (!fontStyles.includes(selectedStyle)) {
throw new Error(
nextFontError(
`Unknown style \`${selectedStyle}\` for font \`${fontFamily}\`.\nAvailable styles: ${formatValues(
fontStyles
)}`
Expand All @@ -99,15 +100,15 @@ export function validateData(functionName: string, data: any): FontOptions {
})

if (!allowedDisplayValues.includes(display)) {
throw new Error(
nextFontError(
`Invalid display value \`${display}\` for font \`${fontFamily}\`.\nAvailable display values: ${formatValues(
allowedDisplayValues
)}`
)
}

if (weights[0] !== 'variable' && axes) {
throw new Error('Axes can only be defined for variable fonts')
nextFontError('Axes can only be defined for variable fonts')
}

return {
Expand Down Expand Up @@ -191,7 +192,7 @@ export async function fetchCSSFromGoogleFonts(url: string, fontFamily: string) {
const mockFile = require(process.env.NEXT_FONT_GOOGLE_MOCKED_RESPONSES)
mockedResponse = mockFile[url]
if (!mockedResponse) {
throw new Error('Missing mocked response for URL: ' + url)
nextFontError('Missing mocked response for URL: ' + url)
}
}

Expand All @@ -208,7 +209,7 @@ export async function fetchCSSFromGoogleFonts(url: string, fontFamily: string) {
})

if (!res.ok) {
throw new Error(`Failed to fetch font \`${fontFamily}\`.\nURL: ${url}`)
nextFontError(`Failed to fetch font \`${fontFamily}\`.\nURL: ${url}`)
}

cssResponse = await res.text()
Expand Down Expand Up @@ -252,18 +253,18 @@ export function getFontAxes(
.map(({ tag }) => tag)
.filter((tag) => tag !== 'wght')
if (defineAbleAxes.length === 0) {
throw new Error(`Font \`${fontFamily}\` has no definable \`axes\``)
nextFontError(`Font \`${fontFamily}\` has no definable \`axes\``)
}
if (!Array.isArray(selectedVariableAxes)) {
throw new Error(
nextFontError(
`Invalid axes value for font \`${fontFamily}\`, expected an array of axes.\nAvailable axes: ${formatValues(
defineAbleAxes
)}`
)
}
selectedVariableAxes.forEach((key) => {
if (!defineAbleAxes.some((tag) => tag === key)) {
throw new Error(
nextFontError(
`Invalid axes value \`${key}\` for font \`${fontFamily}\`.\nAvailable axes: ${formatValues(
defineAbleAxes
)}`
Expand Down
4 changes: 2 additions & 2 deletions packages/font/src/local/loader.ts
Expand Up @@ -5,7 +5,7 @@ import type { AdjustFontFallback, FontLoader } from 'next/font'

import { promisify } from 'util'
import { validateData } from './utils'
import { calculateFallbackFontValues } from '../utils'
import { calculateFallbackFontValues, nextFontError } from '../utils'

const NORMAL_WEIGHT = 400
const BOLD_WEIGHT = 700
Expand All @@ -27,7 +27,7 @@ function getDistanceFromNormalWeight(weight?: string) {
.map(getWeightNumber)

if (Number.isNaN(firstWeight) || Number.isNaN(secondWeight)) {
throw new Error(
nextFontError(
`Invalid weight value in src array: \`${weight}\`.\nExpected \`normal\`, \`bold\` or a number.`
)
}
Expand Down
14 changes: 8 additions & 6 deletions packages/font/src/local/utils.ts
@@ -1,3 +1,5 @@
import { nextFontError } from '../utils'

const allowedDisplayValues = ['auto', 'block', 'swap', 'fallback', 'optional']

const formatValues = (values: string[]) =>
Expand Down Expand Up @@ -30,7 +32,7 @@ type FontOptions = {
}
export function validateData(functionName: string, fontData: any): FontOptions {
if (functionName) {
throw new Error(`@next/font/local has no named exports`)
nextFontError(`@next/font/local has no named exports`)
}
let {
src,
Expand All @@ -45,29 +47,29 @@ export function validateData(functionName: string, fontData: any): FontOptions {
} = fontData || ({} as any)

if (!allowedDisplayValues.includes(display)) {
throw new Error(
nextFontError(
`Invalid display value \`${display}\`.\nAvailable display values: ${formatValues(
allowedDisplayValues
)}`
)
}

if (!src) {
throw new Error('Missing required `src` property')
nextFontError('Missing required `src` property')
}

if (!Array.isArray(src)) {
src = [{ path: src, weight, style }]
} else {
if (src.length === 0) {
throw new Error('Unexpected empty `src` array.')
nextFontError('Unexpected empty `src` array.')
}
}

src = src.map((fontFile: any) => {
const ext = /\.(woff|woff2|eot|ttf|otf)$/.exec(fontFile.path)?.[1]
if (!ext) {
throw new Error(`Unexpected file \`${fontFile.path}\``)
nextFontError(`Unexpected file \`${fontFile.path}\``)
}

return {
Expand All @@ -88,7 +90,7 @@ export function validateData(functionName: string, fontData: any): FontOptions {
'font-style',
].includes(declaration?.prop)
) {
throw new Error(`Invalid declaration prop: \`${declaration.prop}\``)
nextFontError(`Invalid declaration prop: \`${declaration.prop}\``)
}
})
}
Expand Down
6 changes: 6 additions & 0 deletions packages/font/src/utils.ts
Expand Up @@ -55,3 +55,9 @@ export function calculateFallbackFontValues(
sizeAdjust: formatOverrideValue(sizeAdjust),
}
}

export function nextFontError(message: string): never {
const err = new Error(message)
err.name = 'NextFontError'
throw err
}
20 changes: 0 additions & 20 deletions packages/next/build/webpack/config/blocks/css/index.ts
Expand Up @@ -10,7 +10,6 @@ import {
getGlobalImportError,
getGlobalModuleImportError,
getLocalModuleImportError,
getFontLoaderDocumentImportError,
} from './messages'
import { getPostCssPlugins } from './plugins'
import { nonNullable } from '../../../../../lib/non-nullable'
Expand Down Expand Up @@ -191,25 +190,6 @@ export const css = curry(async function css(

// 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({
Expand Down
6 changes: 0 additions & 6 deletions packages/next/build/webpack/config/blocks/css/messages.ts
Expand Up @@ -31,9 +31,3 @@ export function getCustomDocumentError() {
'pages/_document.js'
)}. Please move global styles to ${chalk.cyan('pages/_app.js')}.`
}

export function getFontLoaderDocumentImportError() {
return `Font loader error:\nFont loaders ${chalk.bold(
'cannot'
)} be used within ${chalk.cyan('pages/_document.js')}.`
}
36 changes: 22 additions & 14 deletions packages/next/build/webpack/loaders/next-font-loader/index.ts
Expand Up @@ -2,16 +2,37 @@ import type { FontLoader } from '../../../../font'

import { promises as fs } from 'fs'
import path from 'path'
import chalk from 'next/dist/compiled/chalk'
import loaderUtils from 'next/dist/compiled/loader-utils3'
import postcssFontLoaderPlugn from './postcss-font-loader'
import { promisify } from 'util'
import chalk from 'next/dist/compiled/chalk'
import { CONFIG_FILES } from '../../../../shared/lib/constants'

export default async function nextFontLoader(this: any) {
const fontLoaderSpan = this.currentTraceSpan.traceChild('next-font-loader')
return fontLoaderSpan.traceAsyncFn(async () => {
const callback = this.async()

// next-swc next_font_loaders turns each font loader call into JSON
const {
path: relativeFilePathFromRoot,
import: functionName,
arguments: data,
variableName,
} = JSON.parse(this.resourceQuery.slice(1))

// Throw error if @next/font is used in _document.js
if (/pages[\\/]_document\./.test(relativeFilePathFromRoot)) {
const err = new Error(
`${chalk.bold('Cannot')} be used within ${chalk.cyan(
'pages/_document.js'
)}.`
)
err.name = 'NextFontError'
callback(err)
return
}

const {
isDev,
isServer,
Expand Down Expand Up @@ -53,14 +74,6 @@ export default async function nextFontLoader(this: any) {
return outputPath
}

// next-swc next_font_loaders turns each font loader call into JSON
const {
path: relativeFilePathFromRoot,
import: functionName,
arguments: data,
variableName,
} = JSON.parse(this.resourceQuery.slice(1))

try {
const fontLoader: FontLoader = require(path.join(
this.resourcePath,
Expand Down Expand Up @@ -122,11 +135,6 @@ export default async function nextFontLoader(this: any) {
fontFamilyHash,
})
} catch (err: any) {
err.stack = false
err.message = `Font loader error:\n${err.message}`
err.message += `

${chalk.cyan(`Location: ${relativeFilePathFromRoot}`)}`
callback(err)
}
})
Expand Down
@@ -0,0 +1,45 @@
import { SimpleWebpackError } from './simpleWebpackError'

export function getNextFontError(
err: Error,
module: any
): SimpleWebpackError | false {
try {
const resourceResolveData = module.resourceResolveData
if (resourceResolveData.descriptionFileData.name !== '@next/font') {
return false
}

// Parse the query and get the path of the file where the font function was called.
// provided by next-swc next_font_loaders
const file = JSON.parse(resourceResolveData.query.slice(1)).path

if (err.name === 'NextFontError') {
// Known error thrown by @next/font, display the error message
return new SimpleWebpackError(
file,
`\`@next/font\` error:\n${err.message}`
)
} else {
// Unknown error thrown by @next/font
// It might be becuase of incompatible versions of @next/font and next are being used, or it might be a bug

// eslint-disable-next-line import/no-extraneous-dependencies
const nextFontVersion = require('@next/font/package.json').version
const nextVersion = require('next/package.json').version

let message = `An error occured in \`@next/font\`.`

// Using different versions of @next/font and next, add message that it's possibly fixed by updating both
if (nextFontVersion !== nextVersion) {
message += `\n\nYou might be using incompatible version of \`@next/font\` (${nextFontVersion}) and \`next\` (${nextVersion}). Try updating both \`@next/font\` and \`next\`, if the error still persists it may be a bug.`
}

message += `\n\n${err.stack}`

return new SimpleWebpackError(file, message)
}
} catch {
return false
}
}