Skip to content

Commit

Permalink
Improve error messages of server compilation (#41136)
Browse files Browse the repository at this point in the history
We used to format RSC errors (which are from the SWC loader) before outputting them to the CLI. This PR moves that to a better place in `WellknownErrorPlugin`.

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
  • Loading branch information
shuding committed Oct 3, 2022
1 parent 40639b2 commit 9baf651
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 36 deletions.
Expand Up @@ -11,7 +11,11 @@ export class WellKnownErrorsPlugin {
await Promise.all(
compilation.errors.map(async (err, i) => {
try {
const moduleError = await getModuleBuildError(compilation, err)
const moduleError = await getModuleBuildError(
compiler,
compilation,
err
)
if (moduleError !== false) {
compilation.errors[i] = moduleError
}
Expand Down
@@ -0,0 +1,90 @@
import type { webpack } from 'next/dist/compiled/webpack/webpack'

import { relative } from 'path'
import { SimpleWebpackError } from './simpleWebpackError'

export function formatRSCErrorMessage(
message: string
): null | [string, string] {
if (message && /NEXT_RSC_ERR_/.test(message)) {
let formattedMessage = message
let formattedVerboseMessage = ''

// Comes from the "React Server Components" transform in SWC, always
// attach the module trace.
const NEXT_RSC_ERR_REACT_API = /.+NEXT_RSC_ERR_REACT_API: (.*?)\n/s
const NEXT_RSC_ERR_SERVER_IMPORT = /.+NEXT_RSC_ERR_SERVER_IMPORT: (.*?)\n/s
const NEXT_RSC_ERR_CLIENT_IMPORT = /.+NEXT_RSC_ERR_CLIENT_IMPORT: (.*?)\n/s

if (NEXT_RSC_ERR_REACT_API.test(message)) {
formattedMessage = message.replace(
NEXT_RSC_ERR_REACT_API,
`\n\nYou're importing a component that needs $1. It only works in a Client Component but none of its parents are marked with "client", so they're Server Components by default.\n\n`
)
formattedVerboseMessage =
'\n\nMaybe one of these should be marked as a "client" entry:\n'
} else if (NEXT_RSC_ERR_SERVER_IMPORT.test(message)) {
formattedMessage = message.replace(
NEXT_RSC_ERR_SERVER_IMPORT,
`\n\nYou're importing a component that imports $1. It only works in a Client Component but none of its parents are marked with "client", so they're Server Components by default.\n\n`
)
formattedVerboseMessage =
'\n\nMaybe one of these should be marked as a "client" entry:\n'
} else if (NEXT_RSC_ERR_CLIENT_IMPORT.test(message)) {
formattedMessage = message.replace(
NEXT_RSC_ERR_CLIENT_IMPORT,
`\n\nYou're importing a component that needs $1. That only works in a Server Component but one of its parents is marked with "client", so it's a Client Component.\n\n`
)
formattedVerboseMessage =
'\n\nOne of these is marked as a "client" entry:\n'
}

return [formattedMessage, formattedVerboseMessage]
}

return null
}

// Check if the error is specifically related to React Server Components.
// If so, we'll format the error message to be more helpful.
export function getRscError(
fileName: string,
err: Error,
module: any,
compilation: webpack.Compilation,
compiler: webpack.Compiler
): SimpleWebpackError | false {
const formattedError = formatRSCErrorMessage(err.message)
if (!formattedError) return false

// Get the module trace:
// https://cs.github.com/webpack/webpack/blob/9fcaa243573005d6fdece9a3f8d89a0e8b399613/lib/stats/DefaultStatsFactoryPlugin.js#L414
const visitedModules = new Set()
const moduleTrace = []
let current = module
while (current) {
if (visitedModules.has(current)) break
visitedModules.add(current)
moduleTrace.push(current)
const origin = compilation.moduleGraph.getIssuer(current)
if (!origin) break
current = origin
}

const error = new SimpleWebpackError(
fileName,
formattedError[0] +
formattedError[1] +
moduleTrace
.map((m) =>
m.resource ? ' ' + relative(compiler.context, m.resource) : ''
)
.filter(Boolean)
.join('\n')
)

// Delete the stack because it's created here.
error.stack = ''

return error
}
Expand Up @@ -8,6 +8,7 @@ import { getScssError } from './parseScss'
import { getNotFoundError } from './parseNotFoundError'
import { SimpleWebpackError } from './simpleWebpackError'
import isError from '../../../../lib/is-error'
import { getRscError } from './parseRSC'

function getFileData(
compilation: webpack.Compilation,
Expand Down Expand Up @@ -42,6 +43,7 @@ function getFileData(
}

export async function getModuleBuildError(
compiler: webpack.Compiler,
compilation: webpack.Compilation,
input: any
): Promise<SimpleWebpackError | false> {
Expand Down Expand Up @@ -84,5 +86,16 @@ export async function getModuleBuildError(
return scss
}

const rsc = getRscError(
sourceFilename,
err,
input.module,
compilation,
compiler
)
if (rsc !== false) {
return rsc
}

return false
}
35 changes: 0 additions & 35 deletions packages/next/client/dev/error-overlay/format-webpack-messages.js
Expand Up @@ -183,41 +183,6 @@ function formatWebpackMessages(json, verbose) {
)
}

// TODO: Shall we use invisible characters in the original error
// message as meta information?
if (message && message.message && /NEXT_RSC_ERR_/.test(message.message)) {
// Comes from the "React Server Components" transform in SWC, always
// attach the module trace.
const NEXT_RSC_ERR_REACT_API = /.+NEXT_RSC_ERR_REACT_API: (.*?)\n/s
const NEXT_RSC_ERR_SERVER_IMPORT =
/.+NEXT_RSC_ERR_SERVER_IMPORT: (.*?)\n/s
const NEXT_RSC_ERR_CLIENT_IMPORT =
/.+NEXT_RSC_ERR_CLIENT_IMPORT: (.*?)\n/s

if (NEXT_RSC_ERR_REACT_API.test(message.message)) {
message.message = message.message.replace(
NEXT_RSC_ERR_REACT_API,
`\n\nYou're importing a component that needs $1. It only works in a Client Component but none of its parents are marked with "client", so they're Server Components by default.\n\n`
)
importTraceNote =
'\n\nMaybe one of these should be marked as a "client" entry:\n'
} else if (NEXT_RSC_ERR_SERVER_IMPORT.test(message.message)) {
message.message = message.message.replace(
NEXT_RSC_ERR_SERVER_IMPORT,
`\n\nYou're importing a component that imports $1. It only works in a Client Component but none of its parents are marked with "client", so they're Server Components by default.\n\n`
)
importTraceNote =
'\n\nMaybe one of these should be marked as a "client" entry:\n'
} else if (NEXT_RSC_ERR_CLIENT_IMPORT.test(message.message)) {
message.message = message.message.replace(
NEXT_RSC_ERR_CLIENT_IMPORT,
`\n\nYou're importing a component that needs $1. That only works in a Server Component but one of its parents is marked with "client", so it's a Client Component.\n\n`
)
importTraceNote = '\n\nOne of these is marked as a "client" entry:\n'
}

verbose = true
}
return formatMessage(message, verbose, importTraceNote)
})
const formattedWarnings = json.warnings.map(function (message) {
Expand Down

0 comments on commit 9baf651

Please sign in to comment.