mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
## What? This PR improves error reporting for webpack loaders by: 1. **Preserving loader paths in error messages**: Modified the webpack message formatter to rewrite "Module build failed" headers to preserve the loader path information while removing the verbose prefix. This makes errors more readable in both CLI output and error overlays. 2. **Better error wrapping for non-Error throws**: When loaders throw non-Error values (strings, objects, etc.), the error is now wrapped with loader path information in the stack trace, making it easier to identify which loader caused the error. 3. **Catching deferred loader errors**: Added handling for errors that occur after loader completion (e.g., unhandled Promise rejections, setTimeout throws) by: - Adding an `unhandledRejection` handler in the IPC layer - Delaying resolution in webpack-loaders transform to allow error handlers to fire before sending the 'end' message 4. **Conditional `(from ...)` annotation**: The `(from /path/to/loader.js)` annotation is only added when the loader path is **not already visible** in the error's stack trace. This avoids redundant noise for errors that already include the loader in their stack, while still providing the annotation for errors without loader context (string throws, errors without stacks, etc.). 5. **Hiding internal Next.js loader paths**: Internal Next.js loader paths (under `next/dist/`) are filtered from error messages since they're noise for users. Only user-authored loader paths are shown. 6. **Comprehensive test coverage**: Added e2e tests covering various error scenarios (Error throws, string throws, Promise rejections, setTimeout errors, no-stack errors, filesystem errors) with verification of both CLI output and error overlay display. ## Why? Webpack loader errors were previously difficult to debug because: - The verbose "Module build failed" prefix obscured the actual error message - Non-Error throws lost their context and loader information - Deferred errors (Promise rejections, async throws) were not properly surfaced - Error overlays didn't clearly show which loader caused the problem - The `(from ...)` annotation was always added, even when the loader path was already in the stack trace (addressed per review feedback) ## How? - **format-webpack-messages.ts**: Changed from filtering out loader headers to rewriting them, preserving the `(from ./loaders/...)` information. Internal Next.js loader paths are hidden. - **webpack-loaders.ts**: Added error wrapping for non-Error values, deferred resolution to catch async errors, and conditional `(from ...)` annotation that checks `err.stack` before appending. Resolves loader paths into an array, then checks if any path appears in the stack before deciding to annotate. - **ipc/index.ts**: Added `unhandledRejection` handler to surface Promise-related errors. - **Test loaders**: `error-loader.js` and `fs-error-loader.js` delegate to a `create-error.js` helper that creates errors and invokes the loader callback inside `setTimeout`. This ensures the loader file path is absent from the stack trace, exercising the conditional `(from ...)` annotation path. Other loaders (string-error, promise-error, timeout-error, no-stack-error) test different error scenarios directly. - **Test suite**: Comprehensive e2e tests verify CLI output and error overlay for each error type. Turbopack-specific tests cover promise rejections, timeout errors, no-stack errors, and filesystem errors. --------- Co-authored-by: Claude <[email protected]>
132 lines
5.0 KiB
TypeScript
132 lines
5.0 KiB
TypeScript
import { nextTestSetup } from 'e2e-utils'
|
|
import { retry, waitForRedbox, getRedboxSource } from 'next-test-utils'
|
|
import stripAnsi from 'strip-ansi'
|
|
|
|
describe('webpack-loader-errors', () => {
|
|
const { next, isNextDev, isTurbopack } = nextTestSetup({
|
|
files: __dirname,
|
|
skipDeployment: true,
|
|
skipStart: true,
|
|
})
|
|
|
|
if (!isNextDev) {
|
|
it('should skip in non-dev mode', () => {})
|
|
return
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
await next.start()
|
|
})
|
|
|
|
describe('CLI output', () => {
|
|
// Test string-error before error to ensure each error appears independently
|
|
// in the CLI output (webpack only shows errors[0] per compilation)
|
|
it('should show the loader path and error message when a loader throws a plain string', async () => {
|
|
await next.fetch('/string-error')
|
|
await retry(
|
|
async () => {
|
|
const output = stripAnsi(next.cliOutput)
|
|
expect(output).toContain('string-error.data')
|
|
expect(output).toContain(
|
|
'A string error thrown by string-error-loader'
|
|
)
|
|
expect(output).toMatch(/\(from .+loaders\/string-error-loader/)
|
|
},
|
|
// webpack compilation output appears asynchronously
|
|
30_000
|
|
)
|
|
})
|
|
|
|
it('should show the loader path and error message when a loader throws an Error', async () => {
|
|
await next.fetch('/error')
|
|
await retry(async () => {
|
|
const output = stripAnsi(next.cliOutput)
|
|
expect(output).toContain('error.data')
|
|
expect(output).toContain('An error thrown by error-loader')
|
|
expect(output).toMatch(/\(from .+loaders\/error-loader/)
|
|
}, 30_000)
|
|
})
|
|
|
|
// The following CLI tests are Turbopack-only because webpack's CLI output
|
|
// only logs errors[0] per compilation (see store.ts). When multiple pages
|
|
// have errors, only the first error (by module order) is shown. These
|
|
// error types still work correctly and are tested via the overlay tests.
|
|
if (isTurbopack) {
|
|
it('should surface an unhandled rejected Promise from a loader', async () => {
|
|
await next.fetch('/promise-error')
|
|
await retry(async () => {
|
|
const output = stripAnsi(next.cliOutput)
|
|
expect(output).toContain('An error thrown by promise-error-loader')
|
|
})
|
|
})
|
|
|
|
it('should surface a setTimeout error thrown after loader completion', async () => {
|
|
await next.fetch('/timeout-error')
|
|
await retry(async () => {
|
|
const output = stripAnsi(next.cliOutput)
|
|
expect(output).toContain('An error thrown by timeout-error-loader')
|
|
})
|
|
})
|
|
|
|
it('should show the loader path and error message when a loader throws an Error without stack', async () => {
|
|
await next.fetch('/no-stack-error')
|
|
await retry(async () => {
|
|
const output = stripAnsi(next.cliOutput)
|
|
expect(output).toContain(
|
|
'An error without stack from no-stack-error-loader'
|
|
)
|
|
expect(output).toMatch(/\(from .+loaders\/no-stack-error-loader/)
|
|
})
|
|
})
|
|
|
|
it('should show the loader path and error message when a loader throws a filesystem error', async () => {
|
|
await next.fetch('/fs-error')
|
|
await retry(async () => {
|
|
const output = stripAnsi(next.cliOutput)
|
|
expect(output).toContain('ENOENT')
|
|
expect(output).toMatch(/\(from .+loaders\/fs-error-loader/)
|
|
})
|
|
})
|
|
}
|
|
})
|
|
|
|
// Build errors accumulate globally and the overlay shows the first build
|
|
// error (no pagination for build errors). After the CLI tests compile all
|
|
// error routes, the overlay may show any accumulated error. So we only
|
|
// verify that a loader error with "(from ...)" is displayed, not which
|
|
// specific one. The CLI tests above validate each error type specifically.
|
|
describe('error overlay', () => {
|
|
it('should show error overlay with loader path when a loader throws a plain string', async () => {
|
|
const browser = await next.browser('/string-error')
|
|
await waitForRedbox(browser)
|
|
|
|
const source = await getRedboxSource(browser)
|
|
expect(source).toMatch(/\(from .+loaders\//)
|
|
})
|
|
|
|
it('should show error overlay with loader path when a loader throws an Error', async () => {
|
|
const browser = await next.browser('/error')
|
|
await waitForRedbox(browser)
|
|
|
|
const source = await getRedboxSource(browser)
|
|
expect(source).toMatch(/\(from .+loaders\//)
|
|
})
|
|
|
|
it('should show error overlay with loader path when a loader throws an Error without stack', async () => {
|
|
const browser = await next.browser('/no-stack-error')
|
|
await waitForRedbox(browser)
|
|
|
|
const source = await getRedboxSource(browser)
|
|
expect(source).toMatch(/\(from .+loaders\//)
|
|
})
|
|
|
|
it('should show error overlay with loader path when a loader throws a filesystem error', async () => {
|
|
const browser = await next.browser('/fs-error')
|
|
await waitForRedbox(browser)
|
|
|
|
const source = await getRedboxSource(browser)
|
|
expect(source).toMatch(/\(from .+loaders\//)
|
|
})
|
|
})
|
|
})
|