Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 39 commits into
base: canary
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
67051be
renaming
huozhi Aug 5, 2025
d0c0941
handle skip moving the pages when no custom pages present
huozhi Aug 5, 2025
6eb4731
generate app router 500.html
huozhi Aug 5, 2025
1886aa7
static gen and add test
huozhi Aug 6, 2025
657db6d
reuse global-error
huozhi Aug 6, 2025
98c6bbe
use app-error tos simplify page
huozhi Aug 6, 2025
68bc2c4
filter out in the log
huozhi Aug 6, 2025
5c1fba9
turbopack handling
huozhi Aug 7, 2025
7975881
fix move page path
huozhi Aug 7, 2025
f3b1972
handle empty entry
huozhi Aug 8, 2025
6c9e4de
fix empty entries
huozhi Aug 8, 2025
aa762f8
fix client entry
huozhi Aug 8, 2025
96caaa9
fix existing test
huozhi Aug 8, 2025
a00aa67
fix export case
huozhi Aug 8, 2025
16c580f
fix export case
huozhi Aug 8, 2025
91b1a5b
fix rewrites-with-basepath.test.ts
huozhi Aug 8, 2025
d9a6745
fix build
huozhi Aug 8, 2025
a7a697b
condition
huozhi Aug 8, 2025
1a86140
fix entries
huozhi Aug 9, 2025
c64dc6e
fix i18n and tests
huozhi Aug 9, 2025
74db0c9
fix build error but export output still missing
huozhi Aug 9, 2025
4e25698
update test
huozhi Aug 9, 2025
c8eb0c7
fix export and app only
huozhi Aug 9, 2025
677eca2
fix tests
huozhi Aug 9, 2025
8a2194d
tweak turbopack entry
huozhi Aug 9, 2025
106e600
fix app paths in pages mapping
huozhi Aug 9, 2025
efd5943
fix entries
huozhi Aug 9, 2025
b4a395d
enhance entry cond and fix tests
huozhi Aug 9, 2025
12863c8
reverts few changes and fix tests
huozhi Aug 9, 2025
bd70791
revert
huozhi Aug 9, 2025
3af3e04
revert export cond
huozhi Aug 10, 2025
55a35a3
fix app dir only flag
huozhi Aug 10, 2025
280f932
remove bad _error pages from default export map
huozhi Aug 10, 2025
bbe8137
fix turbopack build global-error entry
huozhi Aug 10, 2025
a89725b
rm
huozhi Aug 10, 2025
8912a29
fix 404 export
huozhi Aug 10, 2025
8731ba4
skip rm not-found file
huozhi Aug 10, 2025
9edcc8e
fix trailing slash false test
huozhi Aug 10, 2025
e920b68
revert orig check for i18n
huozhi Aug 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
static gen and add test
  • Loading branch information
