mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
Skip emitting pages router entries when only app router present (#82444)
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -112,6 +112,7 @@ export interface ExportAppOptions {
|
||||
nextConfig?: NextConfigComplete
|
||||
hasOutdirFromCli?: boolean
|
||||
numWorkers: number
|
||||
appDirOnly: boolean
|
||||
}
|
||||
|
||||
export type ExportPageMetadata = {
|
||||
|
||||
@@ -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') &&
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
+1
@@ -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',
|
||||
])
|
||||
|
||||
+1
@@ -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>
|
||||
)
|
||||
}
|
||||
+47
@@ -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>
|
||||
)
|
||||
}
|
||||
+27
@@ -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: {},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user