Skip to content

Commit

Permalink
Improve @next/font error handling (#43298)
Browse files Browse the repository at this point in the history
Use `WellKnownErrorsPlugin` when formatting `@next/font` errors.

The error handling is different for expected errors (like validation
errors) and unknown ones. If the error is expected it just displays the
formatted error message. If it's an unknown error it will also display
the stacktrace, and in case the versions of `@next/font` and `next` are
different it will encourage you to try to update them both.

Removes the usage of `error-loader` when importing from
`pages/_document.js`, handle that case with `WellKnownErrorsPlugin` as
well.

The current warning when using different versions is removed. If the
versions are incompatible you'll get an error in the overlay instead.

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/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`
- [ ]
[e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
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`](https://github.com/vercel/next.js/blob/canary/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)
  • Loading branch information
Hannes Bornö committed Nov 24, 2022
1 parent ec7609e commit 5788f60
Show file tree
Hide file tree
Showing 20 changed files with 203 additions and 96 deletions.
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
}
}

0 comments on commit 5788f60

Please sign in to comment.