Files
vercel__next.js/test/e2e/webpack-loader-parse-error/webpack-loader-parse-error.test.ts
2026-08-06 09:53:09 +00:00

304 lines
9.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
import stripAnsi from 'strip-ansi'
/**
* Strips the dynamic test directory prefix from paths so snapshots are stable.
* Handles both absolute paths (/full/path/test/tmp/next-test-XXX/) and
* relative paths (./test/tmp/next-test-XXX/).
*/
function normalizePaths(output: string, testDir: string): string {
let result = output.replaceAll(testDir + '/', '')
const relMatch = testDir.match(/(test\/tmp\/next-test-[^/]+)/)
if (relMatch) {
result = result.replaceAll('./' + relMatch[1] + '/', './')
result = result.replaceAll(relMatch[1] + '/', '')
}
return result
}
/**
* Extracts a single error block from CLI output by searching for a title string.
*
* Start boundary: the nearest `` marker (dev) or file-path line (prod)
* before the title.
* End boundary: the first blank line after an "Import trace" section that
* isn't followed by continuation content (indented lines or file paths).
*/
function extractErrorBlock(output: string, errorTitle: string): string {
const titleIdx = output.indexOf(errorTitle)
if (titleIdx === -1) return ''
const beforeTitle = output.substring(0, titleIdx)
// In dev mode, errors start with a `` marker on the same line as the file
// path (e.g. ` ./app/file.js`). In prod mode, errors start with a bare
// file-path line above the title. Match ` ./` specifically so we don't latch
// onto unrelated `` markers in the startup output, such as the experimental
// features list (e.g. ` exampleFlag (disabled by ...)`).
const markerIdx = beforeTitle.lastIndexOf(' ./')
let startIdx: number
if (markerIdx !== -1) {
startIdx = markerIdx
} else {
// Go back two lines: title is preceded by a file-path line (e.g. ./file:3:1)
const lines = beforeTitle.split('\n')
startIdx =
lines.length >= 2
? beforeTitle.length -
lines[lines.length - 1].length -
lines[lines.length - 2].length -
1
: 0
}
// Scan forward to find the end of the Import trace section
const block = output.substring(startIdx)
const lines = block.split('\n')
let inImportTrace = false
for (let i = 0; i < lines.length; i++) {
if (/^Import trace/.test(lines[i])) {
inImportTrace = true
}
// A blank line after the import trace ends the block, unless the next
// line is a continuation (indented or a file path)
if (inImportTrace && lines[i].trim() === '' && i > 0) {
const nextLine = lines[i + 1]
if (
!nextLine ||
(!nextLine.startsWith(' ') && !nextLine.startsWith('./'))
) {
return lines.slice(0, i).join('\n')
}
}
}
return block.trimEnd()
}
describe('webpack-loader-parse-error (development)', () => {
const { next, isTurbopack, isNextDev, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
skipStart: true,
})
if (skipped) return
if (!isNextDev) {
it('skipped in production mode', () => {})
return
}
beforeAll(() => next.start())
it('should show parse error for JS loader that returns broken code', async () => {
const outputIndex = next.cliOutput.length
// Fetch the page to trigger the error
await next.fetch('/')
// Check that the CLI output contains the parse error
// Turbopack: "Parsing ecmascript source code failed"
// Webpack: "Syntax Error"
await retry(async () => {
expect(next.cliOutput).toMatch(
/Parsing ecmascript source code failed|Syntax Error/
)
})
if (isTurbopack) {
const output = normalizePaths(
stripAnsi(next.cliOutput.slice(outputIndex)),
next.testDir
)
const errorBlock = extractErrorBlock(
output,
'Parsing ecmascript source code failed'
)
expect(errorBlock).toMatchInlineSnapshot(`
" ./app/data.broken.js:3:1
Error: Expected '</', got '{'
1 | // This file will be processed by broken-js-loader
2 | // The loader will return invalid JavaScript with a source map
> 3 | export default function Data() {
| ^
4 | return <div>original source content</div>
5 | }
6 |
Parsing ecmascript source code failed
Generated code of loaders [broken-js-loader.js] transform of file content of app/data.broken.js:
./app/data.broken.js:3:46
1 | // Generated by broken-js-loader
2 | export default function Page() {
> 3 | return <div>this is intentionally broken {{{ invalid jsx
| ^
4 | }
5 |
Import trace:
Server Component:
./app/data.broken.js
./app/page.js"
`)
}
})
it('should show parse error for CSS loader that returns broken code', async () => {
const outputIndex = next.cliOutput.length
// Fetch the page to trigger the CSS error
await next.fetch('/css-page')
// Check that the CLI output contains the CSS parse error
// Turbopack: "Parsing CSS source code failed"
// Webpack: "Unknown word"
await retry(async () => {
expect(next.cliOutput).toMatch(
/Parsing CSS source code failed|Unknown word/
)
})
if (isTurbopack) {
const output = normalizePaths(
stripAnsi(next.cliOutput.slice(outputIndex)),
next.testDir
)
const errorBlock = extractErrorBlock(
output,
'Parsing CSS source code failed'
)
expect(errorBlock).toMatchInlineSnapshot(`
" ./app/css-page/styles.broken.css:2:3
Error: Parsing CSS source code failed
1 | .page {
> 2 | color: blue;
| ^
3 | font-size: 16px;
4 | }
5 |
Unexpected token CurlyBracketBlock
Generated code of PostCSS transform of loaders [broken-css-loader.js] transform of file content of app/css-page/styles.broken.css:
./app/css-page/styles.broken.css:5:15
3 | color: red
4 | @@@ THIS IS NOT VALID CSS @@@;
> 5 | background: {{{ invalid
| ^
6 | }
7 |
Import trace:
Client Component Browser:
./app/css-page/styles.broken.css [Client Component Browser]
./app/css-page/page.js [Server Component]"
`)
}
})
})
describe('webpack-loader-parse-error (production)', () => {
const { next, isNextStart, isTurbopack } = nextTestSetup({
files: __dirname,
skipDeployment: true,
skipStart: true,
})
if (!isNextStart) {
it('skipped in development mode', () => {})
return
}
it('should fail the build with parse errors from loaders', async () => {
await expect(next.start()).rejects.toThrow(
'next build failed with code/signal 1'
)
const output = normalizePaths(stripAnsi(next.cliOutput), next.testDir)
if (isTurbopack) {
const jsError = extractErrorBlock(output, "Expected '</', got '{'")
expect(jsError).toMatchInlineSnapshot(`
"./app/data.broken.js:3:1
Error: Expected '</', got '{'
1 | // This file will be processed by broken-js-loader
2 | // The loader will return invalid JavaScript with a source map
> 3 | export default function Data() {
| ^
4 | return <div>original source content</div>
5 | }
6 |
Parsing ecmascript source code failed
Generated code of loaders [broken-js-loader.js] transform of file content of app/data.broken.js:
./app/data.broken.js:3:46
1 | // Generated by broken-js-loader
2 | export default function Page() {
> 3 | return <div>this is intentionally broken {{{ invalid jsx
| ^
4 | }
5 |
Import trace:
Server Component:
./app/data.broken.js
./app/page.js"
`)
const cssError = extractErrorBlock(
output,
'Parsing CSS source code failed'
)
expect(cssError).toMatchInlineSnapshot(`
"./app/css-page/styles.broken.css:5:15
Error: Parsing CSS source code failed
3 | color: red
4 | @@@ THIS IS NOT VALID CSS @@@;
> 5 | background: {{{ invalid
| ^
6 | }
7 |
Unexpected token CurlyBracketBlock
Generated code of PostCSS transform of loaders [broken-css-loader.js] transform of file content of app/css-page/styles.broken.css:
./app/css-page/styles.broken.css:5:15
3 | color: red
4 | @@@ THIS IS NOT VALID CSS @@@;
> 5 | background: {{{ invalid
| ^
6 | }
7 |
Import trace:
Client Component Browser:
./app/css-page/styles.broken.css [Client Component Browser]
./app/css-page/page.js [Server Component]"
`)
} else {
// Webpack stops at the first error (JS), showing the generated code
const jsError = extractErrorBlock(output, "Expected '</', got '{'")
expect(jsError).toMatchInlineSnapshot(`
"./app/data.broken.js
Error: x Expected '</', got '{'
,-[3:1]
1 | // Generated by broken-js-loader
2 | export default function Page() {
3 | return <div>this is intentionally broken {{{ invalid jsx
: ^
4 | }
\`----
Caused by:
Syntax Error
Import trace for requested module:
./app/data.broken.js
./app/page.js"
`)
}
})
})