huozhi committed Aug 9, 2025
commit 1886aa7f568b958d08b2f992e23c3ea9f09bbd2b
2 changes: 1 addition & 1 deletion packages/next/src/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ export async function createPagesMapping({
// If there's any custom /_global-error page, it will override the default one.
...(hasAppPages && {
[UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY]: require.resolve(
'next/dist/client/components/builtin/global-error'
'next/dist/esm/client/components/builtin/app-error'
),
}),
...pages,
Expand Down
4 changes: 3 additions & 1 deletion packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1760,7 +1760,9 @@ export function isReservedPage(page: string) {
}

export function isAppBuiltinPage(page: string) {
return /next[\\/]dist[\\/]client[\\/]components[\\/]builtin[\\/]/.test(page)
return /next[\\/]dist[\\/](esm[\\/])?client[\\/]components[\\/]builtin[\\/]/.test(
page
)
}

export function isCustomErrorPage(page: string) {
Expand Down
39 changes: 36 additions & 3 deletions packages/next/src/build/webpack/loaders/next-app-loader/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type webpack from 'next/dist/compiled/webpack/webpack'
import {
UNDERSCORE_GLOBAL_ERROR_ROUTE,
UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY,
UNDERSCORE_NOT_FOUND_ROUTE,
UNDERSCORE_NOT_FOUND_ROUTE_ENTRY,
type ValueOf,
Expand Down Expand Up @@ -81,6 +83,8 @@ const PARALLEL_VIRTUAL_SEGMENT = 'slot$'

const defaultGlobalErrorPath =
'next/dist/client/components/builtin/global-error.js'
const defaultAppErrorPath =
'next/dist/esm/client/components/builtin/app-error.js'
const defaultNotFoundPath = 'next/dist/client/components/builtin/not-found.js'
const defaultLayoutPath = 'next/dist/client/components/builtin/layout.js'
const defaultGlobalNotFoundPath =
Expand Down Expand Up @@ -152,15 +156,20 @@ async function createTreeCodeFromPath(
globalNotFound: string
}> {
const splittedPath = pagePath.split(/[\\/]/, 1)
// const baseName = splittedPath[splittedPath.length - 1]
const isNotFoundRoute = page === UNDERSCORE_NOT_FOUND_ROUTE_ENTRY
const isDefaultNotFound = isAppBuiltinPage(pagePath)
const isAppErrorRoute = page === UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY
const isDefaultNotFound = isAppBuiltinPage(pagePath) // && (baseName === 'global-not-found' || baseName === 'not-found')
// const isDefaultAppError = isAppBuiltinPage(pagePath) // && (baseName === 'app-error')
// console.log('isDefaultAppError', isDefaultAppError, 'page', page, 'baseName', baseName)

const appDirPrefix = isDefaultNotFound ? APP_DIR_ALIAS : splittedPath[0]
const pages: string[] = []

let rootLayout: string | undefined
let globalError: string = defaultGlobalErrorPath
let globalNotFound: string = defaultNotFoundPath
let appError: string = defaultAppErrorPath

async function resolveAdjacentParallelSegments(
segmentPath: string
Expand Down Expand Up @@ -310,6 +319,7 @@ async function createTreeCodeFromPath(
)
if (resolvedGlobalErrorPath) {
globalError = resolvedGlobalErrorPath
appError = resolvedGlobalErrorPath
}
// Add global-error to root layer's filePaths, so that it's always available,
// by default it's the built-in global-error.js
Expand Down Expand Up @@ -436,7 +446,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},
Expand All @@ -448,6 +458,21 @@ 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, appError])
subtreeCode = `{
children: [${JSON.stringify(UNDERSCORE_GLOBAL_ERROR_ROUTE.slice(1))}, {
children: ['${PAGE_SEGMENT_KEY}', {}, {
page: [
${varName},
${JSON.stringify(appError)}
]
}]
}, {}]
}`
}

// For 404 route
// if global-not-found is in definedFilePaths, remove root layout for /_not-found
Expand All @@ -458,6 +483,12 @@ async function createTreeCodeFromPath(
)
}

if (isAppErrorRoute) {
definedFilePaths = definedFilePaths.filter(
([type]) => type !== 'layout'
)
}

const modulesCode = `{
${definedFilePaths
.map(([file, filePath]) => {
Expand Down Expand Up @@ -777,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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
DEFAULT_RUNTIME_WEBPACK,
EDGE_RUNTIME_WEBPACK,
SERVER_REFERENCE_MANIFEST,
UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY,
UNDERSCORE_NOT_FOUND_ROUTE_ENTRY,
} from '../../../shared/lib/constants'
import {
Expand Down Expand Up @@ -426,6 +427,20 @@ export class FlightClientEntryPlugin {
absolutePagePath: entryRequest,
})
}

if (
name === `app${UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY}` &&
bundlePath === 'app/app-error'
) {
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
Expand Down
81 changes: 81 additions & 0 deletions packages/next/src/client/components/builtin/app-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use client'

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',
},
}

function AppError() {
// For static generation, error will be undefined
// For runtime errors, error will contain the actual error
const errorMessage = 'Internal Server Error.'
const title = '500: Internal Server Error'

return (
<html id="__next_error__">
<head>
<title>{title}</title>
</head>
<body>
<div style={styles.error}>
<div style={styles.desc}>
<style
dangerouslySetInnerHTML={{
/* 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);
}
}
*/
__html: `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)}}`,
}}
/>
<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
9 changes: 7 additions & 2 deletions packages/next/src/export/routes/app-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,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') {
isDefaultNotFound = true
pathname = '/404'
}
// If the page is `/_global-error`, generate a static 500 page with fixed content
// If the page is `/_global-error`, then we should update the page to be `/500`.
if (page === '/_global-error/page') {
pathname = '/500'
isDefaultGlobalError = true
// pathname = '/500'
}

try {
Expand Down Expand Up @@ -201,6 +203,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
Expand Down
5 changes: 5 additions & 0 deletions packages/next/src/lib/metadata/resolve-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { ResolveMetadataSpan } from '../../server/lib/trace/constants'
import { PAGE_SEGMENT_KEY } from '../../shared/lib/segment'
import * as Log from '../../build/output/log'
import { createServerParamsForMetadata } from '../../server/request/params'
import { isClientReference } from '../client-and-server-references'

type StaticIcons = Pick<ResolvedIcons, 'icon' | 'apple'>

Expand Down Expand Up @@ -533,6 +534,10 @@ async function collectViewport({
modType = layoutOrPageModType
}

if (isClientReference(mod)) {
return
}

if (modType) {
route += `/${modType}`
}
Expand Down
25 changes: 21 additions & 4 deletions test/e2e/app-dir/global-error/basic/app/global-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,28 @@
export default function GlobalError({ error }) {
return (
<html>
<head></head>
<head>
<title>500: Internal Server Error</title>
</head>
<body>
<h1>Global Error</h1>
<p id="error">{`Global error: ${error?.message}`}</p>
{error?.digest && <p id="digest">{error?.digest}</p>}
<div
style={{
fontFamily: 'system-ui, sans-serif',
height: '100vh',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<h1>500</h1>
<h2>Global Error</h2>
<p id="error">{`Global error: ${error?.message || 'Internal Server Error'}`}</p>
{error?.digest && (
<p id="digest">{error?.digest || 'nextjs-app-error-digest'}</p>
)}
</div>
</body>
</html>
)
Expand Down
22 changes: 22 additions & 0 deletions test/production/500-page/app-router-only/app-router-only.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { nextTestSetup } from 'e2e-utils'
import fsp from 'fs/promises'
import path from 'path'

describe('500-page app-router-only', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
})

if (skipped) {
return
}

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'
)
expect(html).toContain('app-router-global-error')
})
})
11 changes: 11 additions & 0 deletions test/production/500-page/app-router-only/app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client'

export default function GlobalError() {
return (
<html>
<body>
<h1>app-router-global-error</h1>
</body>
</html>
)
}
11 changes: 11 additions & 0 deletions test/production/500-page/app-router-only/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
3 changes: 3 additions & 0 deletions test/production/500-page/app-router-only/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>app page</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function AppPage() {
return <p>app page</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client'

export default function GlobalError() {
return (
<html>
<body>
<p>app-router-global-error</p>
</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
Loading