Better Babel syntax errors (#12821)
This commit is contained in:
parent
615f658da4
commit
f1423518b9
8 changed files with 153 additions and 9 deletions
|
@ -50,6 +50,7 @@ import WebpackConformancePlugin, {
|
|||
MinificationConformanceCheck,
|
||||
ReactSyncScriptsConformanceCheck,
|
||||
} from './webpack/plugins/webpack-conformance-plugin'
|
||||
import { WellKnownErrorsPlugin } from './webpack/plugins/wellknown-errors-plugin'
|
||||
|
||||
type ExcludesFalse = <T>(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),
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 ?? '<unknown>'
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -33,7 +33,13 @@ test('logbox: can recover from a syntax error without losing state', async () =>
|
|||
await session.patch('index.js', `export default () => <div/`)
|
||||
|
||||
expect(await session.hasRedbox(true)).toBe(true)
|
||||
expect(await session.getRedboxSource()).toMatch('SyntaxError')
|
||||
expect(await session.getRedboxSource()).toMatchInlineSnapshot(`
|
||||
"./index.js:1:26
|
||||
Syntax error: Unexpected token, expected \\"jsxTagEnd\\"
|
||||
|
||||
> 1 | export default () => <div/
|
||||
| ^"
|
||||
`)
|
||||
|
||||
await session.patch(
|
||||
'index.js',
|
||||
|
@ -482,18 +488,33 @@ test('syntax > 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 | <div>
|
||||
> 5 | <p>lol</p>
|
||||
| ^
|
||||
6 | div
|
||||
7 | )
|
||||
8 | }"
|
||||
`)
|
||||
|
||||
await cleanup()
|
||||
})
|
||||
|
|
|
@ -46,7 +46,7 @@ describe('Custom _error', () => {
|
|||
await fs.writeFile(page404, 'export default <h1>')
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue