rsnext/test/development/acceptance-app/rsc-build-errors.test.ts

383 lines
12 KiB
TypeScript
Raw Normal View History

import { FileRef, nextTestSetup } from 'e2e-utils'
Improve client-only imported in external package error (#45484) Currently if an external package imports `'client-only'` it's difficult to figure out why the error occured. This change adds an explanation of which import caused the error. The errors in the images are from using `<style jsx>` in a Server Component. Before, runtime error on the server ![image](https://user-images.githubusercontent.com/25056922/216080617-9eebf99e-2899-4d7e-80b6-ef7ded0831ce.png) After, webpack error with added stack trace ![image](https://user-images.githubusercontent.com/25056922/216303311-05c2f3ca-2857-4fe7-b55c-caaccf5f46bb.png) Fixes NEXT-409 ## 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)
2023-02-07 20:56:03 +01:00
import { check } from 'next-test-utils'
import path from 'path'
import { sandbox } from 'development-sandbox'
import { outdent } from 'outdent'
describe('Error overlay - RSC build errors', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'rsc-build-errors')),
dependencies: {
react: 'latest',
'react-dom': 'latest',
},
skipStart: true,
})
it('should throw an error when getServerSideProps is used', async () => {
const { session, cleanup } = await sandbox(
next,
undefined,
'/client-with-errors/get-server-side-props'
)
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)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
'"getServerSideProps" is not supported in app/'
)
await cleanup()
})
it('should throw an error when metadata export is used in client components', async () => {
const { session, cleanup } = await sandbox(
next,
undefined,
'/client-with-errors/metadata-export'
)
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)
expect(await session.hasRedbox(true)).toBe(true)
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)
expect(await session.hasRedbox(false)).toBe(false)
// Add `generateMetadata` error
uncomment = content.replace(
'// export async function generateMetadata',
'export async function generateMetadata'
)
await session.patch(pageFile, uncomment)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
'You are attempting to export "generateMetadata" from a component marked with "use client", which is disallowed.'
)
await cleanup()
})
it('should throw an error when metadata exports are used together in server components', async () => {
const { session, cleanup } = await sandbox(
next,
undefined,
'/server-with-errors/metadata-export'
)
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)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
'"metadata" and "generateMetadata" cannot be exported at the same time, please keep one of them.'
)
await cleanup()
})
// TODO: investigate flakey test case
it.skip('should throw an error when getStaticProps is used', async () => {
const { session, cleanup } = await sandbox(
next,
undefined,
'/client-with-errors/get-static-props'
)
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)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
'"getStaticProps" is not supported in app/'
)
await cleanup()
})
it('should error when page component export is not valid', async () => {
const { session, cleanup } = await sandbox(
next,
undefined,
'/server-with-errors/page-export'
)
await next.patchFile(
'app/server-with-errors/page-export/page.js',
'export const a = 123'
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxDescription()).toInclude(
'The default export is not a React Component in page: "/server-with-errors/page-export"'
)
await cleanup()
})
it('should error when page component export is not valid on initial load', async () => {
const { session, cleanup } = await sandbox(
next,
undefined,
'/server-with-errors/page-export-initial-error'
)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxDescription()).toInclude(
'The default export is not a React Component in page: "/server-with-errors/page-export-initial-error"'
)
await cleanup()
})
it('should throw an error when "use client" is on the top level but after other expressions', async () => {
const { session, cleanup } = await sandbox(
next,
undefined,
'/swc/use-client'
)
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)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
'directive must be placed before other expressions'
)
await cleanup()
})
it('should throw an error when "Component" is imported in server components', async () => {
const { session, cleanup } = await sandbox(
next,
undefined,
'/server-with-errors/class-component'
)
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)
expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toInclude(
`Youre importing a class component. It only works in a Client Component`
)
await cleanup()
})
it('should allow to use and handle rsc poisoning client-only', async () => {
const { session, cleanup } = await sandbox(
next,
undefined,
'/server-with-errors/client-only-in-server'
)
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)
expect(await session.hasRedbox(true)).toBe(true)
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.`
)
await cleanup()
})
it('should allow to use and handle rsc poisoning server-only', async () => {
const { session, cleanup } = await sandbox(
next,
undefined,
'/client-with-errors/server-only-in-client'
)
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)
expect(await session.hasRedbox(true)).toBe(true)
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.`
)
await cleanup()
})
it('should error for invalid undefined module retuning from next dynamic', async () => {
const { session, cleanup } = await sandbox(
next,
undefined,
'/client-with-errors/dynamic'
)
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')
)
expect(await session.hasRedbox(true)).toBe(true)
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.`
)
await cleanup()
})
it('should throw an error when error file is a server component', async () => {
const { session, cleanup } = await sandbox(
next,
undefined,
'/server-with-errors/error-file'
)
// Remove "use client"
await session.patch(
'app/server-with-errors/error-file/error.js',
'export default function Error() {}'
)
expect(await session.hasRedbox(true)).toBe(true)
await check(() => session.getRedboxSource(), /must be a Client Component/)
expect(
next.normalizeTestDirContent(await session.getRedboxSource())
).toMatchInlineSnapshot(
next.normalizeSnapshot(`
"./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.
Learn more: https://nextjs.org/docs/getting-started/react-essentials#client-components
,-[TEST_DIR/app/server-with-errors/error-file/error.js:1:1]
1 | export default function Error() {}
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
\`----
Import path:
./app/server-with-errors/error-file/error.js"
`)
)
await cleanup()
})
it('should throw an error when error file is a server component with empty error file', async () => {
const { session, cleanup } = await sandbox(
next,
undefined,
'/server-with-errors/error-file'
)
// Empty file
await session.patch('app/server-with-errors/error-file/error.js', '')
expect(await session.hasRedbox(true)).toBe(true)
await check(() => session.getRedboxSource(), /must be a Client Component/)
// TODO: investigate flakey snapshot due to spacing below
// expect(next.normalizeTestDirContent(await session.getRedboxSource()))
// .toMatchInlineSnapshot(next.normalizeSnapshot(`
// "./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"
// `))
await cleanup()
})
Improve client-only imported in external package error (#45484) Currently if an external package imports `'client-only'` it's difficult to figure out why the error occured. This change adds an explanation of which import caused the error. The errors in the images are from using `<style jsx>` in a Server Component. Before, runtime error on the server ![image](https://user-images.githubusercontent.com/25056922/216080617-9eebf99e-2899-4d7e-80b6-ef7ded0831ce.png) After, webpack error with added stack trace ![image](https://user-images.githubusercontent.com/25056922/216303311-05c2f3ca-2857-4fe7-b55c-caaccf5f46bb.png) Fixes NEXT-409 ## 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)
2023-02-07 20:56:03 +01:00
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,
}
}
`
const { session, cleanup } = await sandbox(
next,
undefined,
'/metadata/mutate'
)
await session.patch(pagePath, content)
await check(
async () => ((await session.hasRedbox(true)) ? 'success' : 'fail'),
/success/
)
expect(await session.getRedboxDescription()).toContain(
'Cannot add property x, object is not extensible'
)
await cleanup()
})
})