Files
next.js/test/development/acceptance-app/rsc-build-errors.test.ts
Arian Tron 61f56f997c
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled
first commit
2026-03-10 19:37:31 +03:30

522 lines
18 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { FileRef, nextTestSetup } from 'e2e-utils'
import path from 'path'
import { createSandbox } from 'development-sandbox'
import { outdent } from 'outdent'
describe('Error overlay - RSC build errors', () => {
const { next, isTurbopack } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'rsc-build-errors')),
skipStart: true,
})
it('should throw an error when getServerSideProps is used', async () => {
await using sandbox = await createSandbox(
next,
undefined,
'/client-with-errors/get-server-side-props'
)
const { session } = sandbox
const pageFile = 'app/client-with-errors/get-server-side-props/page.js'
const content = await next.readFile(pageFile)
const uncomment = content.replace(
'// export function getServerSideProps',
'export function getServerSideProps'
)
await session.patch(pageFile, uncomment)
await session.waitForRedbox()
expect(await session.getRedboxSource()).toInclude(
'"getServerSideProps" is not supported in app/'
)
})
it('should throw an error when metadata export is used in client components', async () => {
await using sandbox = await createSandbox(
next,
undefined,
'/client-with-errors/metadata-export'
)
const { session } = sandbox
const pageFile = 'app/client-with-errors/metadata-export/page.js'
const content = await next.readFile(pageFile)
// Add `metadata` error
let uncomment = content.replace(
'// export const metadata',
'export const metadata'
)
await session.patch(pageFile, uncomment)
await session.waitForRedbox()
expect(await session.getRedboxSource()).toInclude(
'You are attempting to export "metadata" from a component marked with "use client", which is disallowed.'
)
// Restore file
await session.patch(pageFile, content)
await session.waitForNoRedbox()
// Add `generateMetadata` error
uncomment = content.replace(
'// export async function generateMetadata',
'export async function generateMetadata'
)
await session.patch(pageFile, uncomment)
await session.waitForRedbox()
expect(await session.getRedboxSource()).toInclude(
'You are attempting to export "generateMetadata" from a component marked with "use client", which is disallowed.'
)
// Fix the error again to test error overlay works with hmr rebuild
await session.patch(pageFile, content)
await session.waitForNoRedbox()
})
it('should throw an error when metadata exports are used together in server components', async () => {
await using sandbox = await createSandbox(
next,
undefined,
'/server-with-errors/metadata-export'
)
const { session } = sandbox
const pageFile = 'app/server-with-errors/metadata-export/page.js'
const content = await next.readFile(pageFile)
const uncomment = content.replace(
'// export async function generateMetadata',
'export async function generateMetadata'
)
await session.patch(pageFile, uncomment)
await session.waitForRedbox()
expect(await session.getRedboxSource()).toInclude(
'"metadata" and "generateMetadata" cannot be exported at the same time, please keep one of them.'
)
})
// TODO: investigate flakey test case
it.skip('should throw an error when getStaticProps is used', async () => {
await using sandbox = await createSandbox(
next,
undefined,
'/client-with-errors/get-static-props'
)
const { session } = sandbox
const pageFile = 'app/client-with-errors/get-static-props/page.js'
const content = await next.readFile(pageFile)
const uncomment = content.replace(
'// export function getStaticProps',
'export function getStaticProps'
)
await session.patch(pageFile, uncomment)
await next.patchFile(pageFile, content)
await session.waitForRedbox()
expect(await session.getRedboxSource()).toInclude(
'"getStaticProps" is not supported in app/'
)
})
it('should throw an error when "use client" is on the top level but after other expressions', async () => {
await using sandbox = await createSandbox(
next,
undefined,
'/swc/use-client'
)
const { session } = sandbox
const pageFile = 'app/swc/use-client/page.js'
const content = await next.readFile(pageFile)
const uncomment = content.replace("// 'use client'", "'use client'")
await next.patchFile(pageFile, uncomment)
await session.waitForRedbox()
expect(await session.getRedboxSource()).toInclude(
'directive must be placed before other expressions'
)
})
it('should throw an error when "Component" is imported in server components', async () => {
await using sandbox = await createSandbox(
next,
undefined,
'/server-with-errors/class-component'
)
const { session } = sandbox
const pageFile = 'app/server-with-errors/class-component/page.js'
const content = await next.readFile(pageFile)
const uncomment = content.replace(
"// import { Component } from 'react'",
"import { Component } from 'react'"
)
await session.patch(pageFile, uncomment)
await session.waitForRedbox()
expect(await session.getRedboxSource()).toInclude(
`Youre importing a class component. It only works in a Client Component`
)
})
it('should allow to use and handle rsc poisoning client-only', async () => {
await using sandbox = await createSandbox(
next,
undefined,
'/server-with-errors/client-only-in-server'
)
const { session } = sandbox
const file =
'app/server-with-errors/client-only-in-server/client-only-lib.js'
const content = await next.readFile(file)
const uncomment = content.replace(
"// import 'client-only'",
"import 'client-only'"
)
await next.patchFile(file, uncomment)
await session.waitForRedbox()
if (isTurbopack) {
// TODO: fix the issue ordering.
// turbopack emits the resolve issue first instead of the transform issue.
expect(await session.getRedboxSource()).toMatchInlineSnapshot(`
"./app/server-with-errors/client-only-in-server/client-only-lib.js (1:1)
You're importing a component that imports client-only. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
Learn more: https://nextjs.org/docs/app/building-your-application/rendering
> 1 | import 'client-only'
| ^^^^^^^^^^^^^^^^^^^^
2 |
3 | export default function ClientOnlyLib() {
4 | return 'client-only-lib'
Ecmascript file had an error
Import trace:
Server Component:
./app/server-with-errors/client-only-in-server/client-only-lib.js
./app/server-with-errors/client-only-in-server/page.js"
`)
} else {
expect(await session.getRedboxSource()).toInclude(
`You're importing a component that imports client-only. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.`
)
}
})
const invalidReactServerApis = [
'Component',
'createContext',
'createFactory',
'PureComponent',
'useDeferredValue',
'useEffect',
'useEffectEvent',
'useImperativeHandle',
'useInsertionEffect',
'useLayoutEffect',
'useReducer',
'useRef',
'useState',
'useSyncExternalStore',
'useTransition',
'useOptimistic',
'useActionState',
]
for (const api of invalidReactServerApis) {
it(`should error when ${api} from react is used in server component`, async () => {
await using sandbox = await createSandbox(
next,
undefined,
`/server-with-errors/react-apis/${api.toLowerCase()}`
)
const { session } = sandbox
await session.waitForRedbox()
expect(await session.getRedboxSource()).toInclude(
// `Component` has a custom error message
api === 'Component'
? `Youre 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.`
: `You're importing a component that needs \`${api}\`. This React Hook only works in a Client Component. To fix, mark the file (or its parent) with the \`"use client"\` directive.`
)
})
}
const invalidReactDomServerApis = [
'flushSync',
'unstable_batchedUpdates',
'useFormStatus',
'useFormState',
]
for (const api of invalidReactDomServerApis) {
it(`should error when ${api} from react-dom is used in server component`, async () => {
await using sandbox = await createSandbox(
next,
undefined,
`/server-with-errors/react-dom-apis/${api.toLowerCase()}`
)
const { session } = sandbox
await session.waitForRedbox()
expect(await session.getRedboxSource()).toInclude(
`You're importing a component that needs \`${api}\`. This React Hook only works in a Client Component. To fix, mark the file (or its parent) with the \`"use client"\` directive.`
)
})
}
it('should allow to use and handle rsc poisoning server-only', async () => {
await using sandbox = await createSandbox(
next,
undefined,
'/client-with-errors/server-only-in-client'
)
const { session } = sandbox
const file =
'app/client-with-errors/server-only-in-client/server-only-lib.js'
const content = await next.readFile(file)
const uncomment = content.replace(
"// import 'server-only'",
"import 'server-only'"
)
await session.patch(file, uncomment)
await session.waitForRedbox()
expect(await session.getRedboxSource()).toInclude(
`You're importing a component that needs "server-only". That only works in a Server Component but one of its parents is marked with "use client", so it's a Client Component.`
)
})
describe("importing 'next/cache' APIs in a client component", () => {
test.each(['revalidatePath', 'revalidateTag', 'cacheLife', 'cacheTag'])(
'%s is not allowed',
async (api) => {
await using sandbox = await createSandbox(
next,
undefined,
`/server-with-errors/next-cache-in-client/${api.toLowerCase()}`
)
const { session } = sandbox
await session.waitForRedbox()
expect(await session.getRedboxSource()).toInclude(
`You're importing a component that needs "${api}". That only works in a Server Component but one of its parents is marked with "use client", so it's a Client Component.`
)
}
)
test.each([
'unstable_cache', // useless in client, but doesn't technically error
'unstable_noStore', // no-op in client, but allowed for legacy reasons
])('%s is allowed', async (api) => {
await using sandbox = await createSandbox(
next,
undefined,
`/server-with-errors/next-cache-in-client/${api.toLowerCase()}`
)
const { session } = sandbox
await session.waitForNoRedbox()
})
})
describe('next/root-params', () => {
const isCacheComponentsEnabled =
process.env.__NEXT_CACHE_COMPONENTS === 'true'
it("importing 'next/root-params' when experimental.rootParams is not enabled", async () => {
await using sandbox = await createSandbox(
next,
undefined,
`/server-with-errors/next-root-params/without-flag`
)
const { session } = sandbox
await session.waitForRedbox()
if (!isCacheComponentsEnabled) {
expect(await session.getRedboxSource()).toInclude(
`'next/root-params' can only be imported when \`experimental.rootParams\` is enabled.`
)
} else {
// in cacheComponents we auto-enable 'next/root-params', so we should get an error about using a non-existent getter instead.
expect(await session.getRedboxSource()).toInclude(
isTurbopack
? `Export whatever doesn't exist in target module`
: `Attempted import error: 'whatever' is not exported from 'next/root-params' (imported as 'whatever').`
)
}
})
it("importing 'next/root-params' in a client component", async () => {
await using sandbox = await createSandbox(
next,
// if cacheComponents is not enabled, the import is guarded behind an experimental flag
isCacheComponentsEnabled
? new Map()
: new Map([
[
'next.config.js',
outdent`
module.exports = { experimental: { rootParams: true } }
`,
],
]),
`/server-with-errors/next-root-params/in-client`
)
const { session } = sandbox
await session.waitForRedbox()
expect(await session.getRedboxSource()).toInclude(
`You're importing a component that needs "next/root-params". That only works in a Server Component but one of its parents is marked with "use client", so it's a Client Component.`
)
})
it("importing 'next/root-params' in a client component in a way that bypasses import analysis", async () => {
await using sandbox = await createSandbox(
next,
// if cacheComponents is not enabled, the import is guarded behind an experimental flag
isCacheComponentsEnabled
? new Map()
: new Map([
[
'next.config.js',
outdent`
module.exports = { experimental: { rootParams: true } }
`,
],
]),
`/server-with-errors/next-root-params/in-client-await-import`
)
const { session } = sandbox
await session.waitForRedbox()
expect(await session.getRedboxSource()).toInclude(
`'next/root-params' cannot be imported from a Client Component module. It should only be used from a Server Component.`
)
})
})
it('should error for invalid undefined module retuning from next dynamic', async () => {
await using sandbox = await createSandbox(
next,
undefined,
'/client-with-errors/dynamic'
)
const { session } = sandbox
const file = 'app/client-with-errors/dynamic/page.js'
const content = await next.readFile(file)
await session.patch(
file,
content.replace('() => <p>hello dynamic world</p>', 'undefined')
)
await session.waitForRedbox()
expect(await session.getRedboxDescription()).toInclude(
`Element type is invalid. Received a promise that resolves to: undefined. Lazy element type must resolve to a class or function.`
)
})
it('should throw an error when error file is a server component', async () => {
await using sandbox = await createSandbox(
next,
undefined,
'/server-with-errors/error-file'
)
const { session } = sandbox
// Remove "use client"
await session.patch(
'app/server-with-errors/error-file/error.js',
'export default function Error() {}'
)
await session.waitForRedbox()
await expect(session.getRedboxSource()).resolves.toMatch(
/must be a Client \n| Component/
)
if (process.env.IS_TURBOPACK_TEST) {
expect(next.normalizeTestDirContent(await session.getRedboxSource()))
.toMatchInlineSnapshot(`
"./app/server-with-errors/error-file/error.js (1:1)
app/server-with-errors/error-file/error.js must be a Client Component. Add the "use client" directive the top of the file to resolve this issue.
Learn more: https://nextjs.org/docs/app/api-reference/directives/use-client
> 1 | export default function Error() {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Ecmascript file had an error"
`)
} else {
await expect(session.getRedboxSource()).resolves.toMatch(
/Add the "use client"/
)
// TODO: investigate flakey snapshot due to spacing below
// expect(next.normalizeTestDirContent(await session.getRedboxSource()))
// .toMatchInlineSnapshot(`
// "./app/server-with-errors/error-file/error.js
// Error: x TEST_DIR/app/server-with-errors/error-file/error.js must be a Client
// | Component. Add the "use client" directive the top of the file to resolve this issue.
// | Learn more: https://nextjs.org/docs/app/api-reference/directives/use-client
// |
// |
// ,----
// 1 | export default function Error() {}
// : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// \`----
// Import trace for requested module:
// ./app/server-with-errors/error-file/error.js"
// `)
}
})
it('should throw an error when error file is a server component with empty error file', async () => {
await using sandbox = await createSandbox(
next,
undefined,
'/server-with-errors/error-file'
)
const { session } = sandbox
// Empty file
await session.patch('app/server-with-errors/error-file/error.js', '')
await session.waitForRedbox()
await expect(session.getRedboxSource()).resolves.toMatch(
/Add the "use client"/
)
// TODO: investigate flakey snapshot due to spacing below
// expect(next.normalizeTestDirContent(await session.getRedboxSource()))
// .toMatchInlineSnapshot(n`
// "./app/server-with-errors/error-file/error.js
// ReactServerComponentsError:
// ./app/server-with-errors/error-file/error.js must be a Client Component. Add the "use client" directive the top of the file to resolve this issue.
// ,-[TEST_DIR/app/server-with-errors/error-file/error.js:1:1]
// 1 |
// : ^
// \`----
// Import path:
// ./app/server-with-errors/error-file/error.js"
// `)
})
it('should freeze parent resolved metadata to avoid mutating in generateMetadata', async () => {
const pagePath = 'app/metadata/mutate/page.js'
const content = outdent`
export default function page(props) {
return <p>mutate</p>
}
export async function generateMetadata(props, parent) {
const parentMetadata = await parent
parentMetadata.x = 1
return {
...parentMetadata,
}
}
`
await using sandbox = await createSandbox(
next,
undefined,
'/metadata/mutate'
)
const { session } = sandbox
await session.patch(pagePath, content)
await session.waitForRedbox()
expect(await session.getRedboxDescription()).toContain(
'Cannot add property x, object is not extensible'
)
})
})