Skip emitting pages router entries when only app router present (#82444)

This commit is contained in:
Jiachi Liu
2025-08-22 00:41:38 +02:00
committed by GitHub
parent 332e73e83d
commit 8f6cd5e809
51 changed files with 1028 additions and 216 deletions
+1
View File
@@ -171,6 +171,7 @@ impl AppProject {
self.app_dir.clone(),
conf.page_extensions(),
conf.is_global_not_found_enabled(),
self.project.next_mode(),
)
}
+102 -38
View File
@@ -106,9 +106,16 @@ impl PagesProject {
document: _,
error: _,
error_500: _,
has_user_pages: _,
should_create_pages_entries,
} = &*pages_structure.await?;
let mut routes = FxIndexMap::default();
// If pages entries shouldn't be created (build mode with no pages), return empty routes
if !should_create_pages_entries {
return Ok(Vc::cell(routes));
}
async fn add_page_to_routes(
routes: &mut FxIndexMap<RcStr, Route>,
page: Vc<PagesStructureItem>,
@@ -272,6 +279,7 @@ impl PagesProject {
self.project.project_path().owned().await?,
next_router_root,
self.project.next_config().page_extensions(),
self.project.next_mode(),
))
}
@@ -1256,18 +1264,23 @@ impl PageEndpoint {
entry_chunk: Vc<Box<dyn OutputAsset>>,
) -> Result<Vc<Box<dyn OutputAsset>>> {
let node_root = self.pages_project.project().node_root().await?;
let chunk_path = entry_chunk.path().await?;
let asset_path = node_root
.join("server")?
.get_path_to(&chunk_path)
.context("ssr chunk entry path must be inside the node root")?;
let pages_manifest = PagesManifest {
pages: [(self.pathname.clone(), asset_path.into())]
// Check if we should include pages in the manifest
let pages_structure = self.pages_structure.await?;
let pages = if pages_structure.should_create_pages_entries {
let chunk_path = entry_chunk.path().await?;
let asset_path = node_root
.join("server")?
.get_path_to(&chunk_path)
.context("ssr chunk entry path must be inside the node root")?;
[(self.pathname.clone(), asset_path.into())]
.into_iter()
.collect(),
.collect()
} else {
FxIndexMap::default() // Empty pages when no user pages should be created
};
let pages_manifest = PagesManifest { pages };
let manifest_path_prefix = get_asset_prefix_from_pathname(&self.pathname);
let asset = Vc::upcast(VirtualOutputAsset::new(
node_root.join(&format!(
@@ -1314,8 +1327,17 @@ impl PageEndpoint {
.client_relative_path()
.owned()
.await?;
// Check if we should include pages in the manifest
let pages_structure = self.pages_structure.await?;
let pages = if pages_structure.should_create_pages_entries {
fxindexmap!(self.pathname.clone() => client_chunks)
} else {
fxindexmap![] // Empty pages when no user pages should be created
};
let build_manifest = BuildManifest {
pages: fxindexmap!(self.pathname.clone() => client_chunks),
pages,
..Default::default()
};
let manifest_path_prefix = get_asset_prefix_from_pathname(&self.pathname);
@@ -1339,10 +1361,18 @@ impl PageEndpoint {
let this = self.await?;
let node_root = this.pages_project.project().node_root().await?;
let client_relative_path = this.pages_project.project().client_relative_path().await?;
let page_loader_path = client_relative_path
.get_relative_path_to(&*page_loader.path().await?)
.context("failed to resolve client-relative path to page loader")?;
let client_build_manifest = fxindexmap!(this.pathname.clone() => vec![page_loader_path]);
// Check if we should include pages in the manifest
let pages_structure = this.pages_structure.await?;
let client_build_manifest = if pages_structure.should_create_pages_entries {
let page_loader_path = client_relative_path
.get_relative_path_to(&*page_loader.path().await?)
.context("failed to resolve client-relative path to page loader")?;
fxindexmap!(this.pathname.clone() => vec![page_loader_path])
} else {
fxindexmap![] // Empty manifest when no user pages should be created
};
let manifest_path_prefix = get_asset_prefix_from_pathname(&this.pathname);
Ok(Vc::upcast(VirtualOutputAsset::new_with_references(
node_root.join(&format!(
@@ -1372,6 +1402,7 @@ impl PageEndpoint {
PageEndpointType::Html => {
let client_chunks = *self.client_chunks().await?.assets;
client_assets.extend(client_chunks.await?.iter().map(|asset| **asset));
let build_manifest = self.build_manifest(client_chunks).to_resolved().await?;
let page_loader = self.page_loader(client_chunks);
let client_build_manifest = self
@@ -1381,8 +1412,10 @@ impl PageEndpoint {
client_assets.push(page_loader);
server_assets.push(build_manifest);
server_assets.push(client_build_manifest);
self.ssr_chunk(emit_manifests)
}
PageEndpointType::Data => self.ssr_data_chunk(emit_manifests),
PageEndpointType::Api => self.api_chunk(emit_manifests),
PageEndpointType::SsrOnly => self.ssr_chunk(emit_manifests),
@@ -1439,9 +1472,13 @@ impl PageEndpoint {
dynamic_import_entries,
server_asset_trace_file,
} => {
server_assets.push(entry);
if let Some(server_asset_trace_file) = &*server_asset_trace_file.await? {
server_assets.push(*server_asset_trace_file);
// Only include the actual SSR entry chunk if pages should be created
let pages_structure = this.pages_structure.await?;
if pages_structure.should_create_pages_entries {
server_assets.push(entry);
if let Some(server_asset_trace_file) = &*server_asset_trace_file.await? {
server_assets.push(*server_asset_trace_file);
}
}
if emit_manifests != EmitManifests::None {
@@ -1482,32 +1519,49 @@ impl PageEndpoint {
};
let files_value = files.await?;
if let Some(&file) = files_value.first() {
let pages_manifest = self.pages_manifest(*file).to_resolved().await?;
server_assets.push(pages_manifest);
}
server_assets.extend(files_value.iter().copied());
file_paths_from_root
.extend(get_js_paths_from_root(&node_root, &files_value).await?);
// Only include the actual edge files if pages should be created
let pages_structure = this.pages_structure.await?;
if pages_structure.should_create_pages_entries {
server_assets.extend(files_value.iter().copied());
file_paths_from_root
.extend(get_js_paths_from_root(&node_root, &files_value).await?);
}
if emit_manifests == EmitManifests::Full {
let loadable_manifest_output = self
.react_loadable_manifest(*dynamic_import_entries, NextRuntime::Edge)
.await?;
server_assets.extend(loadable_manifest_output.iter().copied());
file_paths_from_root.extend(
get_js_paths_from_root(&node_root, &loadable_manifest_output).await?,
);
if pages_structure.should_create_pages_entries {
server_assets.extend(loadable_manifest_output.iter().copied());
file_paths_from_root.extend(
get_js_paths_from_root(&node_root, &loadable_manifest_output)
.await?,
);
}
}
let all_output_assets = all_assets_from_entries(*files).await?;
let (wasm_paths_from_root, all_assets) =
if pages_structure.should_create_pages_entries {
let all_output_assets = all_assets_from_entries(*files).await?;
let mut wasm_paths_from_root = fxindexset![];
wasm_paths_from_root
.extend(get_wasm_paths_from_root(&node_root, &all_output_assets).await?);
let mut wasm_paths_from_root = fxindexset![];
wasm_paths_from_root.extend(
get_wasm_paths_from_root(&node_root, &all_output_assets).await?,
);
let all_assets =
get_asset_paths_from_root(&node_root, &all_output_assets).await?;
let all_assets =
get_asset_paths_from_root(&node_root, &all_output_assets).await?;
(wasm_paths_from_root, all_assets)
} else {
(fxindexset![], vec![])
};
let named_regex = get_named_middleware_regex(&this.pathname).into();
let matchers = MiddlewareMatcher {
@@ -1649,14 +1703,24 @@ impl Endpoint for PageEndpoint {
let node_root = node_root.clone();
let written_endpoint = match *output {
PageEndpointOutput::NodeJs { entry_chunk, .. } => EndpointOutputPaths::NodeJs {
server_entry_path: node_root
.get_path_to(&*entry_chunk.path().await?)
.context("ssr chunk entry path must be inside the node root")?
.to_string(),
server_paths,
client_paths,
},
PageEndpointOutput::NodeJs { entry_chunk, .. } => {
// Only set server_entry_path if pages should be created
let pages_structure = this.pages_structure.await?;
let server_entry_path = if pages_structure.should_create_pages_entries {
node_root
.get_path_to(&*entry_chunk.path().await?)
.context("ssr chunk entry path must be inside the node root")?
.to_string()
} else {
String::new() // Empty path when no pages should be created
};
EndpointOutputPaths::NodeJs {
server_entry_path,
server_paths,
client_paths,
}
}
PageEndpointOutput::Edge { .. } => EndpointOutputPaths::Edge {
server_paths,
client_paths,
+49
View File
@@ -16,6 +16,7 @@ use turbopack_core::issue::{
};
use crate::{
mode::NextMode,
next_app::{
AppPage, AppPath, PageSegment, PageType,
metadata::{
@@ -753,12 +754,14 @@ pub fn get_entrypoints(
app_dir: FileSystemPath,
page_extensions: Vc<Vec<RcStr>>,
is_global_not_found_enabled: Vc<bool>,
next_mode: Vc<NextMode>,
) -> Vc<Entrypoints> {
directory_tree_to_entrypoints(
app_dir.clone(),
get_directory_tree(app_dir.clone(), page_extensions),
get_global_metadata(app_dir, page_extensions),
is_global_not_found_enabled,
next_mode,
Default::default(),
Default::default(),
)
@@ -786,6 +789,7 @@ fn directory_tree_to_entrypoints(
directory_tree: Vc<DirectoryTree>,
global_metadata: Vc<GlobalMetadata>,
is_global_not_found_enabled: Vc<bool>,
next_mode: Vc<NextMode>,
root_layouts: Vc<FileSystemPathVec>,
root_params: Vc<RootParamVecOption>,
) -> Vc<Entrypoints> {
@@ -793,6 +797,7 @@ fn directory_tree_to_entrypoints(
app_dir,
global_metadata,
is_global_not_found_enabled,
next_mode,
rcstr!(""),
directory_tree,
AppPage::new(),
@@ -1247,6 +1252,7 @@ async fn directory_tree_to_entrypoints_internal(
app_dir: FileSystemPath,
global_metadata: ResolvedVc<GlobalMetadata>,
is_global_not_found_enabled: Vc<bool>,
next_mode: Vc<NextMode>,
directory_name: RcStr,
directory_tree: Vc<DirectoryTree>,
app_page: AppPage,
@@ -1258,6 +1264,7 @@ async fn directory_tree_to_entrypoints_internal(
app_dir,
global_metadata,
is_global_not_found_enabled,
next_mode,
directory_name,
directory_tree,
app_page,
@@ -1272,6 +1279,7 @@ async fn directory_tree_to_entrypoints_internal_untraced(
app_dir: FileSystemPath,
global_metadata: ResolvedVc<GlobalMetadata>,
is_global_not_found_enabled: Vc<bool>,
next_mode: Vc<NextMode>,
directory_name: RcStr,
directory_tree: Vc<DirectoryTree>,
app_page: AppPage,
@@ -1518,6 +1526,46 @@ async fn directory_tree_to_entrypoints_internal_untraced(
root_params,
);
}
// Create production global error page only in build mode
// This aligns with webpack: default Pages entries (including /_error) are only added when
// the build isn't app-only. If the build is app-only (no user pages/api), we should still
// expose the app global error so runtime errors render, but we shouldn't emit it otherwise.
if matches!(*next_mode.await?, NextMode::Build) {
// Use built-in global-error.js to create a `_global-error/page` route.
let global_error_tree = AppPageLoaderTree {
page: app_page.clone(),
segment: directory_name.clone(),
parallel_routes: fxindexmap! {
rcstr!("children") => AppPageLoaderTree {
page: app_page.clone(),
segment: rcstr!("__PAGE__"),
parallel_routes: FxIndexMap::default(),
modules: AppDirModules {
page: Some(get_next_package(app_dir.clone())
.await?
.join("dist/client/components/builtin/app-error.js")?),
..Default::default()
},
global_metadata,
}
},
modules: AppDirModules::default(),
global_metadata,
}
.resolved_cell();
let app_global_error_page = app_page
.clone_push_str("_global-error")?
.complete(PageType::Page)?;
add_app_page(
app_dir.clone(),
&mut result,
app_global_error_page,
global_error_tree,
root_params,
);
}
}
let app_page = &app_page;
@@ -1542,6 +1590,7 @@ async fn directory_tree_to_entrypoints_internal_untraced(
app_dir.clone(),
*global_metadata,
is_global_not_found_enabled,
next_mode,
subdir_name.clone(),
*subdirectory,
child_app_page.clone(),
+15
View File
@@ -76,6 +76,8 @@ pub struct PagesStructure {
pub error_500: Option<ResolvedVc<PagesStructureItem>>,
pub api: Option<ResolvedVc<PagesDirectoryStructure>>,
pub pages: Option<ResolvedVc<PagesDirectoryStructure>>,
pub has_user_pages: bool,
pub should_create_pages_entries: bool,
}
#[turbo_tasks::value]
@@ -102,6 +104,7 @@ pub async fn find_pages_structure(
project_root: FileSystemPath,
next_router_root: FileSystemPath,
page_extensions: Vc<Vec<RcStr>>,
next_mode: Vc<crate::mode::NextMode>,
) -> Result<Vc<PagesStructure>> {
let pages_root = project_root.join("pages")?.realpath().owned().await?;
let pages_root = if *pages_root.get_type().await? == FileSystemEntryType::Directory {
@@ -123,6 +126,7 @@ pub async fn find_pages_structure(
Vc::cell(pages_root),
next_router_root,
page_extensions,
next_mode,
))
}
@@ -133,6 +137,7 @@ async fn get_pages_structure_for_root_directory(
project_path: Vc<FileSystemPathOption>,
next_router_path: FileSystemPath,
page_extensions: Vc<Vec<RcStr>>,
next_mode: Vc<crate::mode::NextMode>,
) -> Result<Vc<PagesStructure>> {
let page_extensions_raw = &*page_extensions.await?;
@@ -259,6 +264,14 @@ async fn get_pages_structure_for_root_directory(
project_root.join("pages")?
};
// Check if there are any actual user pages (not just _app, _document, _error)
// error_500_item can be auto-generated for app router, so only count it if there are other user
// pages
let has_user_pages = pages_directory.is_some() || api_directory.is_some();
// Only skip user pages routes during build mode when there are no user pages
let should_create_pages_entries = has_user_pages || next_mode.await?.is_development();
let app_item = {
let app_router_path = next_router_path.join("_app")?;
PagesStructureItem::new(
@@ -311,6 +324,8 @@ async fn get_pages_structure_for_root_directory(
error_500: error_500_item.to_resolved().await?,
api: api_directory,
pages: pages_directory,
has_user_pages,
should_create_pages_entries,
}
.cell())
}
+52 -18
View File
@@ -27,7 +27,7 @@ import { isEdgeRuntime } from '../lib/is-edge-runtime'
import {
APP_CLIENT_INTERNALS,
RSC_MODULE_TYPES,
UNDERSCORE_NOT_FOUND_ROUTE_ENTRY,
UNDERSCORE_NOT_FOUND_ROUTE,
} from '../shared/lib/constants'
import {
CLIENT_STATIC_FILES_RUNTIME_AMP,
@@ -46,6 +46,7 @@ import {
isInstrumentationHookFile,
isInstrumentationHookFilename,
reduceAppConfig,
isAppBuiltinPage,
} from './utils'
import {
getAppPageStaticInfo,
@@ -80,6 +81,11 @@ import type { createValidFileMatcher } from '../server/lib/find-page-file'
import { isReservedPage } from './utils'
import { isParallelRouteSegment } from '../shared/lib/segment'
import { ensureLeadingSlash } from '../shared/lib/page-path/ensure-leading-slash'
import {
UNDERSCORE_NOT_FOUND_ROUTE_ENTRY,
UNDERSCORE_GLOBAL_ERROR_ROUTE,
UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY,
} from '../shared/lib/entry-constants'
/**
* Collect app pages, layouts, and default files from the app directory
@@ -225,10 +231,15 @@ export function extractSlotsFromAppRoutes(mappedAppPages: {
}): SlotInfo[] {
const slots: SlotInfo[] = []
for (const [route] of Object.entries(mappedAppPages)) {
if (route === '/_not-found/page') continue
for (const [page] of Object.entries(mappedAppPages)) {
if (
page === UNDERSCORE_NOT_FOUND_ROUTE_ENTRY ||
page === UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY
) {
continue
}
const segments = route.split('/')
const segments = page.split('/')
for (let i = segments.length - 1; i >= 0; i--) {
const segment = segments[i]
if (isParallelRouteSegment(segment)) {
@@ -327,8 +338,13 @@ export function processAppRoutes(
const appRoutes: RouteInfo[] = []
const appRouteHandlers: RouteInfo[] = []
for (const [route, filePath] of Object.entries(mappedAppPages)) {
if (route === '/_not-found/page') continue
for (const [page, filePath] of Object.entries(mappedAppPages)) {
if (
page === UNDERSCORE_NOT_FOUND_ROUTE_ENTRY ||
page === UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY
) {
continue
}
const relativeFilePath = createRelativeFilePath(
baseDir,
@@ -339,12 +355,12 @@ export function processAppRoutes(
if (validFileMatcher.isAppRouterRoute(filePath)) {
appRouteHandlers.push({
route: normalizeAppPath(normalizePathSep(route)),
route: normalizeAppPath(normalizePathSep(page)),
filePath: relativeFilePath,
})
} else {
appRoutes.push({
route: normalizeAppPath(normalizePathSep(route)),
route: normalizeAppPath(normalizePathSep(page)),
filePath: relativeFilePath,
})
}
@@ -439,10 +455,15 @@ export async function getStaticInfoIncludingLayouts({
return pageStaticInfo
}
// Skip inheritance for global-error pages - always use default config
if (page === UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY) {
return pageStaticInfo
}
const segments = [pageStaticInfo]
// inherit from layout files only if it's a page route
if (isAppPageRoute(page)) {
// inherit from layout files only if it's a page route and not a builtin page
if (isAppPageRoute(page) && !isAppBuiltinPage(pageFilePath)) {
const layoutFiles = []
const potentialLayoutFiles = pageExtensions.map((ext) => 'layout.' + ext)
let dir = dirname(pageFilePath)
@@ -539,6 +560,7 @@ export async function createPagesMapping({
pagesType,
pagesDir,
appDir,
appDirOnly,
}: {
isDev: boolean
pageExtensions: PageExtensions
@@ -546,6 +568,7 @@ export async function createPagesMapping({
pagesType: PAGE_TYPES
pagesDir: string | undefined
appDir: string | undefined
appDirOnly: boolean
}): Promise<MappedPages> {
const isAppRoute = pagesType === 'app'
const pages: MappedPages = {}
@@ -558,9 +581,12 @@ export async function createPagesMapping({
let pageKey = getPageFromPath(pagePath, pageExtensions)
if (isAppRoute) {
pageKey = pageKey.replace(/%5F/g, '_')
if (pageKey === '/not-found') {
if (pageKey === UNDERSCORE_NOT_FOUND_ROUTE) {
pageKey = UNDERSCORE_NOT_FOUND_ROUTE_ENTRY
}
if (pageKey === UNDERSCORE_GLOBAL_ERROR_ROUTE) {
pageKey = UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY
}
}
const normalizedPath = normalizePathSep(
@@ -605,9 +631,9 @@ export async function createPagesMapping({
return pages
}
case PAGE_TYPES.APP: {
const hasAppPages = Object.keys(pages).some((page) =>
page.endsWith('/page')
)
const hasAppPages = Object.keys(pages).length > 0
// Whether to emit App router 500.html entry, which only presents in production and only app router presents
const hasAppGlobalError = !isDev && appDirOnly
return {
// If there's any app pages existed, add a default /_not-found route as 404.
// If there's any custom /_not-found page, it will override the default one.
@@ -616,6 +642,11 @@ export async function createPagesMapping({
'next/dist/client/components/builtin/global-not-found'
),
}),
...(hasAppGlobalError && {
[UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY]: require.resolve(
'next/dist/client/components/builtin/app-error'
),
}),
...pages,
}
}
@@ -632,10 +663,13 @@ export async function createPagesMapping({
const root = isDev && pagesDir ? PAGES_DIR_ALIAS : 'next/dist/pages'
return {
'/_app': `${root}/_app`,
'/_error': `${root}/_error`,
'/_document': `${root}/_document`,
...pages,
// Don't add default pages entries if this is an app-router-only build
...((isDev || !appDirOnly) && {
'/_app': `${root}/_app`,
'/_error': `${root}/_error`,
'/_document': `${root}/_document`,
...pages,
}),
}
}
default: {
+120 -31
View File
@@ -76,11 +76,14 @@ import {
MIDDLEWARE_REACT_LOADABLE_MANIFEST,
SERVER_REFERENCE_MANIFEST,
FUNCTIONS_CONFIG_MANIFEST,
UNDERSCORE_NOT_FOUND_ROUTE,
UNDERSCORE_NOT_FOUND_ROUTE_ENTRY,
DYNAMIC_CSS_MANIFEST,
TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST,
} from '../shared/lib/constants'
import {
UNDERSCORE_NOT_FOUND_ROUTE,
UNDERSCORE_NOT_FOUND_ROUTE_ENTRY,
UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY,
} from '../shared/lib/entry-constants'
import { isDynamicRoute } from '../shared/lib/router/utils'
import type { __ApiPreviewProps } from '../server/api-utils'
import loadConfig from '../server/config'
@@ -135,7 +138,7 @@ import {
printTreeView,
copyTracedFiles,
isReservedPage,
isAppBuiltinNotFoundPage,
isAppBuiltinPage,
collectRoutesUsingEdgeRuntime,
collectMeta,
} from './utils'
@@ -839,7 +842,8 @@ async function writeFullyStaticExport(
dir: string,
enabledDirectories: NextEnabledDirectories,
configOutDir: string,
nextBuildSpan: Span
nextBuildSpan: Span,
appDirOnly: boolean
): Promise<void> {
const exportApp = (require('../export') as typeof import('../export'))
.default as typeof import('../export').default
@@ -853,6 +857,7 @@ async function writeFullyStaticExport(
silent: true,
outdir: path.join(dir, configOutDir),
numWorkers: getNumberOfWorkers(config),
appDirOnly,
},
nextBuildSpan
)
@@ -999,6 +1004,11 @@ export default async function build(
const publicDir = path.join(dir, 'public')
const { pagesDir, appDir } = findPagesDir(dir)
if (!appDirOnly && !pagesDir) {
appDirOnly = true
}
NextBuildContext.pagesDir = pagesDir
NextBuildContext.appDir = appDir
@@ -1175,6 +1185,7 @@ export default async function build(
pagePaths: pagesPaths,
pagesDir,
appDir,
appDirOnly,
})
)
NextBuildContext.mappedPages = mappedPages
@@ -1216,6 +1227,7 @@ export default async function build(
pageExtensions: config.pageExtensions,
pagesDir,
appDir,
appDirOnly,
})
)
@@ -1229,6 +1241,7 @@ export default async function build(
pageExtensions: config.pageExtensions,
pagesDir,
appDir,
appDirOnly,
})
)
@@ -1242,6 +1255,7 @@ export default async function build(
pagesType: PAGE_TYPES.ROOT,
pagesDir: pagesDir,
appDir,
appDirOnly,
})
NextBuildContext.mappedRootPaths = mappedRootPaths
@@ -1320,6 +1334,7 @@ export default async function build(
pageExtensions: config.pageExtensions,
pagesDir,
appDir,
appDirOnly,
})
)
slotsFromDefaults =
@@ -1385,8 +1400,15 @@ export default async function build(
const conflictingPublicFiles: string[] = []
const hasPages404 = mappedPages['/404']?.startsWith(PAGES_DIR_ALIAS)
const hasApp404 = !!mappedAppPages?.[UNDERSCORE_NOT_FOUND_ROUTE_ENTRY]
const hasAppGlobalError =
!!mappedAppPages?.[UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY]
const hasCustomErrorPage =
mappedPages['/_error'].startsWith(PAGES_DIR_ALIAS)
mappedPages['/_error']?.startsWith(PAGES_DIR_ALIAS)
// Check if there are any user pages (non-reserved pages) in the pages router
const hasUserPagesRoutes = Object.keys(mappedPages).some(
(route) => !isReservedPage(route)
)
if (hasPublicDir) {
const hasPublicUnderScoreNextDir = existsSync(
@@ -1847,7 +1869,7 @@ export default async function build(
namedExports: [],
isNextImageImported: true,
hasSsrAmpPages: !!pagesDir,
hasNonStaticErrorPage: true,
hasNonStaticErrorPage: hasUserPagesRoutes,
}
}
@@ -1896,22 +1918,24 @@ export default async function build(
const appPageToCheck = '/_app'
const customAppGetInitialPropsPromise = worker.hasCustomGetInitialProps(
{
page: appPageToCheck,
distDir,
runtimeEnvConfig,
checkingApp: true,
sriEnabled,
}
)
const customAppGetInitialPropsPromise = hasUserPagesRoutes
? worker.hasCustomGetInitialProps({
page: appPageToCheck,
distDir,
runtimeEnvConfig,
checkingApp: true,
sriEnabled,
})
: Promise.resolve(false)
const namedExportsPromise = worker.getDefinedNamedExports({
page: appPageToCheck,
distDir,
runtimeEnvConfig,
sriEnabled,
})
const namedExportsPromise = hasUserPagesRoutes
? worker.getDefinedNamedExports({
page: appPageToCheck,
distDir,
runtimeEnvConfig,
sriEnabled,
})
: Promise.resolve([])
// eslint-disable-next-line @typescript-eslint/no-shadow
let isNextImageImported: boolean | undefined
@@ -2026,10 +2050,8 @@ export default async function build(
}
}
const pageFilePath = isAppBuiltinNotFoundPage(pagePath)
? require.resolve(
'next/dist/client/components/builtin/not-found'
)
const pageFilePath = isAppBuiltinPage(pagePath)
? pagePath
: path.join(
(pageType === 'pages' ? pagesDir : appDir) || '',
pagePath
@@ -2739,13 +2761,18 @@ export default async function build(
}
})
const hasPages500 = usedStaticStatusPages.includes('/500')
const hasPages500 = !appDirOnly && usedStaticStatusPages.includes('/500')
const useDefaultStatic500 =
!hasPages500 && !hasNonStaticErrorPage && !customAppGetInitialProps
const combinedPages = [...staticPages, ...ssgPages]
const isApp404Static = staticPaths.has(UNDERSCORE_NOT_FOUND_ROUTE_ENTRY)
const hasStaticApp404 = hasApp404 && isApp404Static
const isAppGlobalErrorStatic = staticPaths.has(
UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY
)
const hasStaticAppGlobalError =
hasAppGlobalError && isAppGlobalErrorStatic
await updateBuildDiagnostics({
buildStage: 'static-generation',
@@ -2833,13 +2860,13 @@ export default async function build(
})
})
if (useStaticPages404) {
if (useStaticPages404 && !appDirOnly) {
defaultMap['/404'] = {
page: hasPages404 ? '/404' : '/_error',
}
}
if (useDefaultStatic500) {
if (useDefaultStatic500 && !appDirOnly) {
defaultMap['/500'] = {
page: '/_error',
}
@@ -2926,6 +2953,7 @@ export default async function build(
outdir,
statusMessage: 'Generating static pages',
numWorkers: getNumberOfWorkers(exportConfig),
appDirOnly,
},
nextBuildSpan
)
@@ -3551,6 +3579,13 @@ export default async function build(
.replace(/\\/g, '/')
if (existsSync(orig)) {
// if 404.html folder doesn't exist, create it
await fs.mkdir(
path.dirname(
path.join(distDir, 'server', updatedRelativeDest)
),
{ recursive: true }
)
await fs.copyFile(
orig,
path.join(distDir, 'server', updatedRelativeDest)
@@ -3570,20 +3605,73 @@ export default async function build(
})
}
async function moveExportedAppGlobalErrorTo500() {
return staticGenerationSpan
.traceChild('move-exported-app-global-error-')
.traceAsyncFn(async () => {
// If static 500.html exists in pages router, don't move it
if (
existsSync(path.join(distDir, 'server', 'pages', '500.html'))
) {
return
}
// Only handle 500.html generation for static export
const orig = path.join(
distDir,
'server',
'app',
'_global-error.html'
)
if (existsSync(orig)) {
const error500Html = path.join(
distDir,
'server',
'pages',
'500.html'
)
// if 500.html folder doesn't exist, create it
await fs.mkdir(path.dirname(error500Html), {
recursive: true,
})
await fs.copyFile(orig, error500Html)
pagesManifest['/500'] = path
.join('pages', '500.html')
.replace(/\\/g, '/')
}
})
}
// If there's /not-found inside app, we prefer it over the pages 404
if (hasStaticApp404) {
await moveExportedAppNotFoundTo404()
} else {
// Only move /404 to /404 when there is no custom 404 as in that case we don't know about the 404 page
if (!hasPages404 && !hasApp404 && useStaticPages404) {
if (
!hasPages404 &&
!hasApp404 &&
useStaticPages404 &&
!appDirOnly
) {
await moveExportedPage('/_error', '/404', '/404', false, 'html')
}
}
if (useDefaultStatic500) {
if (useDefaultStatic500 && !appDirOnly) {
await moveExportedPage('/_error', '/500', '/500', false, 'html')
}
// If there's app router and no pages router, use app router built-in 500.html
if (
hasStaticAppGlobalError &&
mappedAppPages &&
Object.keys(mappedAppPages).length > 0
) {
await moveExportedAppGlobalErrorTo500()
}
for (const page of combinedPages) {
const isSsg = ssgPages.has(page)
const isStaticSsgFallback = ssgStaticFallbackPages.has(page)
@@ -3937,7 +4025,8 @@ export default async function build(
dir,
enabledDirectories,
configOutDir,
nextBuildSpan
nextBuildSpan,
appDirOnly
)
}
@@ -179,14 +179,19 @@ export async function turbopackBuild(): Promise<{
await Promise.all(promises)
await Promise.all([
manifestLoader.loadBuildManifest('_app'),
manifestLoader.loadPagesManifest('_app'),
manifestLoader.loadFontManifest('_app'),
manifestLoader.loadPagesManifest('_document'),
manifestLoader.loadClientBuildManifest('_error'),
manifestLoader.loadBuildManifest('_error'),
manifestLoader.loadPagesManifest('_error'),
manifestLoader.loadFontManifest('_error'),
// Only load pages router manifests if not app-only
...(!appDirOnly
? [
manifestLoader.loadBuildManifest('_app'),
manifestLoader.loadPagesManifest('_app'),
manifestLoader.loadFontManifest('_app'),
manifestLoader.loadPagesManifest('_document'),
manifestLoader.loadClientBuildManifest('_error'),
manifestLoader.loadBuildManifest('_error'),
manifestLoader.loadPagesManifest('_error'),
manifestLoader.loadFontManifest('_error'),
]
: []),
entrypoints.instrumentation &&
manifestLoader.loadMiddlewareManifest(
'instrumentation',
+32 -5
View File
@@ -49,6 +49,8 @@ import {
} from '../lib/constants'
import {
MODERN_BROWSERSLIST_TARGET,
UNDERSCORE_GLOBAL_ERROR_ROUTE,
UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY,
UNDERSCORE_NOT_FOUND_ROUTE,
} from '../shared/lib/constants'
import prettyBytes from '../lib/pretty-bytes'
@@ -318,8 +320,13 @@ const filterAndSortList = (
) => {
let pages: string[]
if (routeType === 'app') {
// filter out static app route of /favicon.ico
pages = list.filter((e) => e !== '/favicon.ico')
// filter out static app route of /favicon.ico and /_global-error
pages = list.filter((e) => {
if (e === '/favicon.ico') return false
// Hide static /_global-error from build output
if (e === '/_global-error') return false
return true
})
} else {
// filter built-in pages
pages = list
@@ -1067,6 +1074,23 @@ export async function isPageStatic({
buildId: string
sriEnabled: boolean
}): Promise<PageIsStaticResult> {
// Skip page data collection for synthetic _global-error routes
if (page === UNDERSCORE_GLOBAL_ERROR_ROUTE) {
return {
isStatic: true,
isRoutePPREnabled: false,
isHybridAmp: false,
isAmpOnly: false,
prerenderFallbackMode: undefined,
prerenderedRoutes: undefined,
rootParamKeys: undefined,
hasStaticProps: false,
hasServerProps: false,
isNextImageImported: false,
appConfig: {},
}
}
await createIncrementalCache({
cacheHandler,
cacheHandlers,
@@ -1159,7 +1183,10 @@ export async function isPageStatic({
})
}
appConfig = reduceAppConfig(segments)
appConfig =
originalAppPath === UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY
? {}
: reduceAppConfig(segments)
if (appConfig.dynamic === 'force-static' && pathIsEdgeRuntime) {
Log.warn(
@@ -1755,8 +1782,8 @@ export function isReservedPage(page: string) {
return RESERVED_PAGE.test(page)
}
export function isAppBuiltinNotFoundPage(page: string) {
return /next[\\/]dist[\\/]client[\\/]components[\\/]builtin[\\/](not-found|global-not-found)/.test(
export function isAppBuiltinPage(page: string) {
return /next[\\/]dist[\\/](esm[\\/])?client[\\/]components[\\/]builtin[\\/]/.test(
page
)
}
@@ -1,9 +1,13 @@
import type webpack from 'next/dist/compiled/webpack/webpack'
import {
UNDERSCORE_GLOBAL_ERROR_ROUTE,
UNDERSCORE_NOT_FOUND_ROUTE,
UNDERSCORE_NOT_FOUND_ROUTE_ENTRY,
type ValueOf,
} from '../../../../shared/lib/constants'
import {
UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY,
UNDERSCORE_NOT_FOUND_ROUTE_ENTRY,
} from '../../../../shared/lib/entry-constants'
import type { ModuleTuple, CollectedMetadata } from '../metadata/types'
import path from 'path'
@@ -21,7 +25,7 @@ import { isAppRouteRoute } from '../../../../lib/is-app-route-route'
import type { NextConfig } from '../../../../server/config-shared'
import { AppPathnameNormalizer } from '../../../../server/normalizers/built/app/app-pathname-normalizer'
import type { MiddlewareConfig } from '../../../analysis/get-page-static-info'
import { isAppBuiltinNotFoundPage } from '../../../utils'
import { isAppBuiltinPage } from '../../../utils'
import { loadEntrypoint } from '../../../load-entrypoint'
import {
isGroupSegment,
@@ -85,6 +89,7 @@ const defaultNotFoundPath = 'next/dist/client/components/builtin/not-found.js'
const defaultLayoutPath = 'next/dist/client/components/builtin/layout.js'
const defaultGlobalNotFoundPath =
'next/dist/client/components/builtin/global-not-found.js'
const appErrorPath = 'next/dist/client/components/builtin/app-error.js'
type DirResolver = (pathToResolve: string) => string
type PathResolver = (
@@ -153,7 +158,8 @@ async function createTreeCodeFromPath(
}> {
const splittedPath = pagePath.split(/[\\/]/, 1)
const isNotFoundRoute = page === UNDERSCORE_NOT_FOUND_ROUTE_ENTRY
const isDefaultNotFound = isAppBuiltinNotFoundPage(pagePath)
const isAppErrorRoute = page === UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY
const isDefaultNotFound = isAppBuiltinPage(pagePath)
const appDirPrefix = isDefaultNotFound ? APP_DIR_ALIAS : splittedPath[0]
const pages: string[] = []
@@ -439,7 +445,7 @@ async function createTreeCodeFromPath(
const varName = `notFound${nestedCollectedDeclarations.length}`
nestedCollectedDeclarations.push([varName, notFoundPath])
subtreeCode = `{
children: [${JSON.stringify(UNDERSCORE_NOT_FOUND_ROUTE)}, {
children: [${JSON.stringify(UNDERSCORE_NOT_FOUND_ROUTE.slice(1))}, {
children: ['${PAGE_SEGMENT_KEY}', {}, {
page: [
${varName},
@@ -452,6 +458,22 @@ async function createTreeCodeFromPath(
}
}
// If it's app-error route, set app-error as children page
if (isAppErrorRoute) {
const varName = `appError${nestedCollectedDeclarations.length}`
nestedCollectedDeclarations.push([varName, appErrorPath])
subtreeCode = `{
children: [${JSON.stringify(UNDERSCORE_GLOBAL_ERROR_ROUTE.slice(1))}, {
children: ['${PAGE_SEGMENT_KEY}', {}, {
page: [
${varName},
${JSON.stringify(appErrorPath)}
]
}]
}, {}]
}`
}
// For 404 route
// if global-not-found is in definedFilePaths, remove root layout for /_not-found
// TODO: remove this once global-not-found is stable.
@@ -461,6 +483,12 @@ async function createTreeCodeFromPath(
)
}
if (isAppErrorRoute) {
definedFilePaths = definedFilePaths.filter(
([type]) => type !== 'layout'
)
}
const modulesCode = `{
${definedFilePaths
.map(([file, filePath]) => {
@@ -780,7 +808,9 @@ const nextAppLoader: AppLoader = async function nextAppLoader() {
!!treeCodeResult.globalNotFound &&
isGlobalNotFoundEnabled
if (!treeCodeResult.rootLayout && !isGlobalNotFoundPath) {
const isAppErrorRoute = page === UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY
if (!treeCodeResult.rootLayout && !isGlobalNotFoundPath && !isAppErrorRoute) {
if (!isDev) {
// If we're building and missing a root layout, exit the build
Log.error(
@@ -121,18 +121,24 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
this.context || this.rootContext,
absolutePagePath
)
const appPath = this.utils.contextify(
this.context || this.rootContext,
swapDistFolderWithEsmDistFolder(absoluteAppPath)
)
const errorPath = this.utils.contextify(
this.context || this.rootContext,
swapDistFolderWithEsmDistFolder(absoluteErrorPath)
)
const documentPath = this.utils.contextify(
this.context || this.rootContext,
swapDistFolderWithEsmDistFolder(absoluteDocumentPath)
)
const appPath = absoluteAppPath
? this.utils.contextify(
this.context || this.rootContext,
swapDistFolderWithEsmDistFolder(absoluteAppPath)
)
: ''
const errorPath = absoluteErrorPath
? this.utils.contextify(
this.context || this.rootContext,
swapDistFolderWithEsmDistFolder(absoluteErrorPath)
)
: ''
const documentPath = absoluteDocumentPath
? this.utils.contextify(
this.context || this.rootContext,
swapDistFolderWithEsmDistFolder(absoluteDocumentPath)
)
: ''
const userland500Path = absolute500Path
? this.utils.contextify(
this.context || this.rootContext,
@@ -24,8 +24,11 @@ import {
DEFAULT_RUNTIME_WEBPACK,
EDGE_RUNTIME_WEBPACK,
SERVER_REFERENCE_MANIFEST,
UNDERSCORE_NOT_FOUND_ROUTE_ENTRY,
} from '../../../shared/lib/constants'
import {
UNDERSCORE_NOT_FOUND_ROUTE_ENTRY,
UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY,
} from '../../../shared/lib/entry-constants'
import {
isClientComponentEntryModule,
isCSSMod,
@@ -50,6 +53,7 @@ import {
import type { MetadataRouteLoaderOptions } from '../loaders/next-metadata-route-loader'
import type { FlightActionEntryLoaderActions } from '../loaders/next-flight-action-entry-loader'
import getWebpackBundler from '../../../shared/lib/get-webpack-bundler'
import { isAppBuiltinPage } from '../../utils'
interface Options {
dev: boolean
@@ -325,10 +329,11 @@ export class FlightClientEntryPlugin {
const clientEntriesToInject = []
const mergedCSSimports: CssImports = {}
for (const connection of getModuleReferencesInOrder(
const moduleReferences = getModuleReferencesInOrder(
entryModule,
compilation.moduleGraph
)) {
)
for (const connection of moduleReferences) {
// Entry can be any user defined entry files such as layout, page, error, loading, etc.
let entryRequest = (
connection.dependency as unknown as webpack.NormalModule
@@ -355,13 +360,16 @@ export class FlightClientEntryPlugin {
)
const isAbsoluteRequest = path.isAbsolute(entryRequest)
const isAppRouterBuiltinPage = isAppBuiltinPage(entryRequest)
// Next.js internals are put into a separate entry.
if (!isAbsoluteRequest) {
Object.keys(clientComponentImports).forEach(
(value) => (internalClientComponentEntryImports[value] = new Set())
)
continue
if (!isAppRouterBuiltinPage) {
continue
}
}
// TODO-APP: Enable these lines. This ensures no entrypoint is created for layout/page when there are no client components.
@@ -370,9 +378,10 @@ export class FlightClientEntryPlugin {
// continue
// }
const relativeRequest = isAbsoluteRequest
? path.relative(compilation.options.context!, entryRequest)
: entryRequest
const relativeRequest =
isAbsoluteRequest && !isAppRouterBuiltinPage
? path.relative(compilation.options.context!, entryRequest)
: entryRequest
// Replace file suffix as `.js` will be added.
// bundlePath will have app/ prefix but not src/.
@@ -439,6 +448,17 @@ export class FlightClientEntryPlugin {
absolutePagePath: entryRequest,
})
}
if (name === `app${UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY}`) {
clientEntriesToInject.push({
compiler,
compilation,
entryName: name,
clientComponentImports,
bundlePath: `app${UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY}`,
absolutePagePath: entryRequest,
})
}
}
// Make sure CSS imports are deduplicated before injecting the client entry
+1
View File
@@ -89,6 +89,7 @@ const nextTypegen = async (
pageExtensions: nextConfig.pageExtensions,
pagesDir,
appDir,
appDirOnly: !!appDir && !pagesDir,
})
const validFileMatcher = createValidFileMatcher(
@@ -0,0 +1,80 @@
import React from 'react'
const styles: Record<string, React.CSSProperties> = {
error: {
// https://github.com/sindresorhus/modern-normalize/blob/main/modern-normalize.css#L38-L52
fontFamily:
'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
height: '100vh',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
},
desc: {
lineHeight: '48px',
},
h1: {
display: 'inline-block',
margin: '0 20px 0 0',
paddingRight: 23,
fontSize: 24,
fontWeight: 500,
verticalAlign: 'top',
},
h2: {
fontSize: 14,
fontWeight: 400,
lineHeight: '28px',
},
wrap: {
display: 'inline-block',
},
} as const
/* CSS minified from
body { margin: 0; color: #000; background: #fff; }
.next-error-h1 {
border-right: 1px solid rgba(0, 0, 0, .3);
}
@media (prefers-color-scheme: dark) {
body { color: #fff; background: #000; }
.next-error-h1 {
border-right: 1px solid rgba(255, 255, 255, .3);
}
}
*/
const themeCss = `body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}
@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}`
function AppError() {
const errorMessage = 'Internal Server Error.'
const title = `500: ${errorMessage}`
return (
<html id="__next_error__">
<head>
<title>{title}</title>
</head>
<body>
<div style={styles.error}>
<div style={styles.desc}>
<style
dangerouslySetInnerHTML={{
__html: themeCss,
}}
/>
<h1 className="next-error-h1" style={styles.h1}>
500
</h1>
<div style={styles.wrap}>
<h2 style={styles.h2}>{errorMessage}</h2>
</div>
</div>
</div>
</body>
</html>
)
}
export default AppError
@@ -12,7 +12,9 @@ export function HandleISRError({ error }: { error: any }) {
if (workAsyncStorage) {
const store = workAsyncStorage.getStore()
if (store?.isRevalidate || store?.isStaticGeneration) {
console.error(error)
if (error) {
console.error(error)
}
throw error
}
}
+94 -40
View File
@@ -444,8 +444,15 @@ async function exportAppImpl(
return exportMap
})
// During static export, remove export 404/500 of pages router
// when only app router presents
if (!options.buildExport && options.appDirOnly) {
delete exportPathMap['/404']
delete exportPathMap['/500']
}
// only add missing 404 page when `buildExport` is false
if (!options.buildExport) {
if (!options.buildExport && !options.appDirOnly) {
// only add missing /404 if not specified in `exportPathMap`
if (!exportPathMap['/404']) {
exportPathMap['/404'] = { page: '/_error' }
@@ -455,7 +462,7 @@ async function exportAppImpl(
* exports 404.html for backwards compat
* E.g. GitHub Pages, GitLab Pages, Cloudflare Pages, Netlify
*/
if (!exportPathMap['/404.html']) {
if (!exportPathMap['/404.html'] && exportPathMap['/404']) {
// alias /404.html to /404 to be compatible with custom 404 / _error page
exportPathMap['/404.html'] = exportPathMap['/404']
}
@@ -492,7 +499,9 @@ async function exportAppImpl(
}
if (allExportPaths.length === 0) {
return null
if (!prerenderManifest) {
return null
}
}
if (fallbackEnabledPages.size > 0) {
@@ -640,44 +649,53 @@ async function exportAppImpl(
initialPhaseExportPaths = allExportPaths
}
const progress = createProgress(
initialPhaseExportPaths.length + finalPhaseExportPaths.length,
options.statusMessage || 'Exporting'
)
const totalExportPaths =
initialPhaseExportPaths.length + finalPhaseExportPaths.length
let worker: StaticWorker | null = null
let results: ExportPagesResult = []
const worker = createStaticWorker(nextConfig, {
debuggerPortOffset: getNextBuildDebuggerPortOffset({ kind: 'export-page' }),
progress,
})
const results = await exportPagesInBatches(worker, initialPhaseExportPaths)
if (finalPhaseExportPaths.length > 0) {
const renderResumeDataCachesByPage: Record<string, string> = {}
for (const { page, result } of results) {
if (!result) {
continue
}
if ('renderResumeDataCache' in result && result.renderResumeDataCache) {
// The last RDC for each page is used. We only need one. It should have
// all the entries that the fallback shell also needs. We don't need to
// merge them per page.
renderResumeDataCachesByPage[page] = result.renderResumeDataCache
// Remove the RDC string from the result so that it can be garbage
// collected, when there are more results for the same page.
result.renderResumeDataCache = undefined
}
}
const finalPhaseResults = await exportPagesInBatches(
worker,
finalPhaseExportPaths,
renderResumeDataCachesByPage
if (totalExportPaths > 0) {
const progress = createProgress(
totalExportPaths,
options.statusMessage || 'Exporting'
)
results.push(...finalPhaseResults)
worker = createStaticWorker(nextConfig, {
debuggerPortOffset: getNextBuildDebuggerPortOffset({
kind: 'export-page',
}),
progress,
})
results = await exportPagesInBatches(worker, initialPhaseExportPaths)
if (finalPhaseExportPaths.length > 0) {
const renderResumeDataCachesByPage: Record<string, string> = {}
for (const { page, result } of results) {
if (!result) {
continue
}
if ('renderResumeDataCache' in result && result.renderResumeDataCache) {
// The last RDC for each page is used. We only need one. It should have
// all the entries that the fallback shell also needs. We don't need to
// merge them per page.
renderResumeDataCachesByPage[page] = result.renderResumeDataCache
// Remove the RDC string from the result so that it can be garbage
// collected, when there are more results for the same page.
result.renderResumeDataCache = undefined
}
}
const finalPhaseResults = await exportPagesInBatches(
worker,
finalPhaseExportPaths,
renderResumeDataCachesByPage
)
results.push(...finalPhaseResults)
}
}
let hadValidationError = false
@@ -763,8 +781,41 @@ async function exportAppImpl(
if (!options.buildExport && prerenderManifest) {
await Promise.all(
Object.keys(prerenderManifest.routes).map(async (unnormalizedRoute) => {
// Skip handling /_not-found route, it will copy the 404.html file later
// Special handling: map app /_not-found to 404.html (and 404/index.html when trailingSlash)
if (unnormalizedRoute === '/_not-found') {
const { srcRoute } = prerenderManifest!.routes[unnormalizedRoute]
const appPageName = mapAppRouteToPage.get(srcRoute || '')
const pageName = appPageName || srcRoute || unnormalizedRoute
const isAppPath = Boolean(appPageName)
const route = normalizePagePath(unnormalizedRoute)
const pagePath = getPagePath(pageName, distDir, undefined, isAppPath)
const distPagesDir = join(
pagePath,
pageName
.slice(1)
.split('/')
.map(() => '..')
.join('/')
)
const orig = join(distPagesDir, route)
const htmlSrc = `${orig}.html`
// write 404.html at root
const htmlDest404 = join(outDir, '404.html')
await fs.mkdir(dirname(htmlDest404), { recursive: true })
await fs.copyFile(htmlSrc, htmlDest404)
// When trailingSlash, also write 404/index.html
if (subFolders) {
const htmlDest404Index = join(outDir, '404', 'index.html')
await fs.mkdir(dirname(htmlDest404Index), { recursive: true })
await fs.copyFile(htmlSrc, htmlDest404Index)
}
}
// Skip 500.html in static export
if (unnormalizedRoute === '/_global-error') {
return
}
const { srcRoute } = prerenderManifest!.routes[unnormalizedRoute]
@@ -840,6 +891,7 @@ async function exportAppImpl(
}
const segmentsDir = `${orig}${RSC_SEGMENTS_DIR_SUFFIX}`
if (isAppPath && existsSync(segmentsDir)) {
// Output a data file for each of this page's segments
//
@@ -903,7 +955,9 @@ async function exportAppImpl(
await telemetry.flush()
}
await worker.end()
if (worker) {
await worker.end()
}
return collector
}
+14 -2
View File
@@ -30,6 +30,10 @@ import type { RequestLifecycleOpts } from '../../server/base-server'
import type { AppSharedContext } from '../../server/app-render/app-render'
import type { MultiFileWriter } from '../../lib/multi-file-writer'
import { stringifyResumeDataCache } from '../../server/resume-data-cache/resume-data-cache'
import {
UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY,
UNDERSCORE_NOT_FOUND_ROUTE_ENTRY,
} from '../../shared/lib/entry-constants'
/**
* Renders & exports a page associated with the /app directory
@@ -59,12 +63,17 @@ export async function exportAppPage(
}
let isDefaultNotFound = false
let isDefaultGlobalError = false
// 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/page') {
if (page === UNDERSCORE_NOT_FOUND_ROUTE_ENTRY) {
isDefaultNotFound = true
pathname = '/404'
}
// If the page is `/_global-error`, then we should update the page to be `/500`.
if (page === UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY) {
isDefaultGlobalError = true
pathname = '/500'
}
try {
const result = await lazyRenderAppPage(
@@ -197,6 +206,9 @@ export async function exportAppPage(
if (isDefaultNotFound) {
// Override the default /_not-found page status code to 404
status = 404
} else if (isDefaultGlobalError) {
// Override the default /_global-error page status code to 500
status = 500
} else if (isNonSuccessfulStatusCode && !isParallelRoute) {
// If it's parallel route the status from mock response is 404
status = res.statusCode
+1
View File
@@ -112,6 +112,7 @@ export interface ExportAppOptions {
nextConfig?: NextConfigComplete
hasOutdirFromCli?: boolean
numWorkers: number
appDirOnly: boolean
}
export type ExportPageMetadata = {
+17 -1
View File
@@ -2749,9 +2749,10 @@ export default abstract class Server<
const is404 = res.statusCode === 404
let using404Page = false
const hasAppDir = this.enabledDirectories.app
if (is404) {
if (this.enabledDirectories.app) {
if (hasAppDir) {
// Use the not-found entry in app directory
result = await this.findPageComponents({
locale: getRequestMeta(ctx.req, 'locale'),
@@ -2789,6 +2790,21 @@ export default abstract class Server<
// skip ensuring /500 in dev mode as it isn't used and the
// dev overlay is used instead
if (statusPage !== '/500' || !this.renderOpts.dev) {
if (!result && hasAppDir) {
// Otherwise if app router present, load app router built-in 500 page
result = await this.findPageComponents({
locale: getRequestMeta(ctx.req, 'locale'),
page: statusPage,
query,
params: {},
isAppPath: true,
// Ensuring can't be done here because you never "match" a 500
// route.
shouldEnsure: true,
url: ctx.req.url,
})
}
// If the above App Router result is empty, fallback to pages router 500 page
result = await this.findPageComponents({
locale: getRequestMeta(ctx.req, 'locale'),
page: statusPage,
@@ -650,6 +650,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
),
pagesDir: this.pagesDir,
appDir: this.appDir,
appDirOnly: Boolean(this.appDir && !this.pagesDir),
})
)
@@ -16,6 +16,10 @@ import {
extractInterceptionRouteInformation,
isInterceptionRouteAppPath,
} from '../../../shared/lib/router/utils/interception-routes'
import {
UNDERSCORE_GLOBAL_ERROR_ROUTE,
UNDERSCORE_NOT_FOUND_ROUTE,
} from '../../../shared/lib/entry-constants'
interface RouteInfo {
path: string
@@ -235,11 +239,15 @@ export async function createRouteTypesManifest({
}
}
// Process layout routes
// Process layout routes (exclude internal app error/not-found layouts)
for (const { route, filePath } of layoutRoutes) {
if (
route === UNDERSCORE_GLOBAL_ERROR_ROUTE ||
route === UNDERSCORE_NOT_FOUND_ROUTE
)
continue
// Use the resolved route (for interception routes, this gives us the canonical route)
const resolvedRoute = resolveInterceptingRoute(route)
if (!manifest.layoutRoutes[resolvedRoute]) {
manifest.layoutRoutes[resolvedRoute] = {
path: getRelativePath(filePath),
@@ -256,8 +264,13 @@ export async function createRouteTypesManifest({
}
}
// Process app routes
// Process app routes (exclude internal app routes)
for (const { route, filePath } of appRoutes) {
if (
route === UNDERSCORE_GLOBAL_ERROR_ROUTE ||
route === UNDERSCORE_NOT_FOUND_ROUTE
)
continue
// Don't include metadata routes or pages
if (
!filePath.endsWith('page.ts') &&
+7
View File
@@ -78,6 +78,13 @@ export function getMaybePagePath(
return null
}
// Handle absolute paths (e.g., built-in components)
if (path.isAbsolute(pagePath)) {
// Use the absolute path as-is
pagePathCache?.set(cacheKey, pagePath)
return pagePath
}
pagePath = path.join(serverBuildPath, pagePath)
pagePathCache?.set(cacheKey, pagePath)
+8 -2
View File
@@ -59,8 +59,14 @@ 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`
// Re-export entry constants for backward compatibility
export {
UNDERSCORE_NOT_FOUND_ROUTE,
UNDERSCORE_NOT_FOUND_ROUTE_ENTRY,
UNDERSCORE_GLOBAL_ERROR_ROUTE,
UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY,
} from './entry-constants'
export const PHASE_EXPORT = 'phase-export'
export const PHASE_PRODUCTION_BUILD = 'phase-production-build'
export const PHASE_PRODUCTION_SERVER = 'phase-production-server'
@@ -0,0 +1,4 @@
export const UNDERSCORE_NOT_FOUND_ROUTE = '/_not-found'
export const UNDERSCORE_NOT_FOUND_ROUTE_ENTRY = `${UNDERSCORE_NOT_FOUND_ROUTE}/page`
export const UNDERSCORE_GLOBAL_ERROR_ROUTE = '/_global-error'
export const UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY = `${UNDERSCORE_GLOBAL_ERROR_ROUTE}/page`
@@ -159,7 +159,18 @@ describe('unrecognized server actions', () => {
// FIXME: Currently, an unrecognized id in an MPA action results in a 500.
// This is not ideal, and ignores all nested `error.js` files, only showing the topmost one.
expect(response.status()).toBe(500)
expect(response.headers()['content-type']).toStartWith('text/html')
if (isNextDev) {
expect(response.headers()['content-type']).toStartWith(
'text/html'
)
} else {
const responseText = await response.text()
expect(responseText).toBe('Internal Server Error')
expect(response.headers()['content-type']).toStartWith(
'text/plain'
)
}
// In dev, the 500 page doesn't have any SSR'd html, so it won't show anything without JS.
if (!isNextDev) {
expect(await browser.elementByCss('body').text()).toContain(
@@ -39,6 +39,7 @@ describe('async imports in cacheComponents', () => {
expect(prerenderedRoutes).toMatchInlineSnapshot(`
[
"/_global-error",
"/_not-found",
"/inside-render/client/async-module",
"/inside-render/client/sync-module",
@@ -24,6 +24,7 @@ const isPPREnabled = process.env.__NEXT_EXPERIMENTAL_PPR === 'true'
const staticRoutes = prerenderManifest.routes
expect(Object.keys(staticRoutes).sort()).toEqual([
'/',
'/_global-error',
'/_not-found',
'/suspenseful/static',
])
@@ -22,6 +22,7 @@ const isPPREnabled = process.env.__NEXT_EXPERIMENTAL_PPR === 'true'
const staticRoutes = prerenderManifest.routes
expect(Object.keys(staticRoutes).sort()).toEqual([
'/',
'/_global-error',
'/_not-found',
'/slow/static',
'/suspenseful/static',
@@ -0,0 +1,25 @@
module.exports = {
basePath: '/basepath',
i18n: {
locales: ['en', 'sv', 'nl'],
defaultLocale: 'en',
},
async rewrites() {
return {
beforeFiles: [
{
source: '/:locale/rewrite-files/:path*',
destination: '/:path*',
locale: false,
},
{
source: '/:locale/rewrite-api/:path*',
destination: '/api/:path*',
locale: false,
},
],
afterFiles: [],
fallback: [],
}
},
}
@@ -0,0 +1,3 @@
export default function handler(req, res) {
res.send('hello from api')
}
@@ -0,0 +1 @@
hello from file.txt
@@ -1,5 +1,4 @@
import { createNext } from 'e2e-utils'
import { NextInstance } from 'e2e-utils'
import { nextTestSetup } from 'e2e-utils'
import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils'
import path from 'path'
import fs from 'fs-extra'
@@ -7,46 +6,14 @@ import fs from 'fs-extra'
const locales = ['', '/en', '/sv', '/nl']
describe('i18n-ignore-rewrite-source-locale with basepath', () => {
let next: NextInstance
beforeAll(async () => {
next = await createNext({
files: {
'pages/api/hello.js': `
export default function handler(req, res) {
res.send('hello from api')
}`,
'public/file.txt': 'hello from file.txt',
},
dependencies: {},
nextConfig: {
basePath: '/basepath',
i18n: {
locales: ['en', 'sv', 'nl'],
defaultLocale: 'en',
},
async rewrites() {
return {
beforeFiles: [
{
source: '/:locale/rewrite-files/:path*',
destination: '/:path*',
locale: false,
},
{
source: '/:locale/rewrite-api/:path*',
destination: '/api/:path*',
locale: false,
},
],
afterFiles: [],
fallback: [],
}
},
},
})
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
})
afterAll(() => next.destroy())
if (skipped) {
return
}
test.each(locales)(
'get public file by skipping locale in rewrite, locale: %s',
@@ -42,6 +42,8 @@ export const expectedWhenTrailingSlashTrue = [
? ['_next/static/test-build-id/_clientMiddlewareManifest.json']
: []),
'_next/static/test-build-id/_ssgManifest.js',
'_not-found/index.html',
'_not-found/index.txt',
'another/first/index.html',
'another/first/index.txt',
'another/index.html',
@@ -72,6 +74,8 @@ const expectedWhenTrailingSlashFalse = [
? ['_next/static/test-build-id/_clientMiddlewareManifest.json']
: []),
'_next/static/test-build-id/_ssgManifest.js',
'_not-found.html',
'_not-found.txt',
'another.html',
'another.txt',
'another/first.html',
@@ -21,5 +21,5 @@
"strictNullChecks": true
},
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx"]
}
@@ -0,0 +1,64 @@
import { nextTestSetup } from 'e2e-utils'
import fsp from 'fs/promises'
import path from 'path'
describe('500-page app-router-only', () => {
const { next, isNextStart, isTurbopack, isNextDeploy } = nextTestSetup({
files: __dirname,
})
if (isNextDeploy) {
it('should render app router 500 page when route error', async () => {
const $ = await next.render$('/route-error')
expect($('html').attr('id')).toBe('__next_error__')
expect($('body').text()).toContain('Internal Server Error.')
expect($('body').text()).toContain('500')
})
}
if (isNextStart) {
it('should use app router to generate 500.html when no pages _error.tsx exists', async () => {
const html = await fsp.readFile(
path.join(next.testDir, '.next', 'server', 'pages', '500.html'),
'utf8'
)
// Not use pages router to generate 500.html
expect(html).toContain('__next_error__')
expect(html).toContain('Internal Server Error.')
// global-error is not used in app router 500.html
expect(html).not.toContain('app-router-global-error')
})
it('pages manifest should only contain 404 and 500', async () => {
const pagesManifest = await fsp.readFile(
path.join(next.testDir, '.next', 'server', 'pages-manifest.json'),
'utf8'
)
const pagesManifestJson = JSON.parse(pagesManifest)
expect(pagesManifestJson).toMatchInlineSnapshot(`
{
"/404": "pages/404.html",
"/500": "pages/500.html",
}
`)
})
it('should not contain pages router routes default assets', async () => {
// do not contain _app, _document, _error routes folder or files in .next/server/pages
const pagesDir = path.join(next.testDir, '.next', 'server', 'pages')
const files = await fsp.readdir(pagesDir)
expect(files).not.toContain('500')
if (isTurbopack) {
// In turbopack, _app, _document, _error are still generated with manifest, but no javascript files.
// The manifest are under the folder, they're still needed for static generation.
expect(files).not.toContain('_app.js')
expect(files).not.toContain('_document.js')
expect(files).not.toContain('_error.js')
} else {
expect(files).not.toContain('_app')
expect(files).not.toContain('_document')
expect(files).not.toContain('_error')
}
})
}
})
@@ -0,0 +1,5 @@
'use server'
export async function throwErrorAction() {
throw new Error('action error')
}
@@ -0,0 +1,11 @@
'use client'
export default function GlobalError() {
return (
<html>
<body>
<h1>app-router-global-error</h1>
</body>
</html>
)
}
@@ -0,0 +1,11 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
@@ -0,0 +1,3 @@
export default function Page() {
return <p>app page</p>
}
@@ -0,0 +1,6 @@
import { connection } from 'next/server'
export async function GET() {
await connection()
throw new Error('route error')
}
@@ -0,0 +1,8 @@
import { NextRequest, NextResponse } from 'next/server'
export async function middleware(request: NextRequest) {
if (request.nextUrl.pathname === '/middleware-error') {
throw new Error('middleware error')
}
return NextResponse.next()
}
@@ -0,0 +1,3 @@
export default function AppPage() {
return <p>app page</p>
}
@@ -0,0 +1,11 @@
'use client'
export default function GlobalError() {
return (
<html>
<body>
<p>app-router-global-error</p>
</body>
</html>
)
}
@@ -0,0 +1,11 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
@@ -0,0 +1,47 @@
import { nextTestSetup } from 'e2e-utils'
import fsp from 'fs/promises'
import path from 'path'
describe('500-page - mixed-router-no-custom-pages-error', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
})
if (skipped) {
return
}
it('should not use app router global-error for 500.html when no pages _error.tsx exists', async () => {
const $ = await next.render$('/pages-error')
const text = $('#__next').text()
expect(text).toContain('500')
expect(text).toContain('Internal Server Error')
})
it('pages manifest should only contain 404 and 500', async () => {
const pagesManifest = await fsp.readFile(
path.join(next.testDir, '.next', 'server', 'pages-manifest.json'),
'utf8'
)
const pagesManifestJson = JSON.parse(pagesManifest)
expect(pagesManifestJson).toMatchInlineSnapshot(`
{
"/404": "pages/404.html",
"/_app": "pages/_app.js",
"/_document": "pages/_document.js",
"/_error": "pages/_error.js",
"/pages-error": "pages/pages-error.js",
}
`)
})
it('should generate 500.html with pages builtin _error', async () => {
const html = await fsp.readFile(
path.join(next.testDir, '.next', 'server', 'pages', '500.html'),
'utf8'
)
expect(html).toContain('500')
expect(html).toContain('Internal Server Error')
})
})
@@ -0,0 +1,10 @@
export default function PagesPage() {
throw new Error('pages page error')
return <p>pages page</p>
}
export async function getServerSideProps() {
return {
props: {},
}
}
@@ -0,0 +1,3 @@
export default function AppPage() {
return <p>app page</p>
}
@@ -0,0 +1,17 @@
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<p>app-router-global-error</p>
</body>
</html>
)
}
@@ -0,0 +1,11 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
@@ -0,0 +1,27 @@
import { nextTestSetup } from 'e2e-utils'
import fsp from 'fs/promises'
import path from 'path'
describe('500-page - mixed-router-with-custom-pages-error', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
})
if (skipped) {
return
}
it('should not override 500.html with app router global-error when pages _error.tsx exists', async () => {
const $ = await next.render$('/pages-error')
expect($('#__next').text()).toBe('pages-custom-error')
})
it('should generate 500.html with pages builtin _error', async () => {
const html = await fsp.readFile(
path.join(next.testDir, '.next', 'server', 'pages', '500.html'),
'utf8'
)
expect(html).toContain('pages-custom-error')
})
})
@@ -0,0 +1,3 @@
export default function Error() {
return <p>pages-custom-error</p>
}
@@ -0,0 +1,10 @@
export default function PagesPage() {
throw new Error('pages page error')
return <p>pages page</p>
}
export async function getServerSideProps() {
return {
props: {},
}
}