Skip to content

Commit

Permalink
[dev-overlay] pick up build error message (vercel#76290)
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi authored Feb 21, 2025
1 parent 1f98765 commit 8e9f92b
Show file tree
Hide file tree
Showing 13 changed files with 128 additions and 52 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react'
import React, { useCallback, useMemo } from 'react'
import stripAnsi from 'next/dist/compiled/strip-ansi'
import { Terminal } from '../components/terminal'
import { noop as css } from '../helpers/noop-template'
import { ErrorOverlayLayout } from '../components/errors/error-overlay-layout/error-overlay-layout'
Expand All @@ -8,22 +9,41 @@ export interface BuildErrorProps extends ErrorBaseProps {
message: string
}

const getErrorTextFromBuildErrorMessage = (multiLineMessage: string) => {
const lines = multiLineMessage.split('\n')
// The multi-line build error message looks like:
// <file path>:<line number>:<column number>
// <error message>
// <error code frame of compiler or bundler>
// e.g.
// ./path/to/file.js:1:1
// SyntaxError: ...
// > 1 | con st foo =
// ...
return stripAnsi(lines[1] || '')
}

export const BuildError: React.FC<BuildErrorProps> = function BuildError({
message,
...props
}) {
const noop = React.useCallback(() => {}, [])
const noop = useCallback(() => {}, [])
const error = new Error(message)
const formattedMessage = useMemo(
() => getErrorTextFromBuildErrorMessage(message) || 'Failed to compile',
[message]
)

return (
<ErrorOverlayLayout
errorType="Build Error"
errorMessage="Failed to compile"
errorMessage={formattedMessage}
onClose={noop}
error={error}
footerMessage="This error occurred during the build process and can only be dismissed by fixing the error."
{...props}
>
<Terminal content={error.message} />
<Terminal content={message} />
</ErrorOverlayLayout>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ function handleErrors(errors: any) {
})

// Only show the first error.

onBuildError(formatted.errors[0])

// Also log them to the console.
Expand Down
13 changes: 9 additions & 4 deletions test/development/acceptance-app/error-recovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { FileRef, nextTestSetup } from 'e2e-utils'
import { check, describeVariants as describe } from 'next-test-utils'
import path from 'path'
import { outdent } from 'outdent'
import stripAnsi from 'strip-ansi'

describe.each(['default', 'turbo'])('Error recovery app %s', () => {
const { next } = nextTestSetup({
const { next, isTurbopack } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
})
Expand Down Expand Up @@ -470,8 +471,12 @@ describe.each(['default', 'turbo'])('Error recovery app %s', () => {
)
const { session } = sandbox
await session.assertHasRedbox()
await expect(session.getRedboxSource(true)).resolves.toMatch(
/Failed to compile/
)

const source = stripAnsi(await session.getRedboxSource(true))
if (isTurbopack) {
expect(source).toMatch(/Parsing ecmascript source code failed/)
} else {
expect(source).toMatch(/x Expected '}', got '<eof>'/)
}
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ describe('Dynamic IO Dev Errors', () => {
await expect(browser).toDisplayRedbox(`
{
"count": 1,
"description": "Failed to compile",
"description": "Ecmascript file had an error",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.tsx (1:14)
Expand All @@ -156,7 +156,7 @@ describe('Dynamic IO Dev Errors', () => {
await expect(browser).toDisplayRedbox(`
{
"count": 1,
"description": "Failed to compile",
"description": "Error: x Route segment config "revalidate" is not compatible with \`nextConfig.experimental.dynamicIO\`. Please remove it.",
"environmentLabel": null,
"label": "Build Error",
"source": "./app/page.tsx
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ describe('app-dir - server-component-next-dynamic-ssr-false', () => {
source: await getRedboxSource(browser),
}

expect(redbox.description).toBe('Failed to compile')

if (process.env.TURBOPACK) {
expect(redbox.description).toMatchInlineSnapshot(
`"Ecmascript file had an error"`
)
expect(redbox.source).toMatchInlineSnapshot(`
"./app/page.js (3:23)
Ecmascript file had an error
Expand All @@ -35,6 +36,9 @@ describe('app-dir - server-component-next-dynamic-ssr-false', () => {
\`ssr: false\` is not allowed with \`next/dynamic\` in Server Components. Please move it into a client component."
`)
} else {
expect(redbox.description).toMatchInlineSnapshot(
`"Error: x \`ssr: false\` is not allowed with \`next/dynamic\` in Server Components. Please move it into a client component."`
)
expect(redbox.source).toMatchInlineSnapshot(`
"./app/page.js
Error: x \`ssr: false\` is not allowed with \`next/dynamic\` in Server Components. Please move it into a client component.
Expand Down
50 changes: 25 additions & 25 deletions test/development/app-dir/ssr-in-rsc/ssr-in-rsc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ describe('react-dom/server in React Server environment', () => {
if (isTurbopack) {
expect(redbox).toMatchInlineSnapshot(`
{
"description": "Failed to compile",
"description": "Ecmascript file had an error",
"source": "./app/exports/app-code/react-dom-server-edge-implicit/page.js (3:1)
Ecmascript file had an error
1 | import * as ReactDOMServerEdge from 'react-dom/server'
Expand Down Expand Up @@ -378,7 +378,7 @@ describe('react-dom/server in React Server environment', () => {
if (isTurbopack) {
expect(redbox).toMatchInlineSnapshot(`
{
"description": "Failed to compile",
"description": "Ecmascript file had an error",
"source": "./app/exports/app-code/react-dom-server-node-implicit/page.js (3:1)
Ecmascript file had an error
1 | import * as ReactDOMServerNode from 'react-dom/server'
Expand All @@ -395,29 +395,29 @@ describe('react-dom/server in React Server environment', () => {
`)
} else {
expect(redbox).toMatchInlineSnapshot(`
{
"description": "Failed to compile",
"source": "./app/exports/app-code/react-dom-server-node-implicit/page.js
Error: x You're importing a component that imports react-dom/server. To fix it, render or return the content directly as a Server Component instead for perf and security.
| Learn more: https://nextjs.org/docs/app/building-your-application/rendering
,-[1:1]
1 | import * as ReactDOMServerNode from 'react-dom/server'
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 | // Fine to drop once React is on ESM
3 | import ReactDOMServerNodeDefault from 'react-dom/server'
\`----
x You're importing a component that imports react-dom/server. To fix it, render or return the content directly as a Server Component instead for perf and security.
| Learn more: https://nextjs.org/docs/app/building-your-application/rendering
,-[3:1]
1 | import * as ReactDOMServerNode from 'react-dom/server'
2 | // Fine to drop once React is on ESM
3 | import ReactDOMServerNodeDefault from 'react-dom/server'
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 |
5 | export const runtime = 'nodejs'
\`----",
}
`)
{
"description": "Error: x You're importing a component that imports react-dom/server. To fix it, render or return the content directly as a Server Component instead for perf and security.",
"source": "./app/exports/app-code/react-dom-server-node-implicit/page.js
Error: x You're importing a component that imports react-dom/server. To fix it, render or return the content directly as a Server Component instead for perf and security.
| Learn more: https://nextjs.org/docs/app/building-your-application/rendering
,-[1:1]
1 | import * as ReactDOMServerNode from 'react-dom/server'
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 | // Fine to drop once React is on ESM
3 | import ReactDOMServerNodeDefault from 'react-dom/server'
\`----
x You're importing a component that imports react-dom/server. To fix it, render or return the content directly as a Server Component instead for perf and security.
| Learn more: https://nextjs.org/docs/app/building-your-application/rendering
,-[3:1]
1 | import * as ReactDOMServerNode from 'react-dom/server'
2 | // Fine to drop once React is on ESM
3 | import ReactDOMServerNodeDefault from 'react-dom/server'
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 |
5 | export const runtime = 'nodejs'
\`----",
}
`)
}
})

Expand Down
4 changes: 2 additions & 2 deletions test/development/basic/hmr/error-recovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ describe.each([
)

await assertHasRedbox(browser)
expect(await getRedboxHeader(browser)).toMatch('Failed to compile')
expect(await getRedboxHeader(browser)).toMatch('Build Error')

if (process.env.TURBOPACK) {
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -587,7 +587,7 @@ describe.each([
)

await assertHasRedbox(browser)
expect(await getRedboxHeader(browser)).toMatch('Failed to compile')
expect(await getRedboxHeader(browser)).toMatch('Build Error')
let redboxSource = await getRedboxSource(browser)

redboxSource = redboxSource.replace(`${next.testDir}`, '.')
Expand Down
14 changes: 11 additions & 3 deletions test/development/middleware-errors/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
assertHasRedbox,
assertNoRedbox,
check,
getRedboxDescription,
getRedboxSource,
retry,
} from 'next-test-utils'
Expand Down Expand Up @@ -330,9 +331,16 @@ describe('middleware - development errors', () => {
it('renders the error correctly and recovers', async () => {
const browser = await next.browser('/')
await assertHasRedbox(browser)
expect(
await browser.elementByCss('#nextjs__container_errors_desc').text()
).toEqual('Failed to compile')
const description = await getRedboxDescription(browser)
if (isTurbopack) {
expect(description).toMatchInlineSnapshot(
`"Parsing ecmascript source code failed"`
)
} else {
expect(description).toMatchInlineSnapshot(
`"Error: x Expected '{', got '}'"`
)
}
await next.patchFile('middleware.js', `export default function () {}`)
await assertNoRedbox(browser)
expect(await browser.elementByCss('#page-title')).toBeTruthy()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ describe('dynamic-io-segment-configs', () => {
source: await getRedboxSource(browser),
}

expect(redbox.description).toMatchInlineSnapshot(`"Failed to compile"`)
if (isTurbopack) {
expect(redbox.description).toMatchInlineSnapshot(
`"Ecmascript file had an error"`
)
} else {
expect(redbox.description).toMatchInlineSnapshot(
`"Error: x Route segment config "revalidate" is not compatible with \`nextConfig.experimental.dynamicIO\`. Please remove it."`
)
}
expect(redbox.source).toContain(
'"revalidate" is not compatible with `nextConfig.experimental.dynamicIO`. Please remove it.'
)
Expand Down Expand Up @@ -85,9 +93,15 @@ describe('dynamic-io-segment-configs', () => {
source: await getRedboxSource(browser),
}

expect(redbox.description).toMatchInlineSnapshot(
`"Failed to compile"`
)
if (isTurbopack) {
expect(redbox.description).toMatchInlineSnapshot(
`"Ecmascript file had an error"`
)
} else {
expect(redbox.description).toMatchInlineSnapshot(
`"Error: x Route segment config "runtime" is not compatible with \`nextConfig.experimental.dynamicIO\`. Please remove it."`
)
}
expect(redbox.source).toContain(
'"runtime" is not compatible with `nextConfig.experimental.dynamicIO`. Please remove it.'
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ describe('use-cache-segment-configs', () => {
await assertHasRedbox(browser)

const description = await getRedboxDescription(browser)
expect(description).toMatchInlineSnapshot(
`"Ecmascript file had an error"`
)
const source = await getRedboxSource(browser)

expect(description).toBe('Failed to compile')

expect(source).toMatchInlineSnapshot(`
"./app/runtime/page.tsx (1:14)
Ecmascript file had an error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,15 @@ describe('use-cache-unknown-cache-kind', () => {
const errorDescription = await getRedboxDescription(browser)
const errorSource = await getRedboxSource(browser)

expect(errorDescription).toBe('Failed to compile')
if (isTurbopack) {
expect(errorDescription).toMatchInlineSnapshot(
`"Ecmascript file had an error"`
)
} else {
expect(errorDescription).toMatchInlineSnapshot(
`"Error: x Unknown cache kind "custom". Please configure a cache handler for this kind in the "experimental.cacheHandlers" object in your Next.js config."`
)
}

if (isTurbopack) {
expect(errorSource).toMatchInlineSnapshot(`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,15 @@ describe('use-cache-without-experimental-flag', () => {
const errorDescription = await getRedboxDescription(browser)
const errorSource = await getRedboxSource(browser)

expect(errorDescription).toBe('Failed to compile')
if (isTurbopack) {
expect(errorDescription).toMatchInlineSnapshot(
`"Ecmascript file had an error"`
)
} else {
expect(errorDescription).toMatchInlineSnapshot(
`"Error: x To use "use cache", please enable the experimental feature flag "useCache" in your Next.js config."`
)
}

if (isTurbopack) {
expect(errorSource).toMatchInlineSnapshot(`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { join } from 'path'
import {
assertHasRedbox,
findPort,
getRedboxHeader,
getRedboxDescription,
getRedboxSource,
killApp,
launchApp,
Expand All @@ -23,7 +23,14 @@ function runTests({ isDev }) {
if (isDev) {
const browser = await webdriver(appPort, '/')
await assertHasRedbox(browser)
expect(await getRedboxHeader(browser)).toMatch('Failed to compile')
const description = await getRedboxDescription(browser)
if (process.env.TURBOPACK) {
expect(description).toMatchInlineSnapshot(`"Processing image failed"`)
} else {
expect(description).toMatchInlineSnapshot(
`"Error: Image import "../public/invalid.svg" is not a valid image file. The image may be corrupted or an unsupported format."`
)
}
const source = await getRedboxSource(browser)
if (process.env.TURBOPACK) {
expect(source).toMatchInlineSnapshot(`
Expand Down

0 comments on commit 8e9f92b

Please sign in to comment.