diff --git a/packages/next/src/server/app-render/create-component-tree.tsx b/packages/next/src/server/app-render/create-component-tree.tsx index 3e6f0b6a746a80..19d5513b4215f3 100644 --- a/packages/next/src/server/app-render/create-component-tree.tsx +++ b/packages/next/src/server/app-render/create-component-tree.tsx @@ -51,6 +51,12 @@ export function createComponentTree(props: { ) } +function errorMissingDefaultExport(pagePath: string, convention: string) { + throw new Error( + `The default export is not a React Component in "${pagePath}/${convention}"` + ) +} + async function createComponentTreeInternal({ createSegmentPath, loaderTree: tree, @@ -311,30 +317,22 @@ async function createComponentTreeInternal({ (isPage || typeof Component !== 'undefined') && !isValidElementType(Component) ) { - throw new Error( - `The default export is not a React Component in page: "${pagePath}"` - ) + errorMissingDefaultExport(pagePath, 'page') } if ( typeof ErrorComponent !== 'undefined' && !isValidElementType(ErrorComponent) ) { - throw new Error( - `The default export of error is not a React Component in page: ${segment}` - ) + errorMissingDefaultExport(pagePath, 'error') } if (typeof Loading !== 'undefined' && !isValidElementType(Loading)) { - throw new Error( - `The default export of loading is not a React Component in ${segment}` - ) + errorMissingDefaultExport(pagePath, 'loading') } if (typeof NotFound !== 'undefined' && !isValidElementType(NotFound)) { - throw new Error( - `The default export of notFound is not a React Component in ${segment}` - ) + errorMissingDefaultExport(pagePath, 'not-found') } } diff --git a/test/development/acceptance-app/rsc-build-errors.test.ts b/test/development/acceptance-app/rsc-build-errors.test.ts index 3fb1c25c30daed..f16c63315bf8e1 100644 --- a/test/development/acceptance-app/rsc-build-errors.test.ts +++ b/test/development/acceptance-app/rsc-build-errors.test.ts @@ -124,41 +124,6 @@ describe('Error overlay - RSC build errors', () => { 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' - ) - - await session.assertHasRedbox() - 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' - ) - - await session.assertHasRedbox() - 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, diff --git a/test/development/acceptance-app/undefined-default-export.test.ts b/test/development/acceptance-app/undefined-default-export.test.ts new file mode 100644 index 00000000000000..9cc391a5cf7197 --- /dev/null +++ b/test/development/acceptance-app/undefined-default-export.test.ts @@ -0,0 +1,90 @@ +import path from 'path' +import { FileRef, nextTestSetup } from 'e2e-utils' +import { sandbox } from 'development-sandbox' + +describe('Undefined default export', () => { + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + }) + + it('should error if page component does not have default export', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([ + ['app/(group)/specific-path/server/page.js', 'export const a = 123'], + ]), + '/specific-path/server' + ) + + await session.assertHasRedbox() + expect(await session.getRedboxDescription()).toInclude( + 'The default export is not a React Component in "/specific-path/server/page"' + ) + + await cleanup() + }) + + it('should error if not-found component does not have default export when trigger not-found boundary', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/will-not-found/page.js', + ` + import { notFound } from 'next/navigation' + export default function Page() { notFound() } + `, + ], + ['app/will-not-found/not-found.js', 'export const a = 123'], + ]), + '/will-not-found' + ) + + await session.assertHasRedbox() + expect(await session.getRedboxDescription()).toInclude( + 'The default export is not a React Component in "/will-not-found/not-found"' + ) + + 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' + ) + + await session.assertHasRedbox() + expect(await session.getRedboxDescription()).toInclude( + 'The default export is not a React Component in "/server-with-errors/page-export/page"' + ) + + await cleanup() + }) + + it('should error when page component export is not valid on initial load', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/server-with-errors/page-export-initial-error/page.js', + 'export const a = 123', + ], + ]), + '/server-with-errors/page-export-initial-error' + ) + + await session.assertHasRedbox() + expect(await session.getRedboxDescription()).toInclude( + 'The default export is not a React Component in "/server-with-errors/page-export-initial-error/page"' + ) + + await cleanup() + }) +})