diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index f35686ebe8..895ffc9e19 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -50,6 +50,7 @@ import WebpackConformancePlugin, { MinificationConformanceCheck, ReactSyncScriptsConformanceCheck, } from './webpack/plugins/webpack-conformance-plugin' +import { WellKnownErrorsPlugin } from './webpack/plugins/wellknown-errors-plugin' type ExcludesFalse = (x: T | false) => x is T @@ -1004,6 +1005,7 @@ export default async function getBaseWebpackConfig( }), ].filter(Boolean), }), + new WellKnownErrorsPlugin(), ].filter((Boolean as any) as ExcludesFalse), } diff --git a/packages/next/build/webpack/plugins/wellknown-errors-plugin/index.ts b/packages/next/build/webpack/plugins/wellknown-errors-plugin/index.ts new file mode 100644 index 0000000000..48857d9970 --- /dev/null +++ b/packages/next/build/webpack/plugins/wellknown-errors-plugin/index.ts @@ -0,0 +1,17 @@ +import { Compiler } from 'webpack' +import { getModuleBuildError } from './webpackModuleError' + +export class WellKnownErrorsPlugin { + apply(compiler: Compiler) { + compiler.hooks.compilation.tap('WellKnownErrorsPlugin', compilation => { + compilation.hooks.seal.tap('WellKnownErrorsPlugin', () => { + if (compilation.errors?.length) { + compilation.errors = compilation.errors.map(err => { + const moduleError = getModuleBuildError(compilation, err) + return moduleError === false ? err : moduleError + }) + } + }) + }) + } +} diff --git a/packages/next/build/webpack/plugins/wellknown-errors-plugin/parseBabel.ts b/packages/next/build/webpack/plugins/wellknown-errors-plugin/parseBabel.ts new file mode 100644 index 0000000000..77d2a93288 --- /dev/null +++ b/packages/next/build/webpack/plugins/wellknown-errors-plugin/parseBabel.ts @@ -0,0 +1,40 @@ +import Chalk from 'next/dist/compiled/chalk' +import { SimpleWebpackError } from './simpleWebpackError' + +const chalk = new Chalk.constructor({ enabled: true }) + +export function getBabelError( + fileName: string, + err: Error & { + code?: 'BABEL_PARSE_ERROR' + loc?: { line: number; column: number } + } +): SimpleWebpackError | false { + if (err.code !== 'BABEL_PARSE_ERROR') { + return false + } + + // https://github.com/babel/babel/blob/34693d6024da3f026534dd8d569f97ac0109602e/packages/babel-core/src/parser/index.js + if (err.loc) { + const lineNumber = Math.max(1, err.loc.line) + const column = Math.max(1, err.loc.column) + + let message = err.message + // Remove file information, which instead is provided by webpack. + .replace(/^.+?: /, '') + // Remove column information from message + .replace( + new RegExp(`[^\\S\\r\\n]*\\(${lineNumber}:${column}\\)[^\\S\\r\\n]*`), + '' + ) + + return new SimpleWebpackError( + `${chalk.cyan(fileName)}:${chalk.yellow( + lineNumber.toString() + )}:${chalk.yellow(column.toString())}`, + chalk.red.bold('Syntax error').concat(`: ${message}`) + ) + } + + return false +} diff --git a/packages/next/build/webpack/plugins/wellknown-errors-plugin/simpleWebpackError.ts b/packages/next/build/webpack/plugins/wellknown-errors-plugin/simpleWebpackError.ts new file mode 100644 index 0000000000..f934c9ff48 --- /dev/null +++ b/packages/next/build/webpack/plugins/wellknown-errors-plugin/simpleWebpackError.ts @@ -0,0 +1,11 @@ +// This class creates a simplified webpack error that formats nicely based on +// webpack's build in serializer. +// https://github.com/webpack/webpack/blob/c9d4ff7b054fc581c96ce0e53432d44f9dd8ca72/lib/Stats.js#L294-L356 +export class SimpleWebpackError extends Error { + file: string + + constructor(file: string, message: string) { + super(message) + this.file = file + } +} diff --git a/packages/next/build/webpack/plugins/wellknown-errors-plugin/webpackModuleError.ts b/packages/next/build/webpack/plugins/wellknown-errors-plugin/webpackModuleError.ts new file mode 100644 index 0000000000..4ab7ae5000 --- /dev/null +++ b/packages/next/build/webpack/plugins/wellknown-errors-plugin/webpackModuleError.ts @@ -0,0 +1,43 @@ +import * as path from 'path' +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { compilation } from 'webpack' +import { getBabelError } from './parseBabel' +import { SimpleWebpackError } from './simpleWebpackError' + +function getFilename(compilation: compilation.Compilation, m: any): string { + const requestShortener = compilation.requestShortener + if (typeof m?.readableIdentifier === 'function') { + return m.readableIdentifier(requestShortener) + } + + if (typeof m.resource === 'string') { + const res = path.relative(compilation.context, m.resource) + return res.startsWith('.') ? res : `.${path.sep}${res}` + } + return m.request ?? '' +} + +export function getModuleBuildError( + compilation: compilation.Compilation, + input: any +): SimpleWebpackError | false { + if ( + !( + typeof input === 'object' && + input?.name === 'ModuleBuildError' && + Boolean(input.module) && + input.error instanceof Error + ) + ) { + return false + } + + const err: Error = input.error + const sourceFilename = getFilename(compilation, input.module) + const babel = getBabelError(sourceFilename, err) + if (babel !== false) { + return babel + } + + return false +} diff --git a/test/acceptance/ReactRefreshLogBox.test.js b/test/acceptance/ReactRefreshLogBox.test.js index b1ccf854fa..0fab9b385d 100644 --- a/test/acceptance/ReactRefreshLogBox.test.js +++ b/test/acceptance/ReactRefreshLogBox.test.js @@ -33,7 +33,13 @@ test('logbox: can recover from a syntax error without losing state', async () => await session.patch('index.js', `export default () =>
1 | export default () =>
runtime error', async () => { i++ throw Error('no ' + i) }, 1000) - export default function FunctionNamed() { - ` + export default function FunctionNamed() {` ) await new Promise(resolve => setTimeout(resolve, 1000)) expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatch('SyntaxError') + expect(await session.getRedboxSource()).toMatchInlineSnapshot(` + "./index.js:8:47 + Syntax error: Unexpected token + + 6 | throw Error('no ' + i) + 7 | }, 1000) + > 8 | export default function FunctionNamed() { + | ^" + `) // Test that runtime error does not take over: await new Promise(resolve => setTimeout(resolve, 2000)) expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatch('SyntaxError') + expect(await session.getRedboxSource()).toMatchInlineSnapshot(` + "./index.js:8:47 + Syntax error: Unexpected token + + 6 | throw Error('no ' + i) + 7 | }, 1000) + > 8 | export default function FunctionNamed() { + | ^" + `) await cleanup() }) @@ -600,8 +621,18 @@ test('unterminated JSX', async () => { expect(await session.hasRedbox(true)).toBe(true) const source = await session.getRedboxSource() - expect(source).not.toMatch('Unexpected token') - expect(source).toMatch('Unterminated JSX contents') + expect(source).toMatchInlineSnapshot(` + "./index.js:5:22 + Syntax error: Unterminated JSX contents + + 3 | return ( + 4 |
+ > 5 |

lol

+ | ^ + 6 | div + 7 | ) + 8 | }" + `) await cleanup() }) diff --git a/test/integration/custom-error/test/index.test.js b/test/integration/custom-error/test/index.test.js index c42259d0c1..0ea8161ebf 100644 --- a/test/integration/custom-error/test/index.test.js +++ b/test/integration/custom-error/test/index.test.js @@ -46,7 +46,7 @@ describe('Custom _error', () => { await fs.writeFile(page404, 'export default

') const html = await renderViaHTTP(appPort, '/404') await fs.remove(page404) - expect(html).toContain('Module build failed') + expect(html).toContain('Syntax error') expect(stderr).not.toMatch(customErrNo404Match) }) }) diff --git a/test/integration/typescript-numeric-sep-exclusive/test/index.test.js b/test/integration/typescript-numeric-sep-exclusive/test/index.test.js index 81a569fe19..66407c22b6 100644 --- a/test/integration/typescript-numeric-sep-exclusive/test/index.test.js +++ b/test/integration/typescript-numeric-sep-exclusive/test/index.test.js @@ -16,7 +16,7 @@ describe('TypeScript Exclusivity of Numeric Separator', () => { expect(code).toBe(1) expect(stderr).toContain('Failed to compile.') - expect(stderr).toContain('SyntaxError:') + expect(stderr).toContain('Syntax error') expect(stderr).toContain('config to enable transformation') }) })