diff --git a/packages/next-swc/crates/next-core/src/app_structure.rs b/packages/next-swc/crates/next-core/src/app_structure.rs index b8ef9dbc860f4..026198a458eca 100644 --- a/packages/next-swc/crates/next-core/src/app_structure.rs +++ b/packages/next-swc/crates/next-core/src/app_structure.rs @@ -1035,54 +1035,38 @@ async fn directory_tree_to_entrypoints_internal_untraced( // Next.js has this logic in "collect-app-paths", where the root not-found page // is considered as its own entry point. - let not_found_tree = if components.not_found.is_some() { - LoaderTree { + let not_found_tree = LoaderTree { page: app_page.clone(), segment: directory_name.clone(), parallel_routes: indexmap! { "children".to_string() => LoaderTree { page: app_page.clone(), - segment: "__DEFAULT__".to_string(), - parallel_routes: IndexMap::new(), - components: Components { - default: Some(get_next_package(app_dir).join("dist/client/components/parallel-route-default.js".to_string())), - ..Default::default() - }.cell(), - global_metadata, - }.cell(), - }, - components: components.without_leafs().cell(), - global_metadata, - }.cell() - } else { - // Create default not-found page for production if there's no customized - // not-found - LoaderTree { - page: app_page.clone(), - segment: directory_name.to_string(), - parallel_routes: indexmap! { - "children".to_string() => LoaderTree { - page: app_page.clone(), - segment: "__PAGE__".to_string(), - parallel_routes: IndexMap::new(), - components: Components { - page: Some(get_next_package(app_dir).join("dist/client/components/not-found-error.js".to_string())), - ..Default::default() - }.cell(), + segment: "/_not-found".to_string(), + parallel_routes: indexmap! { + "children".to_string() => LoaderTree { + page: app_page.clone(), + segment: "__PAGE__".to_string(), + parallel_routes: IndexMap::new(), + components: Components { + page: components.not_found.or_else(|| Some(get_next_package(app_dir).join("dist/client/components/not-found-error.js".to_string()))), + ..Default::default() + }.cell(), + global_metadata + }.cell() + }, + components: Components::default().cell(), global_metadata, }.cell(), }, components: components.without_leafs().cell(), global_metadata, - }.cell() - }; + } + .cell(); { - let app_page = app_page.clone_push_str("not-found")?; - add_app_page(app_dir, &mut result, app_page, not_found_tree).await?; - } - { - let app_page = app_page.clone_push_str("_not-found")?; + let app_page = app_page + .clone_push_str("_not-found")? + .complete(PageType::Page)?; add_app_page(app_dir, &mut result, app_page, not_found_tree).await?; } } diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index 1d78ecf0e5598..62c6040c3cde9 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -25,7 +25,11 @@ import { } from '../lib/constants' import { isAPIRoute } from '../lib/is-api-route' import { isEdgeRuntime } from '../lib/is-edge-runtime' -import { APP_CLIENT_INTERNALS, RSC_MODULE_TYPES } from '../shared/lib/constants' +import { + APP_CLIENT_INTERNALS, + RSC_MODULE_TYPES, + UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, +} from '../shared/lib/constants' import { CLIENT_STATIC_FILES_RUNTIME_AMP, CLIENT_STATIC_FILES_RUNTIME_MAIN, @@ -243,7 +247,9 @@ export function createPagesMapping({ let pageKey = getPageFromPath(pagePath, pageExtensions) if (isAppRoute) { pageKey = pageKey.replace(/%5F/g, '_') - pageKey = pageKey.replace(/^\/not-found$/g, '/_not-found') + if (pageKey === '/not-found') { + pageKey = UNDERSCORE_NOT_FOUND_ROUTE_ENTRY + } } const normalizedPath = normalizePathSep( @@ -277,7 +283,8 @@ export function createPagesMapping({ // If there's any app pages existed, add a default not-found page. // If there's any custom not-found page existed, it will override the default one. ...(hasAppPages && { - '/_not-found': 'next/dist/client/components/not-found-error', + [UNDERSCORE_NOT_FOUND_ROUTE_ENTRY]: + 'next/dist/client/components/not-found-error', }), ...pages, } @@ -582,6 +589,7 @@ export async function createEntrypoints( : pagesType === PAGE_TYPES.APP ? posix.join('app', bundleFile) : bundleFile.slice(1) + const absolutePagePath = mappings[page] // Handle paths that have aliases diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index b0ab0f7a3fe87..c60e7bbdbab7e 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -72,6 +72,8 @@ import { MIDDLEWARE_REACT_LOADABLE_MANIFEST, SERVER_REFERENCE_MANIFEST, FUNCTIONS_CONFIG_MANIFEST, + UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, + UNDERSCORE_NOT_FOUND_ROUTE, } from '../shared/lib/constants' import { getSortedRoutes, isDynamicRoute } from '../shared/lib/router/utils' import type { __ApiPreviewProps } from '../server/api-utils' @@ -1034,7 +1036,7 @@ export default async function build( const conflictingPublicFiles: string[] = [] const hasPages404 = mappedPages['/404']?.startsWith(PAGES_DIR_ALIAS) - const hasApp404 = !!mappedAppPages?.['/_not-found'] + const hasApp404 = !!mappedAppPages?.[UNDERSCORE_NOT_FOUND_ROUTE_ENTRY] const hasCustomErrorPage = mappedPages['/_error'].startsWith(PAGES_DIR_ALIAS) @@ -2436,7 +2438,9 @@ export default async function build( !hasPages500 && !hasNonStaticErrorPage && !customAppGetInitialProps const combinedPages = [...staticPages, ...ssgPages] - const isApp404Static = appStaticPaths.has('/_not-found') + const isApp404Static = appStaticPaths.has( + UNDERSCORE_NOT_FOUND_ROUTE_ENTRY + ) const hasStaticApp404 = hasApp404 && isApp404Static // we need to trigger automatic exporting when we have @@ -2679,7 +2683,7 @@ export default async function build( routes.forEach((route) => { if (isDynamicRoute(page) && route === page) return - if (route === '/_not-found') return + if (route === UNDERSCORE_NOT_FOUND_ROUTE) return const { revalidate = appConfig.revalidate ?? false, diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 4cd2f5e290071..515224ab5d812 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -52,7 +52,10 @@ import { INSTRUMENTATION_HOOK_FILENAME, WEBPACK_LAYERS, } from '../lib/constants' -import { MODERN_BROWSERSLIST_TARGET } from '../shared/lib/constants' +import { + MODERN_BROWSERSLIST_TARGET, + UNDERSCORE_NOT_FOUND_ROUTE, +} from '../shared/lib/constants' import prettyBytes from '../lib/pretty-bytes' import { getRouteRegex } from '../shared/lib/router/utils/route-regex' import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher' @@ -688,7 +691,10 @@ export async function printTreeView( }) // If there's no app /_notFound page present, then the 404 is still using the pages/404 - if (!lists.pages.includes('/404') && !lists.app?.includes('/_not-found')) { + if ( + !lists.pages.includes('/404') && + !lists.app?.includes(UNDERSCORE_NOT_FOUND_ROUTE) + ) { lists.pages = [...lists.pages, '/404'] } diff --git a/packages/next/src/build/webpack/loaders/next-app-loader.ts b/packages/next/src/build/webpack/loaders/next-app-loader.ts index bfddc65f066b2..bc6b6deae5ca1 100644 --- a/packages/next/src/build/webpack/loaders/next-app-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-app-loader.ts @@ -1,5 +1,9 @@ import type webpack from 'next/dist/compiled/webpack/webpack' -import type { ValueOf } from '../../../shared/lib/constants' +import { + UNDERSCORE_NOT_FOUND_ROUTE, + UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, + type ValueOf, +} from '../../../shared/lib/constants' import type { ModuleReference, CollectedMetadata } from './metadata/types' import path from 'path' @@ -192,7 +196,8 @@ async function createTreeCodeFromPath( globalError: string }> { const splittedPath = pagePath.split(/[\\/]/, 1) - const isNotFoundRoute = page === '/_not-found' + const isNotFoundRoute = page === UNDERSCORE_NOT_FOUND_ROUTE_ENTRY + const isDefaultNotFound = isAppBuiltinNotFoundPage(pagePath) const appDirPrefix = isDefaultNotFound ? APP_DIR_ALIAS : splittedPath[0] const hasRootNotFound = await resolver( @@ -410,14 +415,16 @@ async function createTreeCodeFromPath( defaultNotFoundPath nestedCollectedAsyncImports.push(notFoundPath) subtreeCode = `{ - children: ['${PAGE_SEGMENT_KEY}', {}, { - page: [ - () => import(/* webpackMode: "eager" */ ${JSON.stringify( - notFoundPath - )}), - ${JSON.stringify(notFoundPath)} - ] - }] + children: [${JSON.stringify(UNDERSCORE_NOT_FOUND_ROUTE)}, { + children: ['${PAGE_SEGMENT_KEY}', {}, { + page: [ + () => import(/* webpackMode: "eager" */ ${JSON.stringify( + notFoundPath + )}), + ${JSON.stringify(notFoundPath)} + ] + }] + }, {}] }` } diff --git a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts index 4dad0b262fe60..5c0eab03cfa9a 100644 --- a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts @@ -22,6 +22,7 @@ import { DEFAULT_RUNTIME_WEBPACK, EDGE_RUNTIME_WEBPACK, SERVER_REFERENCE_MANIFEST, + UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, } from '../../../shared/lib/constants' import { getActions, @@ -333,6 +334,23 @@ export class FlightClientEntryPlugin { bundlePath, absolutePagePath: entryRequest, }) + + // The webpack implementation of writing the client reference manifest relies on all entrypoints writing a page.js even when there is no client components in the page. + // It needs the file in order to write the reference manifest for the path in the `.next/server` folder. + // TODO-APP: This could be better handled, however Turbopack does not have the same problem as we resolve client components in a single graph. + if ( + name === `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}` && + bundlePath === 'app/not-found' + ) { + clientEntriesToInject.push({ + compiler, + compilation, + entryName: name, + clientComponentImports: {}, + bundlePath: `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}`, + absolutePagePath: entryRequest, + }) + } } // Make sure CSS imports are deduplicated before injecting the client entry diff --git a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts index 841131ec6b78e..2c302c71c738a 100644 --- a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts @@ -456,13 +456,6 @@ export class ClientReferenceManifestPlugin { manifestEntryFiles.push(entryName.replace(/\/page(\.[^/]+)?$/, '/page')) } - // Special case for the root not-found page. - // dev: app/not-found - // prod: app/_not-found - if (/^app\/_?not-found(\.[^.]+)?$/.test(entryName)) { - manifestEntryFiles.push(this.dev ? 'app/not-found' : 'app/_not-found') - } - const groupName = entryNameToGroupName(entryName) if (!manifestsPerGroup.has(groupName)) { manifestsPerGroup.set(groupName, []) @@ -503,16 +496,6 @@ export class ClientReferenceManifestPlugin { pagePath.slice('app'.length) )}]=${json}` ) as unknown as webpack.sources.RawSource - - if (pagePath === 'app/not-found') { - // Create a separate special manifest for the root not-found page. - assets['server/app/_not-found_' + CLIENT_REFERENCE_MANIFEST + '.js'] = - new sources.RawSource( - `globalThis.__RSC_MANIFEST=(globalThis.__RSC_MANIFEST||{});globalThis.__RSC_MANIFEST[${JSON.stringify( - '/_not-found' - )}]=${json}` - ) as unknown as webpack.sources.RawSource - } } pluginState.ASYNC_CLIENT_MODULES = [] diff --git a/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts b/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts index 180655723b5f2..8ce9a288dc3aa 100644 --- a/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts +++ b/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts @@ -7,6 +7,7 @@ import type { NodeFileTraceReasons } from 'next/dist/compiled/@vercel/nft' import { CLIENT_REFERENCE_MANIFEST, TRACE_OUTPUT_VERSION, + UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, } from '../../../shared/lib/constants' import { webpack, sources } from 'next/dist/compiled/webpack/webpack' import { @@ -242,8 +243,7 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { // include the client reference manifest const clientManifestsForPage = entrypoint.name.endsWith('/page') || - entrypoint.name === 'app/not-found' || - entrypoint.name === 'app/_not-found' + entrypoint.name === UNDERSCORE_NOT_FOUND_ROUTE_ENTRY ? nodePath.join( outputPath, '..', diff --git a/packages/next/src/client/components/router-reducer/is-navigating-to-new-root-layout.ts b/packages/next/src/client/components/router-reducer/is-navigating-to-new-root-layout.ts index bc37f03fffc37..48820ba3724aa 100644 --- a/packages/next/src/client/components/router-reducer/is-navigating-to-new-root-layout.ts +++ b/packages/next/src/client/components/router-reducer/is-navigating-to-new-root-layout.ts @@ -1,5 +1,4 @@ import type { FlightRouterState } from '../../../server/app-render/types' -import { GLOBAL_NOT_FOUND_SEGMENT_KEY } from '../../../shared/lib/segment' export function isNavigatingToNewRootLayout( currentTree: FlightRouterState, @@ -9,9 +8,6 @@ export function isNavigatingToNewRootLayout( const currentTreeSegment = currentTree[0] const nextTreeSegment = nextTree[0] - // We currently special-case the global not found segment key, but we don't want it to be treated as a root layout change - if (currentTreeSegment === GLOBAL_NOT_FOUND_SEGMENT_KEY) return false - // If any segment is different before we find the root layout, the root layout has changed. // E.g. /same/(group1)/layout.js -> /same/(group2)/layout.js // First segment is 'same' for both, keep looking. (group1) changed to (group2) before the root layout was found, it must have changed. diff --git a/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts index 2ebf5f7bfbcab..4b610c49841a1 100644 --- a/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts @@ -22,10 +22,7 @@ import { handleMutable } from '../handle-mutable' import { applyFlightData } from '../apply-flight-data' import { prefetchQueue } from './prefetch-reducer' import { createEmptyCacheNode } from '../../app-router' -import { - DEFAULT_SEGMENT_KEY, - GLOBAL_NOT_FOUND_SEGMENT_KEY, -} from '../../../../shared/lib/segment' +import { DEFAULT_SEGMENT_KEY } from '../../../../shared/lib/segment' import { listenForDynamicRequest, updateCacheNodeOnNavigation, @@ -203,28 +200,6 @@ function navigateReducer_noPPR( prefetchEntryCacheStatus === PrefetchCacheEntryStatus.reusable ) - if ( - !applied && - // if we've navigated away from the global not found segment but didn't apply the flight data, we need to refetch - // as otherwise we'd be incorrectly using the global not found cache node for the incoming page - currentTree[0] === GLOBAL_NOT_FOUND_SEGMENT_KEY - ) { - applied = addRefetchToLeafSegments( - cache, - currentCache, - flightSegmentPath, - treePatch, - // eslint-disable-next-line no-loop-func - () => - fetchServerResponse( - url, - currentTree, - state.nextUrl, - state.buildId - ) - ) - } - const hardNavigate = shouldHardNavigate( // TODO-APP: remove '' flightSegmentPathWithLeadingEmpty, @@ -450,28 +425,6 @@ function navigateReducer_PPR( prefetchEntryCacheStatus === PrefetchCacheEntryStatus.reusable ) - if ( - !applied && - // if we've navigated away from the global not found segment but didn't apply the flight data, we need to refetch - // as otherwise we'd be incorrectly using the global not found cache node for the incoming page - currentTree[0] === GLOBAL_NOT_FOUND_SEGMENT_KEY - ) { - applied = addRefetchToLeafSegments( - cache, - currentCache, - flightSegmentPath, - treePatch, - // eslint-disable-next-line no-loop-func - () => - fetchServerResponse( - url, - currentTree, - state.nextUrl, - state.buildId - ) - ) - } - const hardNavigate = shouldHardNavigate( // TODO-APP: remove '' flightSegmentPathWithLeadingEmpty, diff --git a/packages/next/src/export/routes/app-page.ts b/packages/next/src/export/routes/app-page.ts index 639bdd835b9a4..2711751634943 100644 --- a/packages/next/src/export/routes/app-page.ts +++ b/packages/next/src/export/routes/app-page.ts @@ -41,6 +41,7 @@ export async function exportAppPage( fileWriter: FileWriter ): Promise { // If the page is `/_not-found`, then we should update the page to be `/404`. + // UNDERSCORE_NOT_FOUND_ROUTE value used here, however we don't want to import it here as it causes constants to be inlined which we don't want here. if (page === '/_not-found') { pathname = '/404' } diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index ef060e128c11e..77fc90f1dff69 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -101,7 +101,6 @@ import { usedDynamicAPIs, createPostponedAbortSignal, } from './dynamic-rendering' -import { GLOBAL_NOT_FOUND_SEGMENT_KEY } from '../../shared/lib/segment' import { getClientComponentLoaderMetrics, wrapClientComponentLoader, @@ -412,16 +411,6 @@ async function ReactServerApp({ tree, ctx, asNotFound }: ReactServerAppProps) { query ) - // If the page we're rendering is being treated as the global not-found page, we want to special-case - // the segment key so it doesn't collide with a page matching the same path. - // This is necessary because when rendering the global not-found, it will always be the root segment. - // If the not-found page prefetched a link to the root page, it would have the same data path - // (e.g., ['', { children: ['__PAGE__', {}] }]). Without this disambiguation, the router would interpret - // these pages as being able to share the same cache nodes, which is not the case as they render different things. - if (asNotFound) { - initialTree[0] = GLOBAL_NOT_FOUND_SEGMENT_KEY - } - const [MetadataTree, MetadataOutlet] = createMetadataComponents({ tree, errorType: asNotFound ? 'not-found' : undefined, diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 8f49e3c163c79..d201ebe49811c 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -49,6 +49,8 @@ import { NEXT_BUILTIN_DOCUMENT, PAGES_MANIFEST, STATIC_STATUS_PAGES, + UNDERSCORE_NOT_FOUND_ROUTE, + UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, } from '../shared/lib/constants' import { isDynamicRoute } from '../shared/lib/router/utils' import { checkIsOnDemandRevalidate } from './api-utils' @@ -1726,7 +1728,8 @@ export default abstract class Server { ): Promise { const is404Page = // For edge runtime 404 page, /_not-found needs to be treated as 404 page - (process.env.NEXT_RUNTIME === 'edge' && pathname === '/_not-found') || + (process.env.NEXT_RUNTIME === 'edge' && + pathname === UNDERSCORE_NOT_FOUND_ROUTE) || pathname === '/404' // Strip the internal headers. @@ -3234,7 +3237,7 @@ export default abstract class Server { if (this.enabledDirectories.app) { // Use the not-found entry in app directory result = await this.findPageComponents({ - page: this.renderOpts.dev ? '/not-found' : '/_not-found', + page: UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, query, params: {}, isAppPath: true, diff --git a/packages/next/src/server/dev/on-demand-entry-handler.ts b/packages/next/src/server/dev/on-demand-entry-handler.ts index c7289391a9b29..ce04d6530650c 100644 --- a/packages/next/src/server/dev/on-demand-entry-handler.ts +++ b/packages/next/src/server/dev/on-demand-entry-handler.ts @@ -34,6 +34,8 @@ import { COMPILER_INDEXES, COMPILER_NAMES, RSC_MODULE_TYPES, + UNDERSCORE_NOT_FOUND_ROUTE, + UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, } from '../../shared/lib/constants' import { PAGE_SEGMENT_KEY } from '../../shared/lib/segment' import { HMR_ACTIONS_SENT_TO_BROWSER } from './hot-reloader-types' @@ -427,6 +429,29 @@ export async function findPagePathData( // Check appDir first falling back to pagesDir if (appDir) { + if (page === UNDERSCORE_NOT_FOUND_ROUTE_ENTRY) { + const notFoundPath = await findPageFile( + appDir, + 'not-found', + extensions, + true + ) + if (notFoundPath) { + return { + filename: join(appDir, notFoundPath), + bundlePath: `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}`, + page: UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, + } + } + + return { + filename: require.resolve( + 'next/dist/client/components/not-found-error' + ), + bundlePath: `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}`, + page: UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, + } + } pagePath = await findPageFile(appDir, normalizedPagePath, extensions, true) if (pagePath) { const pageUrl = ensureLeadingSlash( @@ -467,14 +492,6 @@ export async function findPagePathData( } } - if (page === '/not-found' && appDir) { - return { - filename: require.resolve('next/dist/client/components/not-found-error'), - bundlePath: 'app/not-found', - page: '/not-found', - } - } - if (page === '/_error') { return { filename: require.resolve('next/dist/pages/_error'), diff --git a/packages/next/src/server/lib/router-server.ts b/packages/next/src/server/lib/router-server.ts index 2ee275d27a0b3..5849ef4f237b1 100644 --- a/packages/next/src/server/lib/router-server.ts +++ b/packages/next/src/server/lib/router-server.ts @@ -32,6 +32,7 @@ import { parseUrl as parseUrlUtil } from '../../shared/lib/router/utils/parse-ur import { PHASE_PRODUCTION_SERVER, PHASE_DEVELOPMENT_SERVER, + UNDERSCORE_NOT_FOUND_ROUTE, } from '../../shared/lib/constants' import { RedirectStatusCode } from '../../client/components/redirect-status-code' import { DevBundlerService } from './dev-bundler-service' @@ -532,14 +533,14 @@ export async function initialize(opts: { const appNotFound = opts.dev ? developmentBundler?.serverFields.hasAppNotFound - : await fsChecker.getItem('/_not-found') + : await fsChecker.getItem(UNDERSCORE_NOT_FOUND_ROUTE) res.statusCode = 404 if (appNotFound) { return await invokeRender( parsedUrl, - opts.dev ? '/not-found' : '/_not-found', + UNDERSCORE_NOT_FOUND_ROUTE, handleIndex, { 'x-invoke-status': '404', diff --git a/packages/next/src/server/load-components.ts b/packages/next/src/server/load-components.ts index 75b004b75f0e3..b342313c0c5a5 100644 --- a/packages/next/src/server/load-components.ts +++ b/packages/next/src/server/load-components.ts @@ -19,6 +19,7 @@ import { REACT_LOADABLE_MANIFEST, CLIENT_REFERENCE_MANIFEST, SERVER_REFERENCE_MANIFEST, + UNDERSCORE_NOT_FOUND_ROUTE, } from '../shared/lib/constants' import { join } from 'path' import { requirePage } from './require' @@ -139,8 +140,7 @@ async function loadComponentsImpl({ // Make sure to avoid loading the manifest for Route Handlers const hasClientManifest = - isAppPath && - (page.endsWith('/page') || page === '/not-found' || page === '/_not-found') + isAppPath && (page.endsWith('/page') || page === UNDERSCORE_NOT_FOUND_ROUTE) // Load the manifest files first const [ diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index fdaf0da519e01..60b03569be582 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -40,6 +40,8 @@ import { SERVER_DIRECTORY, NEXT_FONT_MANIFEST, PHASE_PRODUCTION_BUILD, + UNDERSCORE_NOT_FOUND_ROUTE, + UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, } from '../shared/lib/constants' import { findDir } from '../lib/find-pages-dir' import { NodeNextRequest, NodeNextResponse } from './base-http/node' @@ -1306,25 +1308,23 @@ export default class NextNodeServer extends BaseServer { const is404 = res.statusCode === 404 if (is404 && this.enabledDirectories.app) { - const notFoundPathname = this.renderOpts.dev - ? '/not-found' - : '/_not-found' - if (this.renderOpts.dev) { await this.ensurePage({ - page: notFoundPathname, + page: UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, clientOnly: false, url: req.url, }).catch(() => {}) } - if (this.getEdgeFunctionsPages().includes(notFoundPathname)) { + if ( + this.getEdgeFunctionsPages().includes(UNDERSCORE_NOT_FOUND_ROUTE_ENTRY) + ) { await this.runEdgeFunction({ req: req as BaseNextRequest, res: res as BaseNextResponse, query: query || {}, params: {}, - page: notFoundPathname, + page: UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, appPaths: null, }) return null diff --git a/packages/next/src/server/web-server.ts b/packages/next/src/server/web-server.ts index 4a4b3df468726..797079023b4e2 100644 --- a/packages/next/src/server/web-server.ts +++ b/packages/next/src/server/web-server.ts @@ -32,6 +32,7 @@ import { IncrementalCache } from './lib/incremental-cache' import type { PAGE_TYPES } from '../lib/page-types' import type { Rewrite } from '../lib/load-custom-routes' import { buildCustomRoute } from '../lib/build-custom-route' +import { UNDERSCORE_NOT_FOUND_ROUTE } from '../api/constants' interface WebServerOptions extends Options { webServerConfig: { @@ -242,7 +243,7 @@ export default class NextWebServer extends BaseServer { // For edge runtime if the pathname hit as /_not-found entrypoint, // override the pathname to /404 for rendering - if (pathname === (renderOpts.dev ? '/not-found' : '/_not-found')) { + if (pathname === UNDERSCORE_NOT_FOUND_ROUTE) { pathname = '/404' } return renderToHTML( diff --git a/packages/next/src/shared/lib/constants.ts b/packages/next/src/shared/lib/constants.ts index b6843384aea4e..5f0e5196af416 100644 --- a/packages/next/src/shared/lib/constants.ts +++ b/packages/next/src/shared/lib/constants.ts @@ -33,6 +33,8 @@ export const COMPILER_INDEXES: { [COMPILER_NAMES.edgeServer]: 2, } as const +export const UNDERSCORE_NOT_FOUND_ROUTE = '/_not-found' +export const UNDERSCORE_NOT_FOUND_ROUTE_ENTRY = `${UNDERSCORE_NOT_FOUND_ROUTE}/page` export const PHASE_EXPORT = 'phase-export' export const PHASE_PRODUCTION_BUILD = 'phase-production-build' export const PHASE_PRODUCTION_SERVER = 'phase-production-server' diff --git a/packages/next/src/shared/lib/segment.ts b/packages/next/src/shared/lib/segment.ts index 0a0f80b6c35fa..38c789d17d6f6 100644 --- a/packages/next/src/shared/lib/segment.ts +++ b/packages/next/src/shared/lib/segment.ts @@ -5,4 +5,3 @@ export function isGroupSegment(segment: string) { export const PAGE_SEGMENT_KEY = '__PAGE__' export const DEFAULT_SEGMENT_KEY = '__DEFAULT__' -export const GLOBAL_NOT_FOUND_SEGMENT_KEY = '__NOT_FOUND__' diff --git a/test/development/basic/next-rs-api.test.ts b/test/development/basic/next-rs-api.test.ts index ea01931969f94..70ecaddaeca36 100644 --- a/test/development/basic/next-rs-api.test.ts +++ b/test/development/basic/next-rs-api.test.ts @@ -228,8 +228,8 @@ describe('next.rs api', () => { }) it('should detect the correct routes', async () => { - const entrypointsSubscribtion = project.entrypointsSubscribe() - const entrypoints = await entrypointsSubscribtion.next() + const entrypointsSubscription = project.entrypointsSubscribe() + const entrypoints = await entrypointsSubscription.next() expect(entrypoints.done).toBe(false) expect(Array.from(entrypoints.value.routes.keys()).sort()).toEqual([ '/', @@ -239,7 +239,6 @@ describe('next.rs api', () => { '/app', '/app-edge', '/app-nodejs', - '/not-found', '/page-edge', '/page-nodejs', '/route-edge', @@ -249,7 +248,7 @@ describe('next.rs api', () => { expect(normalizeDiagnostics(entrypoints.value.diagnostics)).toMatchSnapshot( 'diagnostics' ) - entrypointsSubscribtion.return() + entrypointsSubscription.return() }) const routes = [ diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 997feeb48db6d..9cf524e74d121 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -504,9 +504,9 @@ createNextDescribe( "(new)/custom/page.js", "(new)/custom/page_client-reference-manifest.js", "_not-found.html", - "_not-found.js", "_not-found.rsc", - "_not-found_client-reference-manifest.js", + "_not-found/page.js", + "_not-found/page_client-reference-manifest.js", "api/draft-mode/route.js", "api/large-data/route.js", "api/revalidate-path-edge/route.js", diff --git a/test/e2e/app-dir/not-found-default/index.test.ts b/test/e2e/app-dir/not-found-default/index.test.ts index 68c71f1b6115c..2eb4198553ded 100644 --- a/test/e2e/app-dir/not-found-default/index.test.ts +++ b/test/e2e/app-dir/not-found-default/index.test.ts @@ -31,12 +31,6 @@ createNextDescribe( expect(await browser.elementByCss('html').getAttribute('class')).toBe( 'root-layout-html' ) - - if (isNextDev) { - const cliOutput = next.cliOutput - expect(cliOutput).toContain('/not-found') - expect(cliOutput).not.toContain('/_error') - } }) it('should error on server notFound from root layout on server-side', async () => { diff --git a/test/e2e/app-dir/not-found/basic/index.test.ts b/test/e2e/app-dir/not-found/basic/index.test.ts index 18c16f9379583..e8da5b5039ec6 100644 --- a/test/e2e/app-dir/not-found/basic/index.test.ts +++ b/test/e2e/app-dir/not-found/basic/index.test.ts @@ -28,11 +28,11 @@ createNextDescribe( if (isNextStart) { it('should include not found client reference manifest in the file trace', async () => { const fileTrace = JSON.parse( - await next.readFile('.next/server/app/_not-found.js.nft.json') + await next.readFile('.next/server/app/_not-found/page.js.nft.json') ) const isTraced = fileTrace.files.some((filePath) => - filePath.includes('_not-found_client-reference-manifest.js') + filePath.includes('page_client-reference-manifest.js') ) expect(isTraced).toBe(true)