mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
## What Replaces the `@babel/code-frame` dependency with a new Rust-based implementation (`next-code-[frame](https://github.com/arthurprs/qfilter/pull/20#issuecomment-3986882055)` crate) for rendering code frames in error messages. ### Why - **Crash fix**: `@babel/code-frame` uses the `js-tokens` library for syntax highlighting, which has [known issues](https://github.com/lydell/js-tokens?tab=readme-ov-file#known-failures) with large string literals and long lines. This can cause Next.js to throw RangeErrors when rendering errors, hiding the original issue! - **Long line support**: The old implementation had no concept of terminal width, dumping entire lines into the output. The new implementation uses "horizontal scrolling" — truncating lines and centering the error location in the visible window. - **Performance**: The Rust implementation only processes the visible line range (typically ~6 lines), not the entire file. Syntax highlighting uses a skip-scan heuristic to start tokenizing near the visible window rather than from byte 0. - **Dependency reduction**: Drops the semi-unmaintained `@babel/code-frame` bundled dependency in favor of code we control. ### Benchmarks In-process benchmarks comparing `render_code_frame()` (Rust, via criterion) against `codeFrameColumns()` (Babel, via hrtime with DCE prevention). Both have syntax highlighting and color output enabled. No process startup or file I/O is included in the measurement. | Scenario | `next-code-frame` (Rust) | `@babel/code-frame` | Speedup | |---|---|---|---| | Small file (~490 lines TSX) | **5.4 µs** | 507 µs | **~94x** | | Large file (~39k lines JS) | **143 µs** | 82.9 ms | **~580x** | | Large file minified | **51 µs** | - | **-** | The gap widens with file size because Babel's `highlight()` runs a regex tokenizer over the **entire source** before slicing to the visible window, while the Rust implementation uses a windowed line index and skip-scan heuristic — only processing the visible window regardless of file size. However, for minified files we do end up tokenizing the whole thing so we end up being only 8x faster ### How - New `crates/next-code-frame/` Rust crate with: - Frame rendering with terminal-width-aware horizontal scrolling - Regex-based syntax highlighting (matching Babel's color scheme) - Skip-scan heuristic for O(1)-ish highlighting regardless of file size - Windowed line index that only scans/stores offsets for the visible region - Comprehensive test suite (800+ lines) - Exposed via both NAPI (native) and WASM bindings - JS wrappers in `packages/next/src/shared/lib/errors/`: - `code-frame.ts` — primary wrapper using native bindings - `optional-code-frame.ts` — graceful fallback returning `undefined` if bindings are unavailable - All existing callsites (`diagnosticFormatter`, `parseScss`, dev overlay, turbopack utils) updated - for `patch-error-inspect.ts` i adopted an `injection` style approach to avoid coupling to the native dependency ### Concerns / review focus areas - **Reliability**: The regex-based tokenizer is best-effort and language-agnostic — it should never crash on invalid syntax, but highlighting accuracy may differ from Babel's `js-tokens` in edge cases. - **Native dependency**: This moves code frame rendering into the native binary. Performance should be better, but worth verifying there are no regressions in environments where native bindings behave differently (e.g. WASM fallback path). - **Regressions**: The output format and color scheme closely match Babel's, but there may be subtle differences. The horizontal scrolling behavior is new. Fixes #85357 Closes PACK-5754
921 lines
40 KiB
TypeScript
921 lines
40 KiB
TypeScript
import * as path from 'path'
|
||
import { nextTestSetup } from 'e2e-utils'
|
||
import stripAnsi from 'strip-ansi'
|
||
import { retry } from 'next-test-utils'
|
||
|
||
function normalizeCliOutput(output: string) {
|
||
return (
|
||
stripAnsi(output)
|
||
// TODO(veil): Should not appear in sourcemapped stackframes.
|
||
.replaceAll('webpack:///', 'bundler:///')
|
||
.replaceAll(/at [a-zA-Z] \(/g, 'at <mangled> (')
|
||
)
|
||
}
|
||
|
||
describe('app-dir - server source maps', () => {
|
||
const dependencies = {
|
||
// `link:` simulates a package in a monorepo
|
||
'internal-pkg': `link:./internal-pkg`,
|
||
'external-pkg': `file:./external-pkg`,
|
||
}
|
||
const { skipped, next, isNextDev, isTurbopack, isRspack } = nextTestSetup({
|
||
dependencies,
|
||
files: path.join(__dirname, 'fixtures/default'),
|
||
// Deploy tests don't have access to runtime logs.
|
||
// Manually verify that the runtime logs match.
|
||
skipDeployment: true,
|
||
})
|
||
|
||
if (skipped) return
|
||
|
||
it('logged errors have a sourcemapped stack with a codeframe', async () => {
|
||
if (isNextDev) {
|
||
const outputIndex = next.cliOutput.length
|
||
await next.render('/rsc-error-log')
|
||
|
||
await retry(() => {
|
||
expect(next.cliOutput.slice(outputIndex)).toContain(
|
||
'Error: rsc-error-log'
|
||
)
|
||
})
|
||
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
|
||
'Error: rsc-error-log' +
|
||
'\n at logError (app/rsc-error-log/page.js:4:17)' +
|
||
'\n at Page (app/rsc-error-log/page.js:9:3)' +
|
||
'\n 2 |' +
|
||
'\n 3 | function logError() {' +
|
||
"\n> 4 | const error = new Error('rsc-error-log')" +
|
||
'\n | ^' +
|
||
'\n 5 | console.error(error)' +
|
||
'\n 6 | }' +
|
||
'\n 7 |' +
|
||
'\n'
|
||
)
|
||
} else {
|
||
if (isTurbopack) {
|
||
// TODO(veil): Sourcemap names
|
||
// TODO(veil): relative paths in webpack
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain(
|
||
'(app/rsc-error-log/page.js:4:17)'
|
||
)
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain(
|
||
'' +
|
||
"\n> 4 | const error = new Error('rsc-error-log')" +
|
||
'\n | ^'
|
||
)
|
||
} else {
|
||
// TODO(veil): line/column numbers are flaky in Webpack
|
||
}
|
||
}
|
||
})
|
||
|
||
it('logged errors have a sourcemapped `cause`', async () => {
|
||
if (isNextDev) {
|
||
const outputIndex = next.cliOutput.length
|
||
await next.render('/rsc-error-log-cause')
|
||
|
||
await retry(() => {
|
||
expect(next.cliOutput.slice(outputIndex)).toContain(
|
||
'Error: rsc-error-log-cause'
|
||
)
|
||
})
|
||
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
|
||
'Error: rsc-error-log-cause' +
|
||
'\n at logError (app/rsc-error-log-cause/page.js:2:17)' +
|
||
'\n at Page (app/rsc-error-log-cause/page.js:8:3)' +
|
||
'\n 1 | function logError(cause) {' +
|
||
"\n> 2 | const error = new Error('rsc-error-log-cause', { cause })" +
|
||
'\n | ^' +
|
||
'\n 3 | console.error(error)' +
|
||
'\n 4 | }' +
|
||
'\n 5 | {' +
|
||
'\n [cause]: Error: Boom' +
|
||
'\n at Page (app/rsc-error-log-cause/page.js:7:17)' +
|
||
'\n 5 |' +
|
||
'\n 6 | export default function Page() {' +
|
||
"\n > 7 | const error = new Error('Boom')" +
|
||
'\n | ^' +
|
||
'\n 8 | logError(error)' +
|
||
'\n 9 | return null' +
|
||
'\n 10 | }' +
|
||
'\n'
|
||
)
|
||
} else {
|
||
if (isTurbopack) {
|
||
// TODO(veil): Sourcemap names
|
||
// TODO(veil): relative paths
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain(
|
||
'(app/rsc-error-log-cause/page.js:2:17)'
|
||
)
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain(
|
||
'(app/rsc-error-log-cause/page.js:7:17)'
|
||
)
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain(
|
||
'' +
|
||
"\n> 2 | const error = new Error('rsc-error-log-cause', { cause })" +
|
||
'\n | ^'
|
||
)
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain(
|
||
'' +
|
||
"\n > 7 | const error = new Error('Boom')" +
|
||
'\n | ^'
|
||
)
|
||
} else {
|
||
// TODO(veil): line/column numbers are flaky in Webpack
|
||
}
|
||
}
|
||
})
|
||
|
||
it('logged errors collapse deeply nested causes at depth 2', async () => {
|
||
if (isNextDev) {
|
||
const outputIndex = next.cliOutput.length
|
||
await next.render('/rsc-error-log-nested')
|
||
|
||
await retry(() => {
|
||
expect(next.cliOutput.slice(outputIndex)).toContain(
|
||
'Error: rsc-error-log-nested'
|
||
)
|
||
})
|
||
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
|
||
'Error: rsc-error-log-nested' +
|
||
'\n at logError (app/rsc-error-log-nested/page.js:6:18)' +
|
||
'\n at Page (app/rsc-error-log-nested/page.js:11:3)' +
|
||
"\n 4 | const depth2 = new Error('Depth 2 error', { cause: depth3 })" +
|
||
"\n 5 | const depth1 = new Error('Depth 1 error', { cause: depth2 })" +
|
||
"\n> 6 | const depth0 = new Error('rsc-error-log-nested', { cause: depth1 })" +
|
||
'\n | ^' +
|
||
'\n 7 | console.error(depth0)' +
|
||
'\n 8 | }' +
|
||
'\n 9 | {' +
|
||
'\n [cause]: Error: Depth 1 error' +
|
||
'\n at logError (app/rsc-error-log-nested/page.js:5:18)' +
|
||
'\n at Page (app/rsc-error-log-nested/page.js:11:3)' +
|
||
"\n 3 | const depth3 = new Error('Depth 3 error', { cause: depth4 })" +
|
||
"\n 4 | const depth2 = new Error('Depth 2 error', { cause: depth3 })" +
|
||
"\n > 5 | const depth1 = new Error('Depth 1 error', { cause: depth2 })" +
|
||
'\n | ^' +
|
||
"\n 6 | const depth0 = new Error('rsc-error-log-nested', { cause: depth1 })" +
|
||
'\n 7 | console.error(depth0)' +
|
||
'\n 8 | } {' +
|
||
'\n [cause]: Error: Depth 2 error' +
|
||
'\n at logError (app/rsc-error-log-nested/page.js:4:18)' +
|
||
'\n at Page (app/rsc-error-log-nested/page.js:11:3)' +
|
||
"\n 2 | const depth4 = new Error('Depth 4 error')" +
|
||
"\n 3 | const depth3 = new Error('Depth 3 error', { cause: depth4 })" +
|
||
"\n > 4 | const depth2 = new Error('Depth 2 error', { cause: depth3 })" +
|
||
'\n | ^' +
|
||
"\n 5 | const depth1 = new Error('Depth 1 error', { cause: depth2 })" +
|
||
"\n 6 | const depth0 = new Error('rsc-error-log-nested', { cause: depth1 })" +
|
||
'\n 7 | console.error(depth0) {' +
|
||
'\n [cause]: [Error]' +
|
||
'\n }' +
|
||
'\n }' +
|
||
'\n}'
|
||
)
|
||
// Verify depth 3+ are NOT shown (truncated to [Error])
|
||
expect(
|
||
normalizeCliOutput(next.cliOutput.slice(outputIndex))
|
||
).not.toContain('Error: Depth 3 error')
|
||
expect(
|
||
normalizeCliOutput(next.cliOutput.slice(outputIndex))
|
||
).not.toContain('Error: Depth 4 error')
|
||
} else {
|
||
if (isTurbopack) {
|
||
// TODO(veil): Sourcemap names
|
||
// TODO(veil): relative paths in production
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain(
|
||
'(app/rsc-error-log-nested/page.js:6:18)'
|
||
)
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain('[cause]: [Error]')
|
||
expect(normalizeCliOutput(next.cliOutput)).not.toContain(
|
||
'Error: Depth 3 error'
|
||
)
|
||
expect(normalizeCliOutput(next.cliOutput)).not.toContain(
|
||
'Error: Depth 4 error'
|
||
)
|
||
} else {
|
||
// TODO(veil): line/column numbers are flaky in Webpack
|
||
}
|
||
}
|
||
})
|
||
|
||
it('logged errors include `[errors]` for AggregateError', async () => {
|
||
if (isNextDev) {
|
||
const outputIndex = next.cliOutput.length
|
||
await next.render('/rsc-error-log-aggregate')
|
||
|
||
await retry(() => {
|
||
expect(next.cliOutput.slice(outputIndex)).toContain(
|
||
'AggregateError: rsc-error-log-aggregate'
|
||
)
|
||
})
|
||
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
|
||
'AggregateError: rsc-error-log-aggregate' +
|
||
'\n at logError (app/rsc-error-log-aggregate/page.js:6:26)' +
|
||
'\n at Page (app/rsc-error-log-aggregate/page.js:15:3)' +
|
||
"\n 4 | const error2 = new TypeError('Error 2')" +
|
||
"\n 5 | const rootError = new Error('Root error')" +
|
||
'\n> 6 | const aggregateError = new AggregateError(' +
|
||
'\n | ^' +
|
||
'\n 7 | [error1, error2],' +
|
||
"\n 8 | 'rsc-error-log-aggregate'," +
|
||
'\n 9 | { cause: rootError } {' +
|
||
'\n [cause]: Error: Root error' +
|
||
'\n at logError (app/rsc-error-log-aggregate/page.js:5:21)' +
|
||
'\n at Page (app/rsc-error-log-aggregate/page.js:15:3)' +
|
||
"\n 3 | const error1 = new Error('Error 1')" +
|
||
"\n 4 | const error2 = new TypeError('Error 2')" +
|
||
"\n > 5 | const rootError = new Error('Root error')" +
|
||
'\n | ^' +
|
||
'\n 6 | const aggregateError = new AggregateError(' +
|
||
'\n 7 | [error1, error2],' +
|
||
"\n 8 | 'rsc-error-log-aggregate',," +
|
||
'\n [errors]: [' +
|
||
'\n Error: Error 1' +
|
||
'\n at logError (app/rsc-error-log-aggregate/page.js:3:18)' +
|
||
'\n at Page (app/rsc-error-log-aggregate/page.js:15:3)' +
|
||
'\n 1 | /* global AggregateError */' +
|
||
'\n 2 | function logError() {' +
|
||
"\n > 3 | const error1 = new Error('Error 1')" +
|
||
'\n | ^' +
|
||
"\n 4 | const error2 = new TypeError('Error 2')" +
|
||
"\n 5 | const rootError = new Error('Root error')" +
|
||
'\n 6 | const aggregateError = new AggregateError(,' +
|
||
'\n TypeError: Error 2' +
|
||
'\n at logError (app/rsc-error-log-aggregate/page.js:4:18)' +
|
||
'\n at Page (app/rsc-error-log-aggregate/page.js:15:3)' +
|
||
'\n 2 | function logError() {' +
|
||
"\n 3 | const error1 = new Error('Error 1')" +
|
||
"\n > 4 | const error2 = new TypeError('Error 2')" +
|
||
'\n | ^' +
|
||
"\n 5 | const rootError = new Error('Root error')" +
|
||
'\n 6 | const aggregateError = new AggregateError(' +
|
||
'\n 7 | [error1, error2],' +
|
||
'\n ]' +
|
||
'\n}'
|
||
)
|
||
} else {
|
||
if (isTurbopack) {
|
||
// TODO(veil): Sourcemap names
|
||
// TODO(veil): relative paths in production
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain(
|
||
'(app/rsc-error-log-aggregate/page.js:6:26)'
|
||
)
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain('[errors]:')
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain('[cause]:')
|
||
} else {
|
||
// TODO(veil): line/column numbers are flaky in Webpack
|
||
}
|
||
}
|
||
})
|
||
|
||
it('stack frames are ignore-listed in ssr', async () => {
|
||
if (isNextDev) {
|
||
const outputIndex = next.cliOutput.length
|
||
const browser = await next.browser('/ssr-error-log-ignore-listed')
|
||
|
||
await retry(() => {
|
||
expect(next.cliOutput.slice(outputIndex)).toContain(
|
||
'Error: ssr-error-log-ignore-listed'
|
||
)
|
||
})
|
||
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
|
||
isTurbopack
|
||
? 'Error: ssr-error-log-ignore-listed' +
|
||
'\n at logError (app/ssr-error-log-ignore-listed/page.js:9:17)' +
|
||
'\n at runWithInternalIgnored (app/ssr-error-log-ignore-listed/page.js:19:13)' +
|
||
'\n at runWithExternalSourceMapped (app/ssr-error-log-ignore-listed/page.js:18:29)' +
|
||
'\n at runWithExternal (app/ssr-error-log-ignore-listed/page.js:17:32)' +
|
||
'\n at runWithInternalSourceMapped (app/ssr-error-log-ignore-listed/page.js:16:18)' +
|
||
// Realpath does not point into node_modules so we don't ignore it.
|
||
'\n at runInternalSourceMapped (internal-pkg/sourcemapped.ts:5:10)' +
|
||
'\n at runWithInternal (app/ssr-error-log-ignore-listed/page.js:15:28)' +
|
||
// Realpath does not point into node_modules so we don't ignore it.
|
||
'\n at runInternal (internal-pkg/index.js:2:10)' +
|
||
'\n at Page (app/ssr-error-log-ignore-listed/page.js:14:14)' +
|
||
'\n 7 |' +
|
||
'\n'
|
||
: 'Error: ssr-error-log-ignore-listed' +
|
||
'\n at logError (app/ssr-error-log-ignore-listed/page.js:9:17)' +
|
||
'\n at runWithInternalIgnored (app/ssr-error-log-ignore-listed/page.js:19:13)' +
|
||
// TODO(veil-NDX-910): Webpacks's sourcemap loader drops `ignoreList`
|
||
// TODO(veil): Webpack's sourcemap loader creates an incorrect `sources` entry.
|
||
// Can be worked around by using `./sourcemapped.ts` instead of `sourcemapped.ts`.
|
||
'\n at runInternalIgnored (webpack-internal:/(ssr)/internal-pkg/ignored.ts:6:10)' +
|
||
'\n at runWithExternalSourceMapped (app/ssr-error-log-ignore-listed/page.js:18:29)' +
|
||
'\n at runWithExternal (app/ssr-error-log-ignore-listed/page.js:17:32)' +
|
||
'\n at runWithInternalSourceMapped (app/ssr-error-log-ignore-listed/page.js:16:18)' +
|
||
// TODO(veil): Webpack's sourcemap loader creates an incorrect `sources` entry.
|
||
// Can be worked around by using `./sourcemapped.ts` instead of `sourcemapped.ts`.
|
||
// Realpath does not point into node_modules so we don't ignore it.
|
||
'\n at runInternalSourceMapped (webpack-internal:/(ssr)/internal-pkg/sourcemapped.ts:5:10)' +
|
||
'\n at runWithInternal (app/ssr-error-log-ignore-listed/page.js:15:28)' +
|
||
// Realpath does not point into node_modules so we don't ignore it.
|
||
'\n at runInternal (internal-pkg/index.js:2:10)' +
|
||
'\n at Page (app/ssr-error-log-ignore-listed/page.js:14:14)' +
|
||
'\n 7 |' +
|
||
'\n'
|
||
)
|
||
if (isTurbopack) {
|
||
// TODO(veil): Turbopack errors because it thinks the sources are not part of the project.
|
||
await expect(browser).toDisplayCollapsedRedbox(`
|
||
{
|
||
"description": "ssr-error-log-ignore-listed",
|
||
"environmentLabel": null,
|
||
"label": "Console Error",
|
||
"source": "app/ssr-error-log-ignore-listed/page.js (9:17) @ logError
|
||
> 9 | const error = new Error('ssr-error-log-ignore-listed')
|
||
| ^",
|
||
"stack": [
|
||
"logError app/ssr-error-log-ignore-listed/page.js (9:17)",
|
||
"runWithInternalIgnored app/ssr-error-log-ignore-listed/page.js (19:13)",
|
||
"runWithExternalSourceMapped app/ssr-error-log-ignore-listed/page.js (18:29)",
|
||
"runWithExternal app/ssr-error-log-ignore-listed/page.js (17:32)",
|
||
"runWithInternalSourceMapped app/ssr-error-log-ignore-listed/page.js (16:18)",
|
||
"runInternalSourceMapped internal-pkg/sourcemapped.ts (5:10)",
|
||
"runWithInternal app/ssr-error-log-ignore-listed/page.js (15:28)",
|
||
"runInternal internal-pkg/index.js (2:10)",
|
||
"Page app/ssr-error-log-ignore-listed/page.js (14:14)",
|
||
],
|
||
}
|
||
`)
|
||
} else {
|
||
// TODO(veil-NDX-910): Webpacks's sourcemap loader drops `ignoreList`
|
||
// TODO(veil): Webpack's sourcemap loader creates an incorrect `sources` entry.
|
||
await expect(browser).toDisplayCollapsedRedbox(`
|
||
{
|
||
"description": "ssr-error-log-ignore-listed",
|
||
"environmentLabel": null,
|
||
"label": "Console Error",
|
||
"source": "app/ssr-error-log-ignore-listed/page.js (9:17) @ logError
|
||
> 9 | const error = new Error('ssr-error-log-ignore-listed')
|
||
| ^",
|
||
"stack": [
|
||
"logError app/ssr-error-log-ignore-listed/page.js (9:17)",
|
||
"runWithInternalIgnored app/ssr-error-log-ignore-listed/page.js (19:13)",
|
||
"runInternalIgnored ignored.ts (6:10)",
|
||
"runWithExternalSourceMapped app/ssr-error-log-ignore-listed/page.js (18:29)",
|
||
"runWithExternal app/ssr-error-log-ignore-listed/page.js (17:32)",
|
||
"runWithInternalSourceMapped app/ssr-error-log-ignore-listed/page.js (16:18)",
|
||
"runInternalSourceMapped sourcemapped.ts (5:10)",
|
||
"runWithInternal app/ssr-error-log-ignore-listed/page.js (15:28)",
|
||
"runInternal internal-pkg/index.js (2:10)",
|
||
"Page app/ssr-error-log-ignore-listed/page.js (14:14)",
|
||
],
|
||
}
|
||
`)
|
||
}
|
||
} else {
|
||
if (isTurbopack) {
|
||
// TODO(veil): Sourcemap names
|
||
// TODO(veil): relative paths
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain(
|
||
'(app/ssr-error-log-ignore-listed/page.js:9:17)'
|
||
)
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain(
|
||
'\n' +
|
||
"> 9 | const error = new Error('ssr-error-log-ignore-listed')\n" +
|
||
' | ^\n'
|
||
)
|
||
} else {
|
||
// TODO(veil): line/column numbers are flaky in Webpack
|
||
}
|
||
}
|
||
})
|
||
|
||
it('stack frames are ignore-listed in rsc', async () => {
|
||
const outputIndex = next.cliOutput.length
|
||
await next.render('/rsc-error-log-ignore-listed')
|
||
|
||
if (isNextDev) {
|
||
await retry(() => {
|
||
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
|
||
'Error: rsc-error-log-ignore-listed'
|
||
)
|
||
})
|
||
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
|
||
isTurbopack
|
||
? 'Error: rsc-error-log-ignore-listed' +
|
||
'\n at logError (app/rsc-error-log-ignore-listed/page.js:8:17)' +
|
||
'\n at runWithInternalIgnored (app/rsc-error-log-ignore-listed/page.js:18:13)' +
|
||
'\n at runWithExternalSourceMapped (app/rsc-error-log-ignore-listed/page.js:17:29)' +
|
||
'\n at runWithExternal (app/rsc-error-log-ignore-listed/page.js:16:32)' +
|
||
'\n at runWithInternalSourceMapped (app/rsc-error-log-ignore-listed/page.js:15:18)' +
|
||
// Realpath does not point into node_modules so we don't ignore it.
|
||
'\n at runInternalSourceMapped (internal-pkg/sourcemapped.ts:5:10)' +
|
||
'\n at runWithInternal (app/rsc-error-log-ignore-listed/page.js:14:28)' +
|
||
// Realpath does not point into node_modules so we don't ignore it.
|
||
'\n at runInternal (internal-pkg/index.js:2:10)' +
|
||
'\n at Page (app/rsc-error-log-ignore-listed/page.js:13:14)' +
|
||
'\n 6 |' +
|
||
'\n'
|
||
: 'Error: rsc-error-log-ignore-listed' +
|
||
'\n at logError (app/rsc-error-log-ignore-listed/page.js:8:17)' +
|
||
'\n at runWithInternalIgnored (app/rsc-error-log-ignore-listed/page.js:18:13)' +
|
||
// TODO(veil): Webpacks's sourcemap loader drops `ignoreList`
|
||
// TODO(veil): Webpack's sourcemap loader creates an incorrect `sources` entry.
|
||
// Can be worked around by using `./sourcemapped.ts` instead of `sourcemapped.ts`.
|
||
'\n at runInternalIgnored (webpack-internal:/(rsc)/internal-pkg/ignored.ts:6:10)' +
|
||
'\n at runWithExternalSourceMapped (app/rsc-error-log-ignore-listed/page.js:17:29)' +
|
||
'\n at runWithExternal (app/rsc-error-log-ignore-listed/page.js:16:32)' +
|
||
'\n at runWithInternalSourceMapped (app/rsc-error-log-ignore-listed/page.js:15:18)' +
|
||
// TODO(veil): Webpack's sourcemap loader creates an incorrect `sources` entry.
|
||
// Can be worked around by using `./sourcemapped.ts` instead of `sourcemapped.ts`.
|
||
// Realpath does not point into node_modules so we don't ignore it.
|
||
'\n at runInternalSourceMapped (webpack-internal:/(rsc)/internal-pkg/sourcemapped.ts:5:10)' +
|
||
'\n at runWithInternal (app/rsc-error-log-ignore-listed/page.js:14:28)' +
|
||
// Realpath does not point into node_modules so we don't ignore it.
|
||
'\n at runInternal (internal-pkg/index.js:2:10)' +
|
||
'\n at Page (app/rsc-error-log-ignore-listed/page.js:13:14)' +
|
||
'\n 6 |' +
|
||
'\n'
|
||
)
|
||
} else {
|
||
if (isTurbopack) {
|
||
// TODO(veil): Sourcemap names
|
||
// TODO(veil): relative paths
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain(
|
||
'at <unknown> (app/rsc-error-log-ignore-listed/page.js:8:17)'
|
||
)
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain(
|
||
'' +
|
||
"\n> 8 | const error = new Error('rsc-error-log-ignore-listed')" +
|
||
'\n | ^'
|
||
)
|
||
} else {
|
||
// TODO(veil): line/column numbers are flaky in Webpack
|
||
}
|
||
}
|
||
})
|
||
|
||
it('thrown SSR errors', async () => {
|
||
if (isNextDev) {
|
||
const outputIndex = next.cliOutput.length
|
||
const browser = await next.browser('/ssr-throw')
|
||
|
||
await retry(() => {
|
||
expect(next.cliOutput.slice(outputIndex)).toContain('Error: ssr-throw')
|
||
})
|
||
|
||
const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
|
||
expect(cliOutput).toContain(
|
||
'⨯ Error: ssr-throw' +
|
||
'\n at throwError (app/ssr-throw/Thrower.js:4:9)' +
|
||
'\n at Thrower (app/ssr-throw/Thrower.js:8:3)' +
|
||
'\n 2 |' +
|
||
'\n 3 | function throwError() {' +
|
||
"\n> 4 | throw new Error('ssr-throw')" +
|
||
'\n | ^' +
|
||
'\n 5 | }' +
|
||
'\n 6 |' +
|
||
'\n 7 | export function Thrower() { {' +
|
||
"\n digest: '"
|
||
)
|
||
expect(cliOutput).toMatch(/digest: '\d+'/)
|
||
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "ssr-throw",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "app/ssr-throw/Thrower.js (4:9) @ throwError
|
||
> 4 | throw new Error('ssr-throw')
|
||
| ^",
|
||
"stack": [
|
||
"throwError app/ssr-throw/Thrower.js (4:9)",
|
||
"Thrower app/ssr-throw/Thrower.js (8:3)",
|
||
"Page app/ssr-throw/page.js (6:10)",
|
||
],
|
||
}
|
||
`)
|
||
} else {
|
||
// SSR errors are not logged because React retries them during hydration.
|
||
}
|
||
})
|
||
|
||
it('logged errors preserve their name', async () => {
|
||
let cliOutput = next.cliOutput
|
||
if (isNextDev) {
|
||
const outputIndex = next.cliOutput.length
|
||
await next.render('/rsc-error-log-custom-name')
|
||
cliOutput = next.cliOutput.slice(outputIndex)
|
||
}
|
||
|
||
await retry(() => {
|
||
expect(cliOutput).toContain(
|
||
// TODO: isNextDev ? 'UnnamedError: rsc-error-log-custom-name-Foo' : '[Error]: rsc-error-log-custom-name-Foo'
|
||
isNextDev
|
||
? 'Error: rsc-error-log-custom-name-Foo'
|
||
: 'Error: rsc-error-log-custom-name-Foo'
|
||
)
|
||
})
|
||
|
||
expect(cliOutput).toContain(
|
||
// TODO: isNextDev ? 'NamedError [MyError]: rsc-error-log-custom-name-Bar' : '[MyError]: rsc-error-log-custom-name-Bar'
|
||
isNextDev
|
||
? 'Error [MyError]: rsc-error-log-custom-name-Bar'
|
||
: 'Error [MyError]: rsc-error-log-custom-name-Bar'
|
||
)
|
||
})
|
||
|
||
it('handles invalid sourcemaps gracefully', async () => {
|
||
if (isNextDev) {
|
||
const outputIndex = next.cliOutput.length
|
||
await next.render('/bad-sourcemap')
|
||
|
||
await retry(() => {
|
||
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
|
||
'Error: bad-sourcemap'
|
||
)
|
||
})
|
||
if (isTurbopack) {
|
||
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
|
||
// Node.js is fine with invalid URLs in index maps apparently.
|
||
'' +
|
||
'Error: bad-sourcemap' +
|
||
'\n at logError (app/bad-sourcemap/custom:/[badhost]/app/bad-sourcemap/page.js:6:17)' +
|
||
'\n at Page (app/bad-sourcemap/custom:/[badhost]/app/bad-sourcemap/page.js:10:3)' +
|
||
'\n'
|
||
)
|
||
} else {
|
||
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
|
||
// Node.js is not fine with invalid URLs in vanilla source maps.
|
||
// Feel free to adjust these locations. They're just here to showcase
|
||
// sourcemapping is broken on invalid sources.
|
||
'' +
|
||
`\nwebpack-internal:///(rsc)/./app/bad-sourcemap/page.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: TypeError [ERR_INVALID_ARG_TYPE]: The "payload" argument must be of type object. Received null` +
|
||
'\nError: bad-sourcemap' +
|
||
'\n at logError (webpack-internal:///(rsc)/./app/bad-sourcemap/page.js:12:19)' +
|
||
'\n at Page (webpack-internal:///(rsc)/./app/bad-sourcemap/page.js:15:5)'
|
||
)
|
||
// Expect the invalid sourcemap warning only once per render.
|
||
// Dynamic I/O renders three times.
|
||
// One from filterStackFrameDEV.
|
||
// One from findSourceMapURLDEV.
|
||
expect(
|
||
normalizeCliOutput(next.cliOutput.slice(outputIndex)).split(
|
||
'Invalid source map.'
|
||
).length - 1
|
||
).toEqual(3)
|
||
}
|
||
} else {
|
||
// Bundlers silently drop invalid sourcemaps.
|
||
expect(
|
||
normalizeCliOutput(next.cliOutput).split('Invalid source map.').length -
|
||
1
|
||
).toEqual(0)
|
||
}
|
||
})
|
||
|
||
it('sourcemaps errors during module evaluation', async () => {
|
||
const outputIndex = next.cliOutput.length
|
||
const browser = await next.browser('/module-evaluation')
|
||
|
||
if (isNextDev) {
|
||
await retry(() => {
|
||
expect(next.cliOutput.slice(outputIndex)).toContain(
|
||
'Error: module-evaluation'
|
||
)
|
||
})
|
||
const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
|
||
if (isTurbopack) {
|
||
expect(cliOutput).toContain(
|
||
'Error: module-evaluation' +
|
||
// TODO(veil): Should map to no name like you'd get with native stacks without a bundler.
|
||
'\n at module evaluation (app/module-evaluation/module.js:1:22)' +
|
||
// TODO(veil): Added frames from bundler should be sourcemapped (https://linear.app/vercel/issue/NDX-509/)
|
||
'\n at module evaluation (app/module-evaluation/page.js:1:1)' +
|
||
'\n at module evaluation (.next'
|
||
)
|
||
} else {
|
||
expect(cliOutput).toContain(
|
||
'Error: module-evaluation' +
|
||
// TODO(veil): Should map to no name like you'd get with native stacks without a bundler.
|
||
// TODO(veil): Location should be sourcemapped
|
||
'\n at eval (app/module-evaluation/module.js:1:22)' +
|
||
// TODO(veil): Added frames from bundler should be sourcemapped (https://linear.app/vercel/issue/NDX-509/)
|
||
'\n at <unknown> (rsc)/.'
|
||
)
|
||
}
|
||
|
||
expect(cliOutput).toContain(
|
||
'' +
|
||
"\n> 1 | export const error = new Error('module-evaluation')" +
|
||
'\n | ^'
|
||
)
|
||
|
||
if (isTurbopack) {
|
||
await expect(browser).toDisplayCollapsedRedbox(`
|
||
{
|
||
"description": "module-evaluation",
|
||
"environmentLabel": "Prerender",
|
||
"label": "Console Error",
|
||
"source": "app/module-evaluation/module.js (1:22) @ module evaluation
|
||
> 1 | export const error = new Error('module-evaluation')
|
||
| ^",
|
||
"stack": [
|
||
"module evaluation app/module-evaluation/module.js (1:22)",
|
||
"module evaluation app/module-evaluation/page.js (1:1)",
|
||
"module evaluation app/module-evaluation/page.js (6:1)",
|
||
"Page <anonymous>",
|
||
],
|
||
}
|
||
`)
|
||
} else if (isRspack) {
|
||
await expect(browser).toDisplayCollapsedRedbox(`
|
||
{
|
||
"description": "module-evaluation",
|
||
"environmentLabel": "Prerender",
|
||
"label": "Console Error",
|
||
"source": "app/module-evaluation/module.js (1:22) @ eval
|
||
> 1 | export const error = new Error('module-evaluation')
|
||
| ^",
|
||
"stack": [
|
||
"eval app/module-evaluation/module.js (1:22)",
|
||
"<FIXME-file-protocol>",
|
||
"<FIXME-file-protocol>",
|
||
"eval about:/Prerender/webpack-internal:///(rsc)/app/module-evaluation/page.js (5:60)",
|
||
"<FIXME-file-protocol>",
|
||
"<FIXME-file-protocol>",
|
||
"Function.all <anonymous>",
|
||
"Function.all <anonymous>",
|
||
"Page <anonymous>",
|
||
],
|
||
}
|
||
`)
|
||
} else {
|
||
await expect(browser).toDisplayCollapsedRedbox(`
|
||
{
|
||
"description": "module-evaluation",
|
||
"environmentLabel": "Prerender",
|
||
"label": "Console Error",
|
||
"source": "app/module-evaluation/module.js (1:22) @ eval
|
||
> 1 | export const error = new Error('module-evaluation')
|
||
| ^",
|
||
"stack": [
|
||
"eval app/module-evaluation/module.js (1:22)",
|
||
"<FIXME-file-protocol>",
|
||
"eval about:/Prerender/webpack-internal:///(rsc)/app/module-evaluation/page.js (5:65)",
|
||
"<FIXME-file-protocol>",
|
||
"Page <anonymous>",
|
||
],
|
||
}
|
||
`)
|
||
}
|
||
} else {
|
||
if (isTurbopack) {
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain(
|
||
'' +
|
||
'\nError: module-evaluation' +
|
||
// TODO(veil): Turbopack internals. Feel free to update. Tracked in https://linear.app/vercel/issue/NEXT-4362
|
||
'\n at module evaluation (app/module-evaluation/module.js:1:22)'
|
||
)
|
||
expect(normalizeCliOutput(next.cliOutput)).toContain(
|
||
'' +
|
||
"\n> 1 | export const error = new Error('module-evaluation')" +
|
||
'\n | ^'
|
||
)
|
||
} else {
|
||
expect(
|
||
normalizeCliOutput(next.cliOutput).replaceAll(
|
||
/at \d+ /g,
|
||
'at <WebpackModuleID> '
|
||
)
|
||
).toContain(
|
||
'' +
|
||
'\nError: module-evaluation' +
|
||
// TODO(veil): column numbers are flaky in Webpack
|
||
'\n at <WebpackModuleID> (bundler:///app/module-evaluation/module.js:1:'
|
||
)
|
||
}
|
||
}
|
||
})
|
||
|
||
it('ignore-lists anonymous rsc stack frame sandwiches', async () => {
|
||
if (isNextDev) {
|
||
const outputIndex = next.cliOutput.length
|
||
const browser = await next.browser('/rsc-anonymous-stack-frame-sandwich')
|
||
|
||
// TODO(veil): Implement sandwich heuristic in `filterStackFrameDEV`
|
||
if (isTurbopack) {
|
||
await expect(browser).toDisplayCollapsedRedbox(`
|
||
[
|
||
{
|
||
"description": "rsc-anonymous-stack-frame-sandwich: external",
|
||
"environmentLabel": "Prerender",
|
||
"label": "Console Error",
|
||
"source": "app/rsc-anonymous-stack-frame-sandwich/page.js (5:29) @ Page
|
||
> 5 | runHiddenSetOfSetsExternal('rsc-anonymous-stack-frame-sandwich: external')
|
||
| ^",
|
||
"stack": [
|
||
"Set.forEach <anonymous>",
|
||
"Set.forEach <anonymous>",
|
||
"Page app/rsc-anonymous-stack-frame-sandwich/page.js (5:29)",
|
||
"Page <anonymous>",
|
||
],
|
||
},
|
||
{
|
||
"description": "rsc-anonymous-stack-frame-sandwich: internal",
|
||
"environmentLabel": "Prerender",
|
||
"label": "Console Error",
|
||
"source": "app/rsc-anonymous-stack-frame-sandwich/page.js (6:29) @ Page
|
||
> 6 | runHiddenSetOfSetsInternal('rsc-anonymous-stack-frame-sandwich: internal')
|
||
| ^",
|
||
"stack": [
|
||
"Set.forEach <anonymous>",
|
||
"Set.forEach <anonymous>",
|
||
"Page app/rsc-anonymous-stack-frame-sandwich/page.js (6:29)",
|
||
"Page <anonymous>",
|
||
],
|
||
},
|
||
]
|
||
`)
|
||
} else if (isRspack) {
|
||
// 2nd error from runHiddenSetOfSetsInternal hits https://linear.app/vercel/issue/NEXT-4412
|
||
await expect(browser).toDisplayCollapsedRedbox(`
|
||
[
|
||
{
|
||
"description": "rsc-anonymous-stack-frame-sandwich: external",
|
||
"environmentLabel": "Prerender",
|
||
"label": "Console Error",
|
||
"source": "app/rsc-anonymous-stack-frame-sandwich/page.js (5:29) @ Page
|
||
> 5 | runHiddenSetOfSetsExternal('rsc-anonymous-stack-frame-sandwich: external')
|
||
| ^",
|
||
"stack": [
|
||
"Set.forEach <anonymous>",
|
||
"Set.forEach <anonymous>",
|
||
"Page app/rsc-anonymous-stack-frame-sandwich/page.js (5:29)",
|
||
"Page <anonymous>",
|
||
],
|
||
},
|
||
{
|
||
"description": "rsc-anonymous-stack-frame-sandwich: internal",
|
||
"environmentLabel": "Prerender",
|
||
"label": "Console Error",
|
||
"source": "app/rsc-anonymous-stack-frame-sandwich/page.js (6:29) @ Page
|
||
> 6 | runHiddenSetOfSetsInternal('rsc-anonymous-stack-frame-sandwich: internal')
|
||
| ^",
|
||
"stack": [
|
||
"eval webpack-internal:/(ssr)/internal-pkg/ignored.ts (18:54)",
|
||
"eval webpack-internal:/(ssr)/internal-pkg/ignored.ts (12:7)",
|
||
"Set.forEach <anonymous>",
|
||
"eval webpack-internal:/(ssr)/internal-pkg/ignored.ts (11:9)",
|
||
"Set.forEach <anonymous>",
|
||
"runSetOfSets webpack-internal:/(ssr)/internal-pkg/ignored.ts (10:13)",
|
||
"runHiddenSetOfSets webpack-internal:/(ssr)/internal-pkg/ignored.ts (18:3)",
|
||
"Page app/rsc-anonymous-stack-frame-sandwich/page.js (6:29)",
|
||
"Page <anonymous>",
|
||
],
|
||
},
|
||
]
|
||
`)
|
||
} else {
|
||
// 2nd error from runHiddenSetOfSetsInternal hits https://linear.app/vercel/issue/NEXT-4412
|
||
await expect(browser).toDisplayCollapsedRedbox(`
|
||
[
|
||
{
|
||
"description": "rsc-anonymous-stack-frame-sandwich: external",
|
||
"environmentLabel": "Prerender",
|
||
"label": "Console Error",
|
||
"source": "app/rsc-anonymous-stack-frame-sandwich/page.js (5:29) @ Page
|
||
> 5 | runHiddenSetOfSetsExternal('rsc-anonymous-stack-frame-sandwich: external')
|
||
| ^",
|
||
"stack": [
|
||
"Set.forEach <anonymous>",
|
||
"Set.forEach <anonymous>",
|
||
"Page app/rsc-anonymous-stack-frame-sandwich/page.js (5:29)",
|
||
"Page <anonymous>",
|
||
],
|
||
},
|
||
{
|
||
"description": "rsc-anonymous-stack-frame-sandwich: internal",
|
||
"environmentLabel": "Prerender",
|
||
"label": "Console Error",
|
||
"source": "app/rsc-anonymous-stack-frame-sandwich/page.js (6:29) @ Page
|
||
> 6 | runHiddenSetOfSetsInternal('rsc-anonymous-stack-frame-sandwich: internal')
|
||
| ^",
|
||
"stack": [
|
||
"eval webpack-internal:/(rsc)/internal-pkg/ignored.ts (18:54)",
|
||
"eval webpack-internal:/(rsc)/internal-pkg/ignored.ts (12:7)",
|
||
"Set.forEach <anonymous>",
|
||
"eval webpack-internal:/(rsc)/internal-pkg/ignored.ts (11:9)",
|
||
"Set.forEach <anonymous>",
|
||
"runSetOfSets webpack-internal:/(rsc)/internal-pkg/ignored.ts (10:13)",
|
||
"runHiddenSetOfSets webpack-internal:/(rsc)/internal-pkg/ignored.ts (18:3)",
|
||
"Page app/rsc-anonymous-stack-frame-sandwich/page.js (6:29)",
|
||
"Page <anonymous>",
|
||
],
|
||
},
|
||
]
|
||
`)
|
||
}
|
||
|
||
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
|
||
'' +
|
||
'Error: rsc-anonymous-stack-frame-sandwich: external' +
|
||
'\n at Page (app/rsc-anonymous-stack-frame-sandwich/page.js:5:29)' +
|
||
'\n 3 |' +
|
||
'\n 4 | export default function Page() {' +
|
||
"\n> 5 | runHiddenSetOfSetsExternal('rsc-anonymous-stack-frame-sandwich: external')" +
|
||
'\n | ^'
|
||
)
|
||
// TODO: assert on 2nd error once that's bug free
|
||
} else {
|
||
// TODO(veil): assert on 1st error once cursor position is consistent
|
||
// TODO(veil): assert on 2nd error once that's bug free
|
||
}
|
||
})
|
||
|
||
it('ignore-lists anonymous ssr stack frame sandwiches', async () => {
|
||
if (isNextDev) {
|
||
const outputIndex = next.cliOutput.length
|
||
const browser = await next.browser('/ssr-anonymous-stack-frame-sandwich')
|
||
|
||
if (isTurbopack) {
|
||
await expect(browser).toDisplayCollapsedRedbox(`
|
||
[
|
||
{
|
||
"description": "ssr-anonymous-stack-frame-sandwich: external",
|
||
"environmentLabel": null,
|
||
"label": "Console Error",
|
||
"source": "app/ssr-anonymous-stack-frame-sandwich/page.js (6:29) @ Page
|
||
> 6 | runHiddenSetOfSetsExternal('ssr-anonymous-stack-frame-sandwich: external')
|
||
| ^",
|
||
"stack": [
|
||
"Page app/ssr-anonymous-stack-frame-sandwich/page.js (6:29)",
|
||
],
|
||
},
|
||
{
|
||
"description": "ignore-listed frames",
|
||
"environmentLabel": null,
|
||
"label": "Console Error",
|
||
"source": "app/ssr-anonymous-stack-frame-sandwich/page.js (7:29) @ Page
|
||
> 7 | runHiddenSetOfSetsInternal('ssr-anonymous-stack-frame-sandwich: internal')
|
||
| ^",
|
||
"stack": [
|
||
"<unknown> internal-pkg/sourcemapped.ts (18:43)",
|
||
"<unknown> internal-pkg/sourcemapped.ts (11:7)",
|
||
"Set.forEach <anonymous>",
|
||
"<unknown> internal-pkg/sourcemapped.ts (10:9)",
|
||
"Set.forEach <anonymous>",
|
||
"runSetOfSets internal-pkg/sourcemapped.ts (9:13)",
|
||
"runHiddenSetOfSets internal-pkg/sourcemapped.ts (17:3)",
|
||
"Page app/ssr-anonymous-stack-frame-sandwich/page.js (7:29)",
|
||
],
|
||
},
|
||
]
|
||
`)
|
||
} else {
|
||
// 2nd error from runHiddenSetOfSetsInternal hits https://linear.app/vercel/issue/NEXT-4412
|
||
await expect(browser).toDisplayCollapsedRedbox(`
|
||
[
|
||
{
|
||
"description": "ssr-anonymous-stack-frame-sandwich: external",
|
||
"environmentLabel": null,
|
||
"label": "Console Error",
|
||
"source": "app/ssr-anonymous-stack-frame-sandwich/page.js (6:29) @ Page
|
||
> 6 | runHiddenSetOfSetsExternal('ssr-anonymous-stack-frame-sandwich: external')
|
||
| ^",
|
||
"stack": [
|
||
"Page app/ssr-anonymous-stack-frame-sandwich/page.js (6:29)",
|
||
],
|
||
},
|
||
{
|
||
"description": "ignore-listed frames",
|
||
"environmentLabel": null,
|
||
"label": "Console Error",
|
||
"source": "app/ssr-anonymous-stack-frame-sandwich/page.js (7:29) @ Page
|
||
> 7 | runHiddenSetOfSetsInternal('ssr-anonymous-stack-frame-sandwich: internal')
|
||
| ^",
|
||
"stack": [
|
||
"eval sourcemapped.ts (18:43)",
|
||
"eval sourcemapped.ts (11:7)",
|
||
"Set.forEach <anonymous>",
|
||
"eval sourcemapped.ts (10:9)",
|
||
"Set.forEach <anonymous>",
|
||
"runSetOfSets sourcemapped.ts (9:13)",
|
||
"runHiddenSetOfSets sourcemapped.ts (17:3)",
|
||
"Page app/ssr-anonymous-stack-frame-sandwich/page.js (7:29)",
|
||
],
|
||
},
|
||
]
|
||
`)
|
||
}
|
||
|
||
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
|
||
'' +
|
||
'Error: ssr-anonymous-stack-frame-sandwich: external' +
|
||
'\n at Page (app/ssr-anonymous-stack-frame-sandwich/page.js:6:29)' +
|
||
'\n 4 |' +
|
||
'\n 5 | export default function Page() {' +
|
||
"\n> 6 | runHiddenSetOfSetsExternal('ssr-anonymous-stack-frame-sandwich: external')" +
|
||
'\n | ^'
|
||
)
|
||
// TODO(veil): assert on 2nd error once that's bug free
|
||
} else {
|
||
// TODO(veil): assert on 1st error once cursor position is consistent
|
||
// TODO(veil): assert on 2nd error once that's bug free
|
||
}
|
||
})
|
||
})
|