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:
parent
d5a188de06
commit
d45d0f96f5
2 changed files with 280 additions and 56 deletions
|
@ -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\nYou’re 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\nYou’re 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] +
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
)
|
Loading…
Reference in a new issue