mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
Currently, importing a new module causes a complete eviction (`clear()`) and re-evaluation of server chunks, both in the Turbopack runtime's module cache and Node's `require.cache`. Here's what happens today: - A new module is imported into a chunk's graph - This changes its `availability_info`, which in dev for non-entry chunks is encoded into the chunk's filepath - `VersionedContentMap` works entirely on chunk paths. When we construct instructions to transition a chunk into its new state, we fail to find the prior state and fall back to `clear()` as described above. An ideal version of this is a refactor that implements `VersionedContentMap` on a per-module basis, not a per-chunk one. This PR doesn't do that, but achieves consistent module-level updates when a chunk's availability info changes. It does this by implementing chunk lists for server entry chunks the same way the client hmr implementation does: an entry chunk's version aggregates the versions of its dependent chunks (including dynamically imported ones), keyed by merger rather than path. Entry chunks do not encode availability info in their paths, so they are insulated from missing versions. Updates are applied in Node.js through the same shared merged-update machinery in the unified hmr runtime that the client already uses, plus a new `ChunkListUpdate` branch in the server hmr client to unwrap the merged updates. Details: - The merged-update wire format (`EcmascriptMergedUpdate` etc.) moves from turbopack-browser into shared `turbopack_ecmascript::chunk_list`, along with runtime-agnostic `ChunkListVersion` and `update_chunk_list` implementations. - `turbopack-nodejs` gains a chunk content merger mirroring the browser one. `EcmascriptBuildNodeEntryChunk`'s versioned content is now a chunk-list content over its sync and async chunks. It is version-only and never emitted; the entry chunk still inlines its loader calls. - The aggregate server hmr subscription now tracks only entry chunks. Shared chunks under `server/chunks/` ride the entry's `ChunkListUpdate` as module deltas, so their content-hash paths no longer matter. - The Node hmr client applies `ChunkListUpdate` by feeding each nested merged update through the shared apply path. Modules that appear in an "added" chunk but already exist in the module cache were moved by a chunk rename and are treated as modified. - On a successful partial apply, the hot reloader clears the manifest cache for updated chunks and notifies browsers to refetch RSC, without clearing `require.cache`. ### Test Plan Adds a series of e2e tests to catch the described manual test plan in both entry and dynamic chunks.
1692 lines
44 KiB
TypeScript
1692 lines
44 KiB
TypeScript
/* eslint-env jest */
|
||
import { createSandbox } from 'development-sandbox'
|
||
import { FileRef, nextTestSetup } from 'e2e-utils'
|
||
import { getToastErrorCount, retry } from 'next-test-utils'
|
||
import path from 'path'
|
||
import { outdent } from 'outdent'
|
||
|
||
describe('ReactRefreshLogBox app', () => {
|
||
const { next, isTurbopack, isRspack } = nextTestSetup({
|
||
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
|
||
skipStart: true,
|
||
patchFileDelay: 1000,
|
||
})
|
||
|
||
test('should strip whitespace correctly with newline', async () => {
|
||
await using sandbox = await createSandbox(next)
|
||
const { browser, session } = sandbox
|
||
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
export default function Page() {
|
||
return (
|
||
<>
|
||
<p>index page</p>
|
||
|
||
<a onClick={() => {
|
||
throw new Error('idk')
|
||
}}>
|
||
click me
|
||
</a>
|
||
</>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
await browser.elementByCss('a').click()
|
||
|
||
await expect(browser).toDisplayRedbox(
|
||
`"Expected Redbox but found no visible one."`
|
||
)
|
||
})
|
||
|
||
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137807
|
||
test('module init error not shown', async () => {
|
||
// Start here:
|
||
await using sandbox = await createSandbox(next)
|
||
const { browser, session } = sandbox
|
||
|
||
// We start here.
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
import * as React from 'react';
|
||
class ClassDefault extends React.Component {
|
||
render() {
|
||
return <h1>Default Export</h1>;
|
||
}
|
||
}
|
||
export default ClassDefault;
|
||
`
|
||
)
|
||
|
||
expect(
|
||
await session.evaluate(() => document.querySelector('h1').textContent)
|
||
).toBe('Default Export')
|
||
|
||
// Add a throw in module init phase:
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
// top offset for snapshot
|
||
import * as React from 'react';
|
||
throw new Error('no')
|
||
class ClassDefault extends React.Component {
|
||
render() {
|
||
return <h1>Default Export</h1>;
|
||
}
|
||
}
|
||
export default ClassDefault;
|
||
`
|
||
)
|
||
|
||
if (isTurbopack) {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "no",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "index.js (3:7) @ module evaluation
|
||
> 3 | throw new Error('no')
|
||
| ^",
|
||
"stack": [
|
||
"module evaluation index.js (3:7)",
|
||
"module evaluation app/page.js (2:1)",
|
||
],
|
||
}
|
||
`)
|
||
} else if (isRspack) {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "no",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "index.js (3:7) @ eval
|
||
> 3 | throw new Error('no')
|
||
| ^",
|
||
"stack": [
|
||
"eval index.js (3:7)",
|
||
"<FIXME-next-dist-dir>",
|
||
"<FIXME-next-dist-dir>",
|
||
"<FIXME-next-dist-dir>",
|
||
"eval ./app/page.js",
|
||
"<FIXME-next-dist-dir>",
|
||
"<FIXME-next-dist-dir>",
|
||
"<FIXME-next-dist-dir>",
|
||
],
|
||
}
|
||
`)
|
||
} else {
|
||
await expect(browser).toDisplayRedbox(`
|
||
[
|
||
{
|
||
"description": "no",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "index.js (3:7) @ eval
|
||
> 3 | throw new Error('no')
|
||
| ^",
|
||
"stack": [
|
||
"eval index.js (3:7)",
|
||
"<FIXME-next-dist-dir>",
|
||
"eval ./app/page.js",
|
||
"<FIXME-next-dist-dir>",
|
||
],
|
||
},
|
||
{
|
||
"description": "no",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "index.js (3:7) @ eval
|
||
> 3 | throw new Error('no')
|
||
| ^",
|
||
"stack": [
|
||
"eval index.js (3:7)",
|
||
"<FIXME-next-dist-dir>",
|
||
"eval ./app/page.js",
|
||
"<FIXME-next-dist-dir>",
|
||
],
|
||
},
|
||
]
|
||
`)
|
||
}
|
||
})
|
||
|
||
// https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554152127
|
||
test('boundaries', async () => {
|
||
await using sandbox = await createSandbox(next)
|
||
const { browser, session } = sandbox
|
||
|
||
await session.write(
|
||
'FunctionDefault.js',
|
||
outdent`
|
||
export default function FunctionDefault() {
|
||
return <h2>hello</h2>
|
||
}
|
||
`
|
||
)
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
import FunctionDefault from './FunctionDefault.js'
|
||
import * as React from 'react'
|
||
class ErrorBoundary extends React.Component {
|
||
constructor() {
|
||
super()
|
||
this.state = { hasError: false, error: null };
|
||
}
|
||
static getDerivedStateFromError(error) {
|
||
return {
|
||
hasError: true,
|
||
error
|
||
};
|
||
}
|
||
render() {
|
||
if (this.state.hasError) {
|
||
return this.props.fallback;
|
||
}
|
||
return this.props.children;
|
||
}
|
||
}
|
||
function App() {
|
||
return (
|
||
<ErrorBoundary fallback={<h2>error</h2>}>
|
||
<FunctionDefault />
|
||
</ErrorBoundary>
|
||
);
|
||
}
|
||
export default App;
|
||
`
|
||
)
|
||
|
||
expect(
|
||
await session.evaluate(() => document.querySelector('h2').textContent)
|
||
).toBe('hello')
|
||
|
||
await session.write(
|
||
'FunctionDefault.js',
|
||
`export default function FunctionDefault() { throw new Error('no'); }`
|
||
)
|
||
|
||
await expect(browser).toDisplayRedbox(
|
||
`"Expected Redbox but found no visible one."`
|
||
)
|
||
})
|
||
|
||
// TODO: investigate why this fails when running outside of the Next.js
|
||
// monorepo e.g. fails when using pnpm create next-app
|
||
// https://github.com/vercel/next.js/pull/23203
|
||
test.skip('internal package errors', async () => {
|
||
await using sandbox = await createSandbox(next)
|
||
const { browser, session } = sandbox
|
||
|
||
// Make a react build-time error.
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
export default function FunctionNamed() {
|
||
return <div>{{}}</div>
|
||
}
|
||
`
|
||
)
|
||
|
||
await expect(browser).toDisplayRedbox()
|
||
})
|
||
|
||
test('unterminated JSX', async () => {
|
||
await using sandbox = await createSandbox(next)
|
||
const { browser, session } = sandbox
|
||
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
export default () => {
|
||
return (
|
||
<div>
|
||
<p>lol</p>
|
||
</div>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
|
||
await session.waitForNoRedbox()
|
||
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
export default () => {
|
||
return (
|
||
<div>
|
||
<p>lol</p>
|
||
div
|
||
)
|
||
}
|
||
`
|
||
)
|
||
|
||
if (isTurbopack) {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "Unexpected token. Did you mean \`{'}'}\` or \`}\`?",
|
||
"environmentLabel": null,
|
||
"label": "Build Error",
|
||
"source": "./index.js (7:1)
|
||
Error: Unexpected token. Did you mean \`{'}'}\` or \`}\`?
|
||
> 7 | }
|
||
| ^",
|
||
"stack": [],
|
||
}
|
||
`)
|
||
} else if (isRspack) {
|
||
await expect({ browser, next }).toDisplayRedbox(`
|
||
{
|
||
"description": " ╰─▶ × Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`?",
|
||
"environmentLabel": null,
|
||
"label": "Build Error",
|
||
"source": "./index.js
|
||
╰─▶ × Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`?
|
||
│ ,-[7:1]
|
||
│ 4 | <p>lol</p>
|
||
│ 5 | div
|
||
│ 6 | )
|
||
│ 7 | }
|
||
│ : ^
|
||
│ \`----
|
||
│ x Expected '</', got '<eof>'
|
||
│ ,-[7:1]
|
||
│ 4 | <p>lol</p>
|
||
│ 5 | div
|
||
│ 6 | )
|
||
│ 7 | }
|
||
│ \`----
|
||
│
|
||
│
|
||
│ Caused by:
|
||
│ Syntax Error
|
||
Import trace for requested module:
|
||
./index.js
|
||
./app/page.js",
|
||
"stack": [],
|
||
}
|
||
`)
|
||
} else {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": " x Unexpected token. Did you mean \`{'}'}\` or \`}\`?",
|
||
"environmentLabel": null,
|
||
"label": "Build Error",
|
||
"source": "./index.js
|
||
Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`?
|
||
,-[7:1]
|
||
4 | <p>lol</p>
|
||
5 | div
|
||
6 | )
|
||
7 | }
|
||
: ^
|
||
\`----
|
||
x Expected '</', got '<eof>'
|
||
,-[7:1]
|
||
4 | <p>lol</p>
|
||
5 | div
|
||
6 | )
|
||
7 | }
|
||
\`----
|
||
Caused by:
|
||
Syntax Error
|
||
Import trace for requested module:
|
||
./index.js
|
||
./app/page.js",
|
||
"stack": [],
|
||
}
|
||
`)
|
||
}
|
||
})
|
||
|
||
// Module trace is only available with webpack 5
|
||
test('conversion to class component (1)', async () => {
|
||
await using sandbox = await createSandbox(next)
|
||
const { browser, session } = sandbox
|
||
|
||
await session.write(
|
||
'Child.js',
|
||
outdent`
|
||
export default function ClickCount() {
|
||
return <p>hello</p>
|
||
}
|
||
`
|
||
)
|
||
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
import Child from './Child';
|
||
|
||
export default function Home() {
|
||
return (
|
||
<div>
|
||
<Child />
|
||
</div>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
|
||
await session.waitForNoRedbox()
|
||
expect(
|
||
await session.evaluate(() => document.querySelector('p').textContent)
|
||
).toBe('hello')
|
||
|
||
await session.patch(
|
||
'Child.js',
|
||
outdent`
|
||
import { Component } from 'react';
|
||
export default class ClickCount extends Component {
|
||
render() {
|
||
throw new Error()
|
||
}
|
||
}
|
||
`
|
||
)
|
||
|
||
if (isTurbopack) {
|
||
// TODO(veil): Possibly https://linear.app/vercel/issue/NEXT-4411
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "Child.js (4:11) @ ClickCount.render
|
||
> 4 | throw new Error()
|
||
| ^",
|
||
"stack": [
|
||
"ClickCount.render Child.js (4:11)",
|
||
"Home index.js (6:7)",
|
||
"Page app/page.js (4:10)",
|
||
],
|
||
}
|
||
`)
|
||
} else {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "Child.js (4:11) @ ClickCount.render
|
||
> 4 | throw new Error()
|
||
| ^",
|
||
"stack": [
|
||
"ClickCount.render Child.js (4:11)",
|
||
"Home index.js (6:7)",
|
||
"Page app/page.js (4:10)",
|
||
],
|
||
}
|
||
`)
|
||
}
|
||
|
||
await session.patch(
|
||
'Child.js',
|
||
outdent`
|
||
import { Component } from 'react';
|
||
export default class ClickCount extends Component {
|
||
render() {
|
||
return <p>hello new</p>
|
||
}
|
||
}
|
||
`
|
||
)
|
||
|
||
await session.waitForNoRedbox()
|
||
expect(
|
||
await session.evaluate(() => document.querySelector('p').textContent)
|
||
).toBe('hello new')
|
||
})
|
||
|
||
test('css syntax errors', async () => {
|
||
await using sandbox = await createSandbox(next)
|
||
const { browser, session } = sandbox
|
||
|
||
await session.write('index.module.css', `.button {}`)
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
import './index.module.css';
|
||
export default () => {
|
||
return (
|
||
<div>
|
||
<p>lol</p>
|
||
</div>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
|
||
await session.waitForNoRedbox()
|
||
|
||
await session.patch('index.module.css', `.button`)
|
||
|
||
if (isTurbopack) {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "Parsing CSS source code failed",
|
||
"environmentLabel": null,
|
||
"label": "Build Error",
|
||
"source": "./index.module.css (1:8)
|
||
Error: Parsing CSS source code failed
|
||
> 1 | .button
|
||
| ^",
|
||
"stack": [],
|
||
}
|
||
`)
|
||
} else if (isRspack) {
|
||
await expect({ browser, next }).toDisplayRedbox(`
|
||
{
|
||
"description": " ╰─▶ × SyntaxError",
|
||
"environmentLabel": null,
|
||
"label": "Build Error",
|
||
"source": "./index.module.css
|
||
╰─▶ × SyntaxError
|
||
│
|
||
│ (1:1) <FIXME-project-root>/index.module.css Unknown word .button
|
||
│
|
||
│ > 1 | .button
|
||
│ | ^
|
||
│
|
||
Import trace for requested module:
|
||
./index.module.css
|
||
./index.js
|
||
./app/page.js",
|
||
"stack": [],
|
||
}
|
||
`)
|
||
} else {
|
||
await expect({ browser, next }).toDisplayRedbox(`
|
||
{
|
||
"description": "Syntax error: <FIXME-project-root>/index.module.css Unknown word .button",
|
||
"environmentLabel": null,
|
||
"label": "Build Error",
|
||
"source": "./index.module.css (1:1)
|
||
Syntax error: <FIXME-project-root>/index.module.css Unknown word .button
|
||
> 1 | .button
|
||
| ^",
|
||
"stack": [],
|
||
}
|
||
`)
|
||
}
|
||
|
||
// Checks for selectors that can't be prefixed.
|
||
// Selector "button" is not pure (pure selectors must contain at least one local class or id)
|
||
await session.patch('index.module.css', `button {}`)
|
||
|
||
if (isTurbopack) {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "Transforming CSS failed",
|
||
"environmentLabel": null,
|
||
"label": "Build Error",
|
||
"source": "./index.module.css
|
||
Error: Transforming CSS failed
|
||
Selector "button" is not pure. Pure selectors must contain at least one local class or id.
|
||
Import traces:
|
||
Client Component Browser:
|
||
./index.module.css [Client Component Browser]
|
||
./index.js [Client Component Browser]
|
||
./app/page.js [Client Component Browser]
|
||
./app/page.js [Server Component]
|
||
Client Component SSR:
|
||
./index.module.css [Client Component SSR]
|
||
./index.js [Client Component SSR]
|
||
./app/page.js [Client Component SSR]
|
||
./app/page.js [Server Component]",
|
||
"stack": [],
|
||
}
|
||
`)
|
||
} else if (isRspack) {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": " ╰─▶ × CssSyntaxError",
|
||
"environmentLabel": null,
|
||
"label": "Build Error",
|
||
"source": "./index.module.css
|
||
╰─▶ × CssSyntaxError
|
||
│
|
||
│ (1:1) Selector "button" is not pure (pure selectors must contain at least one local class or id)
|
||
│
|
||
│ > 1 | button {}
|
||
│ | ^
|
||
│
|
||
Import trace for requested module:
|
||
./index.module.css
|
||
./index.js
|
||
./app/page.js",
|
||
"stack": [],
|
||
}
|
||
`)
|
||
} else {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "Syntax error: Selector "button" is not pure (pure selectors must contain at least one local class or id)",
|
||
"environmentLabel": null,
|
||
"label": "Build Error",
|
||
"source": "./index.module.css (1:1)
|
||
Syntax error: Selector "button" is not pure (pure selectors must contain at least one local class or id)
|
||
> 1 | button {}
|
||
| ^",
|
||
"stack": [],
|
||
}
|
||
`)
|
||
}
|
||
})
|
||
|
||
it('logbox: anchors links in error messages', async () => {
|
||
await using sandbox = await createSandbox(next)
|
||
const { browser, session } = sandbox
|
||
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
import { useCallback } from 'react'
|
||
|
||
export default function Index() {
|
||
const boom = useCallback(() => {
|
||
throw new Error('end https://nextjs.org')
|
||
}, [])
|
||
return (
|
||
<main>
|
||
<button onClick={boom}>Boom!</button>
|
||
</main>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
|
||
await browser.elementByCss('button').click()
|
||
|
||
await expect(browser).toDisplayCollapsedRedbox(`
|
||
{
|
||
"description": "end https://nextjs.org",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "index.js (5:11) @ Index.useCallback[boom]
|
||
> 5 | throw new Error('end https://nextjs.org')
|
||
| ^",
|
||
"stack": [
|
||
"Index.useCallback[boom] index.js (5:11)",
|
||
"button <anonymous>",
|
||
"Index index.js (9:7)",
|
||
"Page app/page.js (4:10)",
|
||
],
|
||
}
|
||
`)
|
||
|
||
expect(
|
||
await session.evaluate(
|
||
() =>
|
||
document
|
||
.querySelector('body nextjs-portal')
|
||
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
|
||
.length
|
||
)
|
||
).toBe(1)
|
||
expect(
|
||
await session.evaluate(
|
||
() =>
|
||
(
|
||
document
|
||
.querySelector('body nextjs-portal')
|
||
.shadowRoot.querySelector(
|
||
'#nextjs__container_errors_desc a:nth-of-type(1)'
|
||
) as any
|
||
).href
|
||
)
|
||
).toMatchInlineSnapshot(`"https://nextjs.org/"`)
|
||
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
import { useCallback } from 'react'
|
||
|
||
export default function Index() {
|
||
const boom = useCallback(() => {
|
||
throw new Error('https://nextjs.org start')
|
||
}, [])
|
||
return (
|
||
<main>
|
||
<button onClick={boom}>Boom!</button>
|
||
</main>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
|
||
await browser.elementByCss('button').click()
|
||
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "https://nextjs.org start",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "index.js (5:11) @ Index.useCallback[boom]
|
||
> 5 | throw new Error('https://nextjs.org start')
|
||
| ^",
|
||
"stack": [
|
||
"Index.useCallback[boom] index.js (5:11)",
|
||
"button <anonymous>",
|
||
"Index index.js (9:7)",
|
||
"Page app/page.js (4:10)",
|
||
],
|
||
}
|
||
`)
|
||
expect(
|
||
await session.evaluate(
|
||
() =>
|
||
document
|
||
.querySelector('body nextjs-portal')
|
||
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
|
||
.length
|
||
)
|
||
).toBe(1)
|
||
expect(
|
||
await session.evaluate(
|
||
() =>
|
||
(
|
||
document
|
||
.querySelector('body nextjs-portal')
|
||
.shadowRoot.querySelector(
|
||
'#nextjs__container_errors_desc a:nth-of-type(1)'
|
||
) as any
|
||
).href
|
||
)
|
||
).toMatchInlineSnapshot(`"https://nextjs.org/"`)
|
||
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
import { useCallback } from 'react'
|
||
|
||
export default function Index() {
|
||
const boom = useCallback(() => {
|
||
throw new Error('middle https://nextjs.org end')
|
||
}, [])
|
||
return (
|
||
<main>
|
||
<button onClick={boom}>Boom!</button>
|
||
</main>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
|
||
await browser.elementByCss('button').click()
|
||
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "middle https://nextjs.org end",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "index.js (5:11) @ Index.useCallback[boom]
|
||
> 5 | throw new Error('middle https://nextjs.org end')
|
||
| ^",
|
||
"stack": [
|
||
"Index.useCallback[boom] index.js (5:11)",
|
||
"button <anonymous>",
|
||
"Index index.js (9:7)",
|
||
"Page app/page.js (4:10)",
|
||
],
|
||
}
|
||
`)
|
||
expect(
|
||
await session.evaluate(
|
||
() =>
|
||
document
|
||
.querySelector('body nextjs-portal')
|
||
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
|
||
.length
|
||
)
|
||
).toBe(1)
|
||
expect(
|
||
await session.evaluate(
|
||
() =>
|
||
(
|
||
document
|
||
.querySelector('body nextjs-portal')
|
||
.shadowRoot.querySelector(
|
||
'#nextjs__container_errors_desc a:nth-of-type(1)'
|
||
) as any
|
||
).href
|
||
)
|
||
).toMatchInlineSnapshot(`"https://nextjs.org/"`)
|
||
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
import { useCallback } from 'react'
|
||
|
||
export default function Index() {
|
||
const boom = useCallback(() => {
|
||
throw new Error('multiple https://nextjs.org links http://example.com')
|
||
}, [])
|
||
return (
|
||
<main>
|
||
<button onClick={boom}>Boom!</button>
|
||
</main>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
|
||
await browser.elementByCss('button').click()
|
||
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "multiple https://nextjs.org links http://example.com",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "index.js (5:11) @ Index.useCallback[boom]
|
||
> 5 | throw new Error('multiple https://nextjs.org links http://example.com')
|
||
| ^",
|
||
"stack": [
|
||
"Index.useCallback[boom] index.js (5:11)",
|
||
"button <anonymous>",
|
||
"Index index.js (9:7)",
|
||
"Page app/page.js (4:10)",
|
||
],
|
||
}
|
||
`)
|
||
expect(
|
||
await session.evaluate(
|
||
() =>
|
||
document
|
||
.querySelector('body nextjs-portal')
|
||
.shadowRoot.querySelectorAll('#nextjs__container_errors_desc a')
|
||
.length
|
||
)
|
||
).toBe(2)
|
||
expect(
|
||
await session.evaluate(
|
||
() =>
|
||
(
|
||
document
|
||
.querySelector('body nextjs-portal')
|
||
.shadowRoot.querySelector(
|
||
'#nextjs__container_errors_desc a:nth-of-type(1)'
|
||
) as any
|
||
).href
|
||
)
|
||
).toMatchInlineSnapshot(`"https://nextjs.org/"`)
|
||
expect(
|
||
await session.evaluate(
|
||
() =>
|
||
(
|
||
document
|
||
.querySelector('body nextjs-portal')
|
||
.shadowRoot.querySelector(
|
||
'#nextjs__container_errors_desc a:nth-of-type(2)'
|
||
) as any
|
||
).href
|
||
)
|
||
).toBe('http://example.com/')
|
||
})
|
||
|
||
// TODO-APP: Catch errors that happen before useEffect
|
||
test.skip('non-Error errors are handled properly', async () => {
|
||
await using sandbox = await createSandbox(next)
|
||
const { browser, session } = sandbox
|
||
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
export default () => {
|
||
throw {'a': 1, 'b': 'x'};
|
||
return (
|
||
<div>hello</div>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
|
||
await expect(browser).toDisplayRedbox()
|
||
|
||
// fix previous error
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
export default () => {
|
||
return (
|
||
<div>hello</div>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
await session.waitForNoRedbox()
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
class Hello {}
|
||
|
||
export default () => {
|
||
throw Hello
|
||
return (
|
||
<div>hello</div>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
|
||
await expect(browser).toDisplayRedbox()
|
||
|
||
// fix previous error
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
export default () => {
|
||
return (
|
||
<div>hello</div>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
await session.waitForNoRedbox()
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
export default () => {
|
||
throw "string error"
|
||
return (
|
||
<div>hello</div>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
|
||
await expect(browser).toDisplayRedbox()
|
||
|
||
// fix previous error
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
export default () => {
|
||
return (
|
||
<div>hello</div>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
await session.waitForNoRedbox()
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
export default () => {
|
||
throw null
|
||
return (
|
||
<div>hello</div>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
|
||
await expect(browser).toDisplayRedbox()
|
||
})
|
||
|
||
test('Should not show __webpack_exports__ when exporting anonymous arrow function', async () => {
|
||
await using sandbox = await createSandbox(next)
|
||
const { browser, session } = sandbox
|
||
|
||
await session.patch(
|
||
'index.js',
|
||
outdent`
|
||
export default () => {
|
||
if (typeof window !== 'undefined') {
|
||
throw new Error('test')
|
||
}
|
||
|
||
return null
|
||
}
|
||
`
|
||
)
|
||
|
||
if (isTurbopack) {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "test",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "index.js (3:11) @ {default export}
|
||
> 3 | throw new Error('test')
|
||
| ^",
|
||
"stack": [
|
||
"{default export} index.js (3:11)",
|
||
"Page app/page.js (4:10)",
|
||
],
|
||
}
|
||
`)
|
||
} else if (isRspack) {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "test",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "index.js (3:11) @ __rspack_default_export
|
||
> 3 | throw new Error('test')
|
||
| ^",
|
||
"stack": [
|
||
"__rspack_default_export index.js (3:11)",
|
||
"Page app/page.js (4:10)",
|
||
],
|
||
}
|
||
`)
|
||
} else {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "test",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "index.js (3:11) @ default
|
||
> 3 | throw new Error('test')
|
||
| ^",
|
||
"stack": [
|
||
"default index.js (3:11)",
|
||
"Page app/page.js (4:10)",
|
||
],
|
||
}
|
||
`)
|
||
}
|
||
})
|
||
|
||
test('Unhandled errors and rejections opens up in the minimized state', async () => {
|
||
await using sandbox = await createSandbox(next)
|
||
const { session, browser } = sandbox
|
||
|
||
const file = outdent`
|
||
export default function Index() {
|
||
//
|
||
setTimeout(() => {
|
||
throw new Error('Unhandled error')
|
||
}, 0)
|
||
setTimeout(() => {
|
||
Promise.reject(new Error('Undhandled rejection'))
|
||
}, 0)
|
||
return (
|
||
<>
|
||
<button
|
||
id="unhandled-error"
|
||
onClick={() => {
|
||
throw new Error('Unhandled error')
|
||
}}
|
||
>
|
||
Unhandled error
|
||
</button>
|
||
<button
|
||
id="unhandled-rejection"
|
||
onClick={() => {
|
||
Promise.reject(new Error('Undhandled rejection'))
|
||
}}
|
||
>
|
||
Unhandled rejection
|
||
</button>
|
||
</>
|
||
)
|
||
}
|
||
`
|
||
|
||
await session.patch('index.js', file)
|
||
|
||
// Unhandled error and rejection in setTimeout
|
||
await retry(async () => {
|
||
expect(await getToastErrorCount(browser)).toBe(2)
|
||
})
|
||
|
||
// Unhandled error in event handler
|
||
await browser.elementById('unhandled-error').click()
|
||
await retry(async () => {
|
||
expect(await getToastErrorCount(browser)).toBe(3)
|
||
})
|
||
|
||
// Unhandled rejection in event handler
|
||
await browser.elementById('unhandled-rejection').click()
|
||
await retry(async () => {
|
||
expect(await getToastErrorCount(browser)).toBe(4)
|
||
})
|
||
await session.waitForNoRedbox()
|
||
|
||
// Add Component error
|
||
await session.patch(
|
||
'index.js',
|
||
file.replace(
|
||
'//',
|
||
"if (typeof window !== 'undefined') throw new Error('Component error')"
|
||
)
|
||
)
|
||
|
||
// Render error should "win" and show up in fullscreen
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "Component error",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "index.js (2:44) @ Index
|
||
> 2 | if (typeof window !== 'undefined') throw new Error('Component error')
|
||
| ^",
|
||
"stack": [
|
||
"Index index.js (2:44)",
|
||
"Page app/page.js (4:10)",
|
||
],
|
||
}
|
||
`)
|
||
})
|
||
|
||
test('Call stack for client error', async () => {
|
||
await using sandbox = await createSandbox(
|
||
next,
|
||
new Map([
|
||
[
|
||
'app/page.js',
|
||
outdent`
|
||
'use client'
|
||
export default function Page() {
|
||
if (typeof window !== 'undefined') {
|
||
throw new Error('Client error')
|
||
}
|
||
return null
|
||
}
|
||
`,
|
||
],
|
||
])
|
||
)
|
||
const { browser } = sandbox
|
||
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "Client error",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "app/page.js (4:11) @ Page
|
||
> 4 | throw new Error('Client error')
|
||
| ^",
|
||
"stack": [
|
||
"Page app/page.js (4:11)",
|
||
],
|
||
}
|
||
`)
|
||
})
|
||
|
||
test('Call stack for server error', async () => {
|
||
await using sandbox = await createSandbox(
|
||
next,
|
||
new Map([
|
||
[
|
||
'app/page.js',
|
||
outdent`
|
||
export default function Page() {
|
||
throw new Error('Server error')
|
||
}
|
||
`,
|
||
],
|
||
])
|
||
)
|
||
const { browser } = sandbox
|
||
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "Server error",
|
||
"environmentLabel": "Server",
|
||
"label": "Runtime Error",
|
||
"source": "app/page.js (2:9) @ Page
|
||
> 2 | throw new Error('Server error')
|
||
| ^",
|
||
"stack": [
|
||
"Page app/page.js (2:9)",
|
||
],
|
||
}
|
||
`)
|
||
})
|
||
|
||
test('should hide unrelated frames in stack trace with unknown anonymous calls', async () => {
|
||
await using sandbox = await createSandbox(
|
||
next,
|
||
new Map([
|
||
[
|
||
'app/page.js',
|
||
outdent`
|
||
export default function Page() {
|
||
try {
|
||
(function() {
|
||
throw new Error("This is an error from an anonymous function");
|
||
})();
|
||
} catch (e) {
|
||
throw e
|
||
}
|
||
}
|
||
`,
|
||
],
|
||
])
|
||
)
|
||
const { browser } = sandbox
|
||
|
||
// TODO(veil): Turbopack uses correct name
|
||
// TODO(veil): Column of 2nd frame should be 7
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "This is an error from an anonymous function",
|
||
"environmentLabel": "Server",
|
||
"label": "Runtime Error",
|
||
"source": "app/page.js (4:13) @ ${isTurbopack ? '<anonymous>' : 'eval'}
|
||
> 4 | throw new Error("This is an error from an anonymous function");
|
||
| ^",
|
||
"stack": [
|
||
"${isTurbopack ? '<anonymous>' : 'eval'} app/page.js (4:13)",
|
||
"Page app/page.js (5:${isTurbopack ? '6' : '5'})",
|
||
],
|
||
}
|
||
`)
|
||
})
|
||
|
||
it('should hide unrelated frames in stack trace with nodejs internal calls', async () => {
|
||
await using sandbox = await createSandbox(
|
||
next,
|
||
new Map([
|
||
[
|
||
'app/page.js',
|
||
// Node.js will throw an error about the invalid URL since this is a server component
|
||
outdent`
|
||
export default function Page() {
|
||
new URL("/", "invalid");
|
||
}`,
|
||
],
|
||
])
|
||
)
|
||
const { browser } = sandbox
|
||
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "Invalid URL",
|
||
"environmentLabel": "Server",
|
||
"label": "Runtime TypeError",
|
||
"source": "app/page.js (2:3) @ Page
|
||
> 2 | new URL("/", "invalid");
|
||
| ^",
|
||
"stack": [
|
||
"Page app/page.js (2:3)",
|
||
],
|
||
}
|
||
`)
|
||
})
|
||
|
||
test('Server component errors should open up in fullscreen', async () => {
|
||
await using sandbox = await createSandbox(
|
||
next,
|
||
new Map([
|
||
// Start with error
|
||
[
|
||
'app/page.js',
|
||
outdent`
|
||
export default function Page() {
|
||
throw new Error('Server component error')
|
||
return <p id="text">Hello world</p>
|
||
}
|
||
`,
|
||
],
|
||
])
|
||
)
|
||
const { session, browser } = sandbox
|
||
|
||
await expect(browser).toDisplayRedbox(
|
||
`
|
||
{
|
||
"description": "Server component error",
|
||
"environmentLabel": "Server",
|
||
"label": "<FIXME-excluded-label>",
|
||
"source": "app/page.js (2:9) @ Page
|
||
> 2 | throw new Error('Server component error')
|
||
| ^",
|
||
"stack": [
|
||
"Page app/page.js (2:9)",
|
||
],
|
||
}
|
||
`,
|
||
|
||
// FIXME: `label` is flaking between "Runtime Error" and "Recoverable Error"
|
||
{ label: false }
|
||
)
|
||
|
||
// Remove error
|
||
await session.patch(
|
||
'app/page.js',
|
||
outdent`
|
||
export default function Page() {
|
||
return <p id="text">Hello world</p>
|
||
}
|
||
`
|
||
)
|
||
expect(await browser.waitForElementByCss('#text').text()).toBe(
|
||
'Hello world'
|
||
)
|
||
await session.waitForNoRedbox()
|
||
|
||
// Re-add error
|
||
await session.patch(
|
||
'app/page.js',
|
||
outdent`
|
||
export default function Page() {
|
||
throw new Error('Server component error!')
|
||
return <p id="text">Hello world</p>
|
||
}
|
||
`
|
||
)
|
||
|
||
await expect(browser).toDisplayRedbox(
|
||
`
|
||
{
|
||
"description": "Server component error!",
|
||
"environmentLabel": "Server",
|
||
"label": "<FIXME-excluded-label>",
|
||
"source": "app/page.js (2:9) @ Page
|
||
> 2 | throw new Error('Server component error!')
|
||
| ^",
|
||
"stack": [
|
||
"Page app/page.js (2:9)",
|
||
],
|
||
}
|
||
`,
|
||
|
||
// FIXME: `label` is flaking between "Runtime Error" and "Recoverable Error"
|
||
{ label: false }
|
||
)
|
||
})
|
||
|
||
test('Import trace when module not found in layout', async () => {
|
||
await using sandbox = await createSandbox(
|
||
next,
|
||
|
||
new Map([['app/module.js', `import "non-existing-module"`]])
|
||
)
|
||
const { browser, session } = sandbox
|
||
|
||
await session.patch(
|
||
'app/layout.js',
|
||
outdent`
|
||
import "./module"
|
||
|
||
export default function RootLayout({ children }) {
|
||
return (
|
||
<html>
|
||
<head></head>
|
||
<body>{children}</body>
|
||
</html>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
|
||
if (isTurbopack) {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "Module not found: Can't resolve 'non-existing-module'",
|
||
"environmentLabel": null,
|
||
"label": "Build Error",
|
||
"source": "./app/module.js (1:1)
|
||
Error: Module not found: Can't resolve 'non-existing-module'
|
||
> 1 | import "non-existing-module"
|
||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^",
|
||
"stack": [],
|
||
}
|
||
`)
|
||
} else if (isRspack) {
|
||
await expect({ browser, next }).toDisplayRedbox(`
|
||
{
|
||
"description": " × Module not found: Can't resolve 'non-existing-module' in '<FIXME-project-root>/app'",
|
||
"environmentLabel": null,
|
||
"label": "Build Error",
|
||
"source": "./app/module.js
|
||
× Module not found: Can't resolve 'non-existing-module' in '<FIXME-project-root>/app'
|
||
â•────
|
||
1 │ import "non-existing-module";
|
||
· ─────────────────────────────
|
||
╰────
|
||
Import trace for requested module:
|
||
./app/module.js
|
||
./app/layout.js",
|
||
"stack": [],
|
||
}
|
||
`)
|
||
} else {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "Module not found: Can't resolve 'non-existing-module'",
|
||
"environmentLabel": null,
|
||
"label": "Build Error",
|
||
"source": "./app/module.js (1:1)
|
||
Module not found: Can't resolve 'non-existing-module'
|
||
> 1 | import "non-existing-module"
|
||
| ^",
|
||
"stack": [],
|
||
}
|
||
`)
|
||
}
|
||
})
|
||
|
||
test("Can't resolve @import in CSS file", async () => {
|
||
await using sandbox = await createSandbox(
|
||
next,
|
||
new Map([
|
||
['app/styles1.css', '@import "./styles2.css"'],
|
||
['app/styles2.css', '@import "./boom.css"'],
|
||
])
|
||
)
|
||
const { browser, session } = sandbox
|
||
await session.patch(
|
||
'app/layout.js',
|
||
outdent`
|
||
import "./styles1.css"
|
||
|
||
export default function RootLayout({ children }) {
|
||
return (
|
||
<html>
|
||
<head></head>
|
||
<body>{children}</body>
|
||
</html>
|
||
)
|
||
}
|
||
`
|
||
)
|
||
|
||
// Wait for patch to apply and new error to show.
|
||
if (isTurbopack) {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "Module not found: Can't resolve './boom.css'",
|
||
"environmentLabel": null,
|
||
"label": "Build Error",
|
||
"source": "./app/styles2.css (1:1)
|
||
Error: Module not found: Can't resolve './boom.css'
|
||
> 1 | @import "./boom.css"
|
||
| ^",
|
||
"stack": [],
|
||
}
|
||
`)
|
||
} else if (isRspack) {
|
||
await expect({ browser, next }).toDisplayRedbox(`
|
||
{
|
||
"description": "Failed to compile",
|
||
"environmentLabel": null,
|
||
"label": "Build Error",
|
||
"source": "╰─▶ × Error: RspackResolver(NotFound("./boom.css"))",
|
||
"stack": [],
|
||
}
|
||
`)
|
||
} else {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "Module not found: Can't resolve './boom.css'",
|
||
"environmentLabel": null,
|
||
"label": "Build Error",
|
||
"source": "./app/styles2.css
|
||
Module not found: Can't resolve './boom.css'
|
||
https://nextjs.org/docs/messages/module-not-found
|
||
Import trace for requested module:
|
||
./app/styles1.css",
|
||
"stack": [],
|
||
}
|
||
`)
|
||
}
|
||
})
|
||
|
||
// TODO: The error overlay is not closed when restoring the working code.
|
||
for (const type of ['server' /* , 'client' */]) {
|
||
test(`${type} component can recover from error thrown in the module`, async () => {
|
||
await using sandbox = await createSandbox(next, undefined, '/' + type)
|
||
const { browser, session } = sandbox
|
||
|
||
await next.patchFile('index.js', "throw new Error('module error')")
|
||
|
||
await retry(async () => {
|
||
// Should use `await expect(browser).toDisplayRedbox()`
|
||
await session.waitForRedbox()
|
||
})
|
||
|
||
if (isRspack) {
|
||
await expect({ browser, next }).toDisplayRedbox(`
|
||
{
|
||
"code": "E394",
|
||
"description": "module error",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "index.js (1:7) @ eval
|
||
> 1 | throw new Error('module error')
|
||
| ^",
|
||
"stack": [
|
||
"eval index.js (1:7)",
|
||
"<FIXME-next-dist-dir>",
|
||
"<FIXME-next-dist-dir>",
|
||
"eval ./app/server/page.js",
|
||
"<FIXME-next-dist-dir>",
|
||
"<FIXME-next-dist-dir>",
|
||
],
|
||
}
|
||
`)
|
||
} else if (!isTurbopack) {
|
||
await expect({ browser, next }).toDisplayRedbox(`
|
||
{
|
||
"code": "E394",
|
||
"description": "module error",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "index.js (1:7) @ eval
|
||
> 1 | throw new Error('module error')
|
||
| ^",
|
||
"stack": [
|
||
"eval index.js (1:7)",
|
||
"<FIXME-next-dist-dir>",
|
||
"eval ./app/server/page.js",
|
||
"<FIXME-next-dist-dir>",
|
||
],
|
||
}
|
||
`)
|
||
}
|
||
|
||
await next.patchFile(
|
||
'index.js',
|
||
'export default function Page() {return <p>hello world</p>}'
|
||
)
|
||
await session.waitForNoRedbox()
|
||
})
|
||
}
|
||
|
||
test('Should show error location for server actions in client component', async () => {
|
||
await using sandbox = await createSandbox(
|
||
next,
|
||
new Map([
|
||
[
|
||
'app/actions.ts',
|
||
`"use server";
|
||
|
||
export async function serverAction(a) {
|
||
throw new Error("server action was here");
|
||
}`,
|
||
],
|
||
[
|
||
'app/page.js',
|
||
`"use client";
|
||
import { serverAction } from "./actions";
|
||
|
||
export default function Home() {
|
||
return (
|
||
<>
|
||
<form action={serverAction}>
|
||
<button id="trigger-action">Submit</button>
|
||
</form>
|
||
</>
|
||
);
|
||
}`,
|
||
],
|
||
])
|
||
)
|
||
const { browser } = sandbox
|
||
await browser.elementByCss('#trigger-action').click()
|
||
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "server action was here",
|
||
"environmentLabel": "Server",
|
||
"label": "Runtime Error",
|
||
"source": "app/actions.ts (4:9) @ serverAction
|
||
> 4 | throw new Error("server action was here");
|
||
| ^",
|
||
"stack": [
|
||
"serverAction app/actions.ts (4:9)",
|
||
"form <anonymous>",
|
||
"Home app/page.js (7:7)",
|
||
],
|
||
}
|
||
`)
|
||
})
|
||
|
||
test('Should show error location for server actions in server component', async () => {
|
||
await using sandbox = await createSandbox(
|
||
next,
|
||
new Map([
|
||
[
|
||
'app/actions.ts',
|
||
`"use server";
|
||
|
||
export async function serverAction(a) {
|
||
throw new Error("server action was here");
|
||
}`,
|
||
],
|
||
[
|
||
'app/page.js',
|
||
`import { serverAction } from "./actions";
|
||
|
||
export default function Home() {
|
||
return (
|
||
<>
|
||
<form action={serverAction}>
|
||
<button id="trigger-action">Submit</button>
|
||
</form>
|
||
</>
|
||
);
|
||
}`,
|
||
],
|
||
])
|
||
)
|
||
const { browser } = sandbox
|
||
await browser.elementByCss('#trigger-action').click()
|
||
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "server action was here",
|
||
"environmentLabel": "Server",
|
||
"label": "Runtime Error",
|
||
"source": "app/actions.ts (4:9) @ serverAction
|
||
> 4 | throw new Error("server action was here");
|
||
| ^",
|
||
"stack": [
|
||
"serverAction app/actions.ts (4:9)",
|
||
"form <anonymous>",
|
||
"Home app/page.js (6:7)",
|
||
],
|
||
}
|
||
`)
|
||
})
|
||
|
||
test('should collapse bundler internal stack frames', async () => {
|
||
await using sandbox = await createSandbox(
|
||
next,
|
||
new Map([
|
||
[
|
||
'app/utils.ts',
|
||
`throw new Error('utils error')
|
||
export function foo(){}
|
||
`,
|
||
],
|
||
[
|
||
'app/page.js',
|
||
`"use client";
|
||
import { foo } from "./utils";
|
||
|
||
export default function Home() {
|
||
foo();
|
||
return "hello";
|
||
}`,
|
||
],
|
||
])
|
||
)
|
||
|
||
const { browser } = sandbox
|
||
|
||
if (isTurbopack) {
|
||
// FIXME: display the sourcemapped stack frames
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "utils error",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "app/utils.ts (1:7) @ module evaluation
|
||
> 1 | throw new Error('utils error')
|
||
| ^",
|
||
"stack": [
|
||
"module evaluation app/utils.ts (1:7)",
|
||
"module evaluation app/page.js (2:1)",
|
||
],
|
||
}
|
||
`)
|
||
} else if (isRspack) {
|
||
await expect(browser).toDisplayRedbox(`
|
||
{
|
||
"description": "utils error",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "app/utils.ts (1:7) @ eval
|
||
> 1 | throw new Error('utils error')
|
||
| ^",
|
||
"stack": [
|
||
"eval app/utils.ts (1:7)",
|
||
"<FIXME-next-dist-dir>",
|
||
"<FIXME-next-dist-dir>",
|
||
"<FIXME-next-dist-dir>",
|
||
"eval ./app/page.js",
|
||
"<FIXME-next-dist-dir>",
|
||
"<FIXME-next-dist-dir>",
|
||
"<FIXME-next-dist-dir>",
|
||
],
|
||
}
|
||
`)
|
||
} else {
|
||
// FIXME: Webpack stack frames are not source mapped
|
||
await expect(browser).toDisplayRedbox(`
|
||
[
|
||
{
|
||
"description": "utils error",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "app/utils.ts (1:7) @ eval
|
||
> 1 | throw new Error('utils error')
|
||
| ^",
|
||
"stack": [
|
||
"eval app/utils.ts (1:7)",
|
||
"<FIXME-next-dist-dir>",
|
||
"eval ./app/page.js",
|
||
"<FIXME-next-dist-dir>",
|
||
],
|
||
},
|
||
{
|
||
"description": "utils error",
|
||
"environmentLabel": null,
|
||
"label": "Runtime Error",
|
||
"source": "app/utils.ts (1:7) @ eval
|
||
> 1 | throw new Error('utils error')
|
||
| ^",
|
||
"stack": [
|
||
"eval app/utils.ts (1:7)",
|
||
"<FIXME-next-dist-dir>",
|
||
"eval ./app/page.js",
|
||
"<FIXME-next-dist-dir>",
|
||
],
|
||
},
|
||
]
|
||
`)
|
||
}
|
||
})
|
||
})
|