Custom RSC compiler error message in pages/ (#44865)

Change the error message when the RSC compiler errors are caused by a
`/pages` page.
 
Fixes NEXT-371

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/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`
- [ ]
[e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
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`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm build && pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Hannes Bornö 2023-01-16 14:47:18 +01:00 committed by GitHub
parent d5a188de06
commit d45d0f96f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 280 additions and 56 deletions

View file

@ -3,83 +3,106 @@ import type { webpack } from 'next/dist/compiled/webpack/webpack'
import { relative } from 'path'
import { SimpleWebpackError } from './simpleWebpackError'
function formatRSCErrorMessage(message: string): null | [string, string] {
if (message && /NEXT_RSC_ERR_/.test(message)) {
let formattedMessage = message
let formattedVerboseMessage = ''
function formatRSCErrorMessage(
message: string,
isPagesDir: boolean
): [string, string] {
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
const NEXT_RSC_ERR_CLIENT_DIRECTIVE = /.+NEXT_RSC_ERR_CLIENT_DIRECTIVE\n/s
const NEXT_RSC_ERR_CLIENT_DIRECTIVE_PAREN =
/.+NEXT_RSC_ERR_CLIENT_DIRECTIVE_PAREN\n/s
const NEXT_RSC_ERR_INVALID_API = /.+NEXT_RSC_ERR_INVALID_API: (.*?)\n/s
// 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
const NEXT_RSC_ERR_CLIENT_DIRECTIVE = /.+NEXT_RSC_ERR_CLIENT_DIRECTIVE\n/s
const NEXT_RSC_ERR_CLIENT_DIRECTIVE_PAREN =
/.+NEXT_RSC_ERR_CLIENT_DIRECTIVE_PAREN\n/s
const NEXT_RSC_ERR_INVALID_API = /.+NEXT_RSC_ERR_INVALID_API: (.*?)\n/s
if (NEXT_RSC_ERR_REACT_API.test(message)) {
const matches = message.match(NEXT_RSC_ERR_REACT_API)
if (matches && matches[1] === 'Component') {
formattedMessage = `\n\nYoure importing a class component. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.\n\n`
} else {
if (NEXT_RSC_ERR_REACT_API.test(message)) {
const matches = message.match(NEXT_RSC_ERR_REACT_API)
if (matches && matches[1] === 'Component') {
formattedMessage = `\n\nYoure importing a class component. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.\n\n`
} else {
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 "use client", so they're Server Components by default.\n\n`
)
}
formattedVerboseMessage =
'\n\nMaybe one of these should be marked as a client entry with "use client":\n'
} else if (NEXT_RSC_ERR_SERVER_IMPORT.test(message)) {
const matches = message.match(NEXT_RSC_ERR_SERVER_IMPORT)
switch (matches && matches[1]) {
case 'react-dom/server':
// If importing "react-dom/server", we should show a different error.
formattedMessage = `\n\nYou're importing a component that imports react-dom/server. To fix it, render or return the content directly as a Server Component instead for perf and security.`
break
case 'next/router':
// If importing "next/router", we should tell them to use "next/navigation".
formattedMessage = `\n\nYou have a Server Component that imports next/router. Use next/navigation instead.`
break
default:
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 "use client", so they're Server Components by default.\n\n`
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 "use client", so they're Server Components by default.\n\n`
)
}
formattedVerboseMessage =
'\n\nMaybe one of these should be marked as a client entry with "use client":\n'
} else if (NEXT_RSC_ERR_SERVER_IMPORT.test(message)) {
const matches = message.match(NEXT_RSC_ERR_SERVER_IMPORT)
switch (matches && matches[1]) {
case 'react-dom/server':
// If importing "react-dom/server", we should show a different error.
formattedMessage = `\n\nYou're importing a component that imports react-dom/server. To fix it, render or return the content directly as a Server Component instead for perf and security.`
break
case 'next/router':
// If importing "next/router", we should tell them to use "next/navigation".
formattedMessage = `\n\nYou have a Server Component that imports next/router. Use next/navigation instead.`
break
default:
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 "use client", so they're Server Components by default.\n\n`
)
}
formattedVerboseMessage =
'\n\nMaybe one of these should be marked as a client entry "use client":\n'
} else if (NEXT_RSC_ERR_CLIENT_IMPORT.test(message)) {
}
formattedVerboseMessage =
'\n\nMaybe one of these should be marked as a client entry "use client":\n'
} else if (NEXT_RSC_ERR_CLIENT_IMPORT.test(message)) {
if (isPagesDir) {
formattedMessage = message.replace(
NEXT_RSC_ERR_CLIENT_IMPORT,
`\n\nYou're importing a component that needs $1. That only works in a Server Component which is not supported in the pages/ directory. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components\n\n`
)
formattedVerboseMessage = '\n\nImport trace for requested module:\n'
} else {
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 "use client", so it's a Client Component.\n\n`
)
formattedVerboseMessage =
'\n\nOne of these is marked as a client entry with "use client":\n'
} else if (NEXT_RSC_ERR_CLIENT_DIRECTIVE.test(message)) {
}
} else if (NEXT_RSC_ERR_CLIENT_DIRECTIVE.test(message)) {
if (isPagesDir) {
formattedMessage = message.replace(
NEXT_RSC_ERR_CLIENT_DIRECTIVE,
`\n\nYou have tried to use the "use client" directive which is not supported in the pages/ directory. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components\n\n`
)
formattedVerboseMessage = '\n\nImport trace for requested module:\n'
} else {
formattedMessage = message.replace(
NEXT_RSC_ERR_CLIENT_DIRECTIVE,
`\n\nThe "use client" directive must be placed before other expressions. Move it to the top of the file to resolve this issue.\n\n`
)
formattedVerboseMessage = '\n\nImport path:\n'
} else if (NEXT_RSC_ERR_CLIENT_DIRECTIVE_PAREN.test(message)) {
}
} else if (NEXT_RSC_ERR_CLIENT_DIRECTIVE_PAREN.test(message)) {
if (isPagesDir) {
formattedMessage = message.replace(
NEXT_RSC_ERR_CLIENT_DIRECTIVE_PAREN,
`\n\nYou have tried to use the "use client" directive which is not supported in the pages/ directory. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components\n\n`
)
formattedVerboseMessage = '\n\nImport trace for requested module:\n'
} else {
formattedMessage = message.replace(
NEXT_RSC_ERR_CLIENT_DIRECTIVE_PAREN,
`\n\n"use client" must be a directive, and placed before other expressions. Remove the parentheses and move it to the top of the file to resolve this issue.\n\n`
)
formattedVerboseMessage = '\n\nImport path:\n'
} else if (NEXT_RSC_ERR_INVALID_API.test(message)) {
formattedMessage = message.replace(
NEXT_RSC_ERR_INVALID_API,
`\n\n"$1" is not supported in app/. Read more: https://beta.nextjs.org/docs/data-fetching/fundamentals\n\n`
)
formattedVerboseMessage = '\n\nFile path:\n'
}
return [formattedMessage, formattedVerboseMessage]
} else if (NEXT_RSC_ERR_INVALID_API.test(message)) {
formattedMessage = message.replace(
NEXT_RSC_ERR_INVALID_API,
`\n\n"$1" is not supported in app/. Read more: https://beta.nextjs.org/docs/data-fetching/fundamentals\n\n`
)
formattedVerboseMessage = '\n\nFile path:\n'
}
return null
return [formattedMessage, formattedVerboseMessage]
}
// Check if the error is specifically related to React Server Components.
@ -91,8 +114,9 @@ export function getRscError(
compilation: webpack.Compilation,
compiler: webpack.Compiler
): SimpleWebpackError | false {
const formattedError = formatRSCErrorMessage(err.message)
if (!formattedError) return false
if (!err.message || !/NEXT_RSC_ERR_/.test(err.message)) {
return false
}
// Get the module trace:
// https://cs.github.com/webpack/webpack/blob/9fcaa243573005d6fdece9a3f8d89a0e8b399613/lib/stats/DefaultStatsFactoryPlugin.js#L414
@ -100,8 +124,12 @@ export function getRscError(
const moduleTrace = []
let current = module
let isPagesDir = false
while (current) {
if (visitedModules.has(current)) break
if (/[\\/]pages/.test(current.resource.replace(compiler.context, ''))) {
isPagesDir = true
}
visitedModules.add(current)
moduleTrace.push(current)
const origin = compilation.moduleGraph.getIssuer(current)
@ -109,6 +137,8 @@ export function getRscError(
current = origin
}
const formattedError = formatRSCErrorMessage(err.message, isPagesDir)
const error = new SimpleWebpackError(
fileName,
formattedError[0] +

View file

@ -0,0 +1,194 @@
/* eslint-env jest */
import { createNextDescribe } from 'e2e-utils'
import { check } from 'next-test-utils'
import { sandbox } from './helpers'
const initialFiles = new Map([
['next.config.js', 'module.exports = { experimental: { appDir: true } }'],
['app/_.js', ''], // app dir need to exists, otherwise the SWC RSC checks will not run
[
'pages/index.js',
`import Comp from '../components/Comp'
export default function Page() { return <Comp /> }`,
],
[
'components/Comp.js',
`export default function Comp() { return <p>Hello world</p> }`,
],
])
createNextDescribe(
'Error Overlay for server components compiler errors in pages',
{
files: {},
dependencies: {
react: 'latest',
'react-dom': 'latest',
},
skipStart: true,
},
({ next }) => {
test("importing 'next/headers' in pages", async () => {
const { session, cleanup } = await sandbox(next, initialFiles, false)
await session.patch(
'components/Comp.js',
`
import { cookies } from 'next/headers'
export default function Page() {
return <p>hello world</p>
}
`
)
expect(await session.hasRedbox(true)).toBe(true)
await check(
() => session.getRedboxSource(),
/That only works in a Server Component/
)
expect(await session.getRedboxSource()).toMatchInlineSnapshot(`
"./components/Comp.js
You're importing a component that needs next/headers. That only works in a Server Component which is not supported in the pages/ directory. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components
,-[1:1]
1 |
2 | import { cookies } from 'next/headers'
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 |
4 | export default function Page() {
5 | return <p>hello world</p>
\`----
Import trace for requested module:
components/Comp.js
pages/index.js"
`)
await cleanup()
})
test("importing 'server-only' in pages", async () => {
const { session, cleanup } = await sandbox(next, initialFiles, false)
await next.patchFile(
'components/Comp.js',
`
import 'server-only'
export default function Page() {
return 'hello world'
}
`
)
expect(await session.hasRedbox(true)).toBe(true)
await check(
() => session.getRedboxSource(),
/That only works in a Server Component/
)
expect(await session.getRedboxSource()).toMatchInlineSnapshot(`
"./components/Comp.js
You're importing a component that needs server-only. That only works in a Server Component which is not supported in the pages/ directory. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components
,-[1:1]
1 |
2 | import 'server-only'
: ^^^^^^^^^^^^^^^^^^^^
3 |
4 | export default function Page() {
5 | return 'hello world'
\`----
Import trace for requested module:
components/Comp.js
pages/index.js"
`)
await cleanup()
})
test('"use client" at the bottom of the page', async () => {
const { session, cleanup } = await sandbox(next, initialFiles, false)
await next.patchFile(
'components/Comp.js',
`
export default function Component() {
return null
}
'use client';
`
)
expect(await session.hasRedbox(true)).toBe(true)
await check(
() => session.getRedboxSource(),
/which is not supported in the pages/
)
expect(await session.getRedboxSource()).toMatchInlineSnapshot(`
"./components/Comp.js
You have tried to use the \\"use client\\" directive which is not supported in the pages/ directory. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components
,-[2:1]
2 | export default function Component() {
3 | return null
4 | }
5 | 'use client';
: ^^^^^^^^^^^^^
6 |
\`----
Import trace for requested module:
components/Comp.js
pages/index.js"
`)
await cleanup()
})
test('"use client" with parentheses', async () => {
const { session, cleanup } = await sandbox(next, initialFiles, false)
await next.patchFile(
'components/Comp.js',
`
;('use client')
export default function Component() {
return null
}
`
)
expect(await session.hasRedbox(true)).toBe(true)
await check(
() => session.getRedboxSource(),
/which is not supported in the pages/
)
expect(await session.getRedboxSource()).toMatchInlineSnapshot(`
"./components/Comp.js
You have tried to use the \\"use client\\" directive which is not supported in the pages/ directory. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components
,-[1:1]
1 |
2 | ;('use client')
: ^^^^^^^^^^^^^^
3 | export default function Component() {
4 | return null
5 | }
\`----
Import trace for requested module:
components/Comp.js
pages/index.js"
`)
await cleanup()
})
}
)