diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 2ab1d7f9c8fb3..a8df7e7d8cec9 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -102,6 +102,11 @@ function ErrorHtml({ ) } +function createNotFoundLoaderTree(loaderTree: LoaderTree): LoaderTree { + // Align the segment with parallel-route-default in next-app-loader + return ['__DEFAULT__', {}, loaderTree[2]] +} + // Find the closest matched component in the loader tree for a given component type function findMatchedComponent( loaderTree: LoaderTree, @@ -614,7 +619,7 @@ export async function renderToHTMLOrFlight( firstItem?: boolean injectedCSS: Set injectedFontPreloadTags: Set - asNotFound?: boolean | 'force' + asNotFound?: boolean }): Promise<{ Component: React.ComponentType styles: React.ReactNode @@ -944,18 +949,15 @@ export async function renderToHTMLOrFlight( // If it's a not found route, and we don't have any matched parallel // routes, we try to render the not found component if it exists. - let isLeaf = - process.env.NODE_ENV === 'production' - ? !segment && !rootLayoutIncluded - : !parallelRouteMap.length && segment === '__DEFAULT__' // hit parallel-route-default - let notFoundComponent = {} if ( NotFound && - // For action not-found we force render the NotFound and stop checking the parallel routes. - (asNotFound === 'force' || - // For normal case where we should look up for not-found, keep checking the parallel routes. - (asNotFound && isLeaf)) + asNotFound && + // In development, it could hit the parallel-route-default not found, so we only need to check the segment. + // Or if there's no parallel routes means it reaches the end. + ((segment === '__DEFAULT__' && !parallelRouteMap.length) || + // For production build the original pathname is /_not-found, always render not-found component. + renderOpts.originalPathname === '/_not-found') ) { notFoundComponent = { children: ( @@ -1382,69 +1384,70 @@ export async function renderToHTMLOrFlight( * A new React Component that renders the provided React Component * using Flight which can then be rendered to HTML. */ - const ServerComponentsRenderer = createServerComponentRenderer<{ - asNotFound: boolean | 'force' - }>( - async (props) => { - // Create full component tree from root to leaf. - const injectedCSS = new Set() - const injectedFontPreloadTags = new Set() - - const { Component: ComponentTree, styles } = await createComponentTree({ - createSegmentPath: (child) => child, - loaderTree, - parentParams: {}, - firstItem: true, - injectedCSS, - injectedFontPreloadTags, - rootLayoutIncluded: false, - asNotFound: props.asNotFound, - }) + const createServerComponentsRenderer = (loaderTreeToRender: LoaderTree) => + createServerComponentRenderer<{ + asNotFound: boolean + }>( + async (props) => { + // Create full component tree from root to leaf. + const injectedCSS = new Set() + const injectedFontPreloadTags = new Set() - const createMetadata = (tree: LoaderTree, errorType?: 'not-found') => ( - // Adding key={requestId} to make metadata remount for each render - // @ts-expect-error allow to use async server component - - ) + const { Component: ComponentTree, styles } = + await createComponentTree({ + createSegmentPath: (child) => child, + loaderTree: loaderTreeToRender, + parentParams: {}, + firstItem: true, + injectedCSS, + injectedFontPreloadTags, + rootLayoutIncluded: false, + asNotFound: props.asNotFound, + }) - const initialTree = createFlightRouterStateFromLoaderTree( - loaderTree, - getDynamicParamFromSegment, - query - ) + const createMetadata = (errorType?: 'not-found') => ( + // Adding key={requestId} to make metadata remount for each render + // @ts-expect-error allow to use async server component + + ) - return ( - <> - {styles} - - - - - ) - }, - ComponentMod, - serverComponentsRenderOpts, - serverComponentsErrorHandler, - nonce - ) + const initialTree = createFlightRouterStateFromLoaderTree( + loaderTreeToRender, + getDynamicParamFromSegment, + query + ) + + return ( + <> + {styles} + + + + + ) + }, + ComponentMod, + serverComponentsRenderOpts, + serverComponentsErrorHandler, + nonce + ) const { HeadManagerContext } = require('../../shared/lib/head-manager-context') as typeof import('../../shared/lib/head-manager-context') @@ -1465,15 +1468,16 @@ export async function renderToHTMLOrFlight( }, async ({ asNotFound, + tree, }: { /** * This option is used to indicate that the page should be rendered as * if it was not found. When it's enabled, instead of rendering the * page component, it renders the not-found segment. * - * If it's 'force', we don't traverse the tree and directly render the NotFound. */ - asNotFound: boolean | 'force' + asNotFound: boolean + tree: LoaderTree }) => { const polyfills = buildManifest.polyfillFiles .filter( @@ -1487,6 +1491,7 @@ export async function renderToHTMLOrFlight( integrity: subresourceIntegrityManifest?.[polyfill], })) + const ServerComponentsRenderer = createServerComponentsRenderer(tree) const content = ( () const injectedFontPreloadTags = new Set() const [RootLayout, rootStyles] = await getRootLayout( - loaderTree, + tree, injectedCSS, injectedFontPreloadTags ) const [NotFound, notFoundStyles] = await getNotFound( - loaderTree, + tree, injectedCSS, pathname ) @@ -1690,7 +1695,7 @@ export async function renderToHTMLOrFlight( {/* @ts-expect-error allow to use async server component */} { - it('should allow navigation on not-found', async () => { - const browser = await next.browser('/trigger-404') - expect(await browser.elementByCss('#not-found-component').text()).toBe( - 'Not Found!' - ) - - expect( - await browser - .elementByCss('#to-result') - .click() - .waitForElementByCss('#result-page') - .text() - ).toBe('Result Page!') - }) - - it('should allow navigation on error', async () => { - const browser = await next.browser('/trigger-error') - expect(await browser.elementByCss('#error-component').text()).toBe( - 'Error Happened!' - ) - - expect( - await browser - .elementByCss('#to-result') - .click() - .waitForElementByCss('#result-page') - .text() - ).toBe('Result Page!') - }) - - it('should allow navigation to other routes on route that was initially not-found', async () => { - // Intentionally non-existent route. - const browser = await next.browser('/testabc') - expect(await browser.elementByCss('#not-found-component').text()).toBe( - 'Not Found!' - ) - - expect( - await browser - .elementByCss('#to-result') - .click() - .waitForElementByCss('#result-page') - .text() - ).toBe('Result Page!') - }) - - it('should allow navigation back to route that was initially not-found', async () => { - // Intentionally non-existent route. - const browser = await next.browser('/testabc') - expect(await browser.elementByCss('#not-found-component').text()).toBe( - 'Not Found!' - ) - +export function runTest({ next }) { + it('should allow navigation on not-found', async () => { + const browser = await next.browser('/trigger-404') + expect(await browser.elementByCss('#not-found-component').text()).toBe( + 'Not Found!' + ) + + expect( await browser .elementByCss('#to-result') .click() .waitForElementByCss('#result-page') - .back() - .waitForElementByCss('#not-found-component') - }) + .text() + ).toBe('Result Page!') + }) - it('should allow navigating to a page calling notfound', async () => { - const browser = await next.browser('/') + it('should allow navigation on error', async () => { + const browser = await next.browser('/trigger-error') + expect(await browser.elementByCss('#error-component').text()).toBe( + 'Error Happened!' + ) + expect( await browser - .elementByCss('#trigger-404-link') + .elementByCss('#to-result') + .click() + .waitForElementByCss('#result-page') + .text() + ).toBe('Result Page!') + }) + + it('should allow navigation to other routes on route that was initially not-found', async () => { + // Intentionally non-existent route. + const browser = await next.browser('/testabc') + expect(await browser.elementByCss('#not-found-component').text()).toBe( + 'Not Found!' + ) + + expect( + await browser + .elementByCss('#to-result') .click() - .waitForElementByCss('#not-found-component') + .waitForElementByCss('#result-page') + .text() + ).toBe('Result Page!') + }) - expect(await browser.elementByCss('#not-found-component').text()).toBe( - 'Not Found!' - ) + it('should allow navigation back to route that was initially not-found', async () => { + // Intentionally non-existent route. + const browser = await next.browser('/testabc') + expect(await browser.elementByCss('#not-found-component').text()).toBe( + 'Not Found!' + ) - await browser.back().waitForElementByCss('#homepage') + await browser + .elementByCss('#to-result') + .click() + .waitForElementByCss('#result-page') + .back() + .waitForElementByCss('#not-found-component') + }) - expect(await browser.elementByCss('#homepage').text()).toBe('Home') - }) + it('should allow navigating to a page calling notfound', async () => { + const browser = await next.browser('/') - it('should allow navigating to a non-existent page', async () => { - const browser = await next.browser('/') + await browser + .elementByCss('#trigger-404-link') + .click() + .waitForElementByCss('#not-found-component') - await browser - .elementByCss('#non-existent-link') - .click() - .waitForElementByCss('#not-found-component') + expect(await browser.elementByCss('#not-found-component').text()).toBe( + 'Not Found!' + ) + + await browser.back().waitForElementByCss('#homepage') + + expect(await browser.elementByCss('#homepage').text()).toBe('Home') + }) + + it('should allow navigating to a non-existent page', async () => { + const browser = await next.browser('/') + + await browser + .elementByCss('#non-existent-link') + .click() + .waitForElementByCss('#not-found-component') - expect(await browser.elementByCss('#not-found-component').text()).toBe( - 'Not Found!' - ) + expect(await browser.elementByCss('#not-found-component').text()).toBe( + 'Not Found!' + ) - await browser.back().waitForElementByCss('#homepage') + await browser.back().waitForElementByCss('#homepage') - expect(await browser.elementByCss('#homepage').text()).toBe('Home') - }) + expect(await browser.elementByCss('#homepage').text()).toBe('Home') + }) +} + +createNextDescribe( + 'app dir - not found navigation', + { + files: __dirname, + }, + ({ next }) => { + runTest({ next }) } ) diff --git a/test/e2e/app-dir/error-boundary-and-not-found-linking/error-navigation-override-node-env.test.ts b/test/e2e/app-dir/error-boundary-and-not-found-linking/error-navigation-override-node-env.test.ts new file mode 100644 index 0000000000000..b32c67d316aaa --- /dev/null +++ b/test/e2e/app-dir/error-boundary-and-not-found-linking/error-navigation-override-node-env.test.ts @@ -0,0 +1,13 @@ +import { createNextDescribe } from 'e2e-utils' +import { runTest } from './error-boundary-and-not-found-linking.test' + +createNextDescribe( + 'app dir - not found navigation - with overridden node env', + { + files: __dirname, + env: { NODE_ENV: 'test' }, + }, + ({ next }) => { + runTest({ next }) + } +) diff --git a/test/lib/next-modes/next-dev.ts b/test/lib/next-modes/next-dev.ts index b4ff43eae9901..c1aaddd64bf2a 100644 --- a/test/lib/next-modes/next-dev.ts +++ b/test/lib/next-modes/next-dev.ts @@ -45,7 +45,7 @@ export class NextDevInstance extends NextInstance { env: { ...process.env, ...this.env, - NODE_ENV: '' as any, + NODE_ENV: this.env.NODE_ENV || ('' as any), PORT: this.forcedPort || '0', __NEXT_TEST_MODE: 'e2e', __NEXT_TEST_WITH_DEVTOOL: '1', diff --git a/test/lib/next-modes/next-start.ts b/test/lib/next-modes/next-start.ts index a80abc0135adc..215851e4e3a91 100644 --- a/test/lib/next-modes/next-start.ts +++ b/test/lib/next-modes/next-start.ts @@ -48,7 +48,7 @@ export class NextStartInstance extends NextInstance { env: { ...process.env, ...this.env, - NODE_ENV: '' as any, + NODE_ENV: this.env.NODE_ENV || ('' as any), PORT: this.forcedPort || '0', __NEXT_TEST_MODE: 'e2e', },