From 9baf651154187341c771a31f72feca9bc999df44 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Mon, 3 Oct 2022 22:19:25 +0200 Subject: [PATCH] Improve error messages of server compilation (#41136) 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) --- .../plugins/wellknown-errors-plugin/index.ts | 6 +- .../wellknown-errors-plugin/parseRSC.ts | 90 +++++++++++++++++++ .../webpackModuleError.ts | 13 +++ .../error-overlay/format-webpack-messages.js | 35 -------- 4 files changed, 108 insertions(+), 36 deletions(-) create mode 100644 packages/next/build/webpack/plugins/wellknown-errors-plugin/parseRSC.ts diff --git a/packages/next/build/webpack/plugins/wellknown-errors-plugin/index.ts b/packages/next/build/webpack/plugins/wellknown-errors-plugin/index.ts index 05557b1fb6f18..953ba57db0e23 100644 --- a/packages/next/build/webpack/plugins/wellknown-errors-plugin/index.ts +++ b/packages/next/build/webpack/plugins/wellknown-errors-plugin/index.ts @@ -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 } diff --git a/packages/next/build/webpack/plugins/wellknown-errors-plugin/parseRSC.ts b/packages/next/build/webpack/plugins/wellknown-errors-plugin/parseRSC.ts new file mode 100644 index 0000000000000..014707775c85d --- /dev/null +++ b/packages/next/build/webpack/plugins/wellknown-errors-plugin/parseRSC.ts @@ -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 +} diff --git a/packages/next/build/webpack/plugins/wellknown-errors-plugin/webpackModuleError.ts b/packages/next/build/webpack/plugins/wellknown-errors-plugin/webpackModuleError.ts index d53997d80abc0..45697e9eca6d2 100644 --- a/packages/next/build/webpack/plugins/wellknown-errors-plugin/webpackModuleError.ts +++ b/packages/next/build/webpack/plugins/wellknown-errors-plugin/webpackModuleError.ts @@ -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, @@ -42,6 +43,7 @@ function getFileData( } export async function getModuleBuildError( + compiler: webpack.Compiler, compilation: webpack.Compilation, input: any ): Promise { @@ -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 } diff --git a/packages/next/client/dev/error-overlay/format-webpack-messages.js b/packages/next/client/dev/error-overlay/format-webpack-messages.js index 1e5d73d7fff8a..b091084ae99e1 100644 --- a/packages/next/client/dev/error-overlay/format-webpack-messages.js +++ b/packages/next/client/dev/error-overlay/format-webpack-messages.js @@ -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) {