Skip to content

Commit 33477b7

Browse files
authored
[segment-explorer] display the builtin conventions (#80961)
Track built-in conventions in segment explorer. The default `global-not-found.js` and `global-error.js` will be displayed. Their style will be different to the others, we'll follow up in the later PRs. Closes NEXT-4576
1 parent c1f4e3e commit 33477b7

File tree

17 files changed

+238
-85
lines changed

17 files changed

+238
-85
lines changed

crates/next-core/src/app_structure.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,14 @@ async fn directory_tree_to_loader_tree_internal(
931931
.await?,
932932
);
933933
}
934+
if modules.global_error.is_none() {
935+
modules.global_error = Some(
936+
get_next_package(app_dir)
937+
.join(rcstr!("dist/client/components/builtin/global-error.js"))
938+
.to_resolved()
939+
.await?,
940+
);
941+
}
934942
}
935943

936944
let mut tree = AppPageLoaderTree {

packages/next/src/build/webpack/loaders/next-app-loader/index.ts

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ async function createTreeCodeFromPath(
159159
const pages: string[] = []
160160

161161
let rootLayout: string | undefined
162-
let globalError: string | undefined
163-
let globalNotFound: string | undefined
162+
let globalError: string = defaultGlobalErrorPath
163+
let globalNotFound: string = defaultNotFoundPath
164164

165165
async function resolveAdjacentParallelSegments(
166166
segmentPath: string
@@ -303,6 +303,33 @@ async function createTreeCodeFromPath(
303303
})
304304
)
305305

306+
// Only resolve global-* convention files at the root layer
307+
if (isRootLayer) {
308+
const resolvedGlobalErrorPath = await resolver(
309+
`${appDirPrefix}/${GLOBAL_ERROR_FILE_TYPE}`
310+
)
311+
if (resolvedGlobalErrorPath) {
312+
globalError = resolvedGlobalErrorPath
313+
}
314+
// Add global-error to root layer's filePaths, so that it's always available,
315+
// by default it's the built-in global-error.js
316+
filePaths.push([GLOBAL_ERROR_FILE_TYPE, globalError])
317+
318+
// TODO(global-not-found): remove this flag assertion condition
319+
// once global-not-found is stable
320+
if (isGlobalNotFoundEnabled) {
321+
const resolvedGlobalNotFoundPath = await resolver(
322+
`${appDirPrefix}/${GLOBAL_NOT_FOUND_FILE_TYPE}`
323+
)
324+
if (resolvedGlobalNotFoundPath) {
325+
globalNotFound = resolvedGlobalNotFoundPath
326+
}
327+
// Add global-not-found to root layer's filePaths, so that it's always available,
328+
// by default it's the built-in global-not-found.js
329+
filePaths.push([GLOBAL_NOT_FOUND_FILE_TYPE, globalNotFound])
330+
}
331+
}
332+
306333
let definedFilePaths = filePaths.filter(
307334
([, filePath]) => filePath !== undefined
308335
) as [ValueOf<typeof FILE_TYPES>, string][]
@@ -361,25 +388,6 @@ async function createTreeCodeFromPath(
361388
}
362389
}
363390

364-
if (!globalError) {
365-
const resolvedGlobalErrorPath = await resolver(
366-
`${appDirPrefix}/${GLOBAL_ERROR_FILE_TYPE}`
367-
)
368-
if (resolvedGlobalErrorPath) {
369-
globalError = resolvedGlobalErrorPath
370-
}
371-
}
372-
// TODO(global-not-found): remove this flag assertion condition
373-
// once global-not-found is stable
374-
if (isGlobalNotFoundEnabled && !globalNotFound) {
375-
const resolvedGlobalNotFoundPath = await resolver(
376-
`${appDirPrefix}/${GLOBAL_NOT_FOUND_FILE_TYPE}`
377-
)
378-
if (resolvedGlobalNotFoundPath) {
379-
globalNotFound = resolvedGlobalNotFoundPath
380-
}
381-
}
382-
383391
let parallelSegmentKey = Array.isArray(parallelSegment)
384392
? parallelSegment[0]
385393
: parallelSegment
@@ -523,8 +531,8 @@ async function createTreeCodeFromPath(
523531
treeCode: `${treeCode}.children;`,
524532
pages: `${JSON.stringify(pages)};`,
525533
rootLayout,
526-
globalError: globalError ?? defaultGlobalErrorPath,
527-
globalNotFound: globalNotFound ?? defaultNotFoundPath,
534+
globalError,
535+
globalNotFound,
528536
}
529537
}
530538

packages/next/src/server/app-render/app-render.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,7 @@ import { getRequiredScripts } from './required-scripts'
9797
import { addPathPrefix } from '../../shared/lib/router/utils/add-path-prefix'
9898
import { makeGetServerInsertedHTML } from './make-get-server-inserted-html'
9999
import { walkTreeWithFlightRouterState } from './walk-tree-with-flight-router-state'
100-
import {
101-
createComponentTree,
102-
getRootParams,
103-
normalizeConventionFilePath,
104-
} from './create-component-tree'
100+
import { createComponentTree, getRootParams } from './create-component-tree'
105101
import { getAssetQueryString } from './get-asset-query-string'
106102
import {
107103
getServerModuleMap,
@@ -200,6 +196,7 @@ import {
200196
} from './module-loading/track-module-loading.external'
201197
import { isReactLargeShellError } from './react-large-shell-error'
202198
import type { GlobalErrorComponent } from '../../client/components/builtin/global-error'
199+
import { normalizeConventionFilePath } from './segment-explorer-path'
203200

204201
export type GetDynamicParamFromSegment = (
205202
// [slug] / [[slug]] / [...slug]

packages/next/src/server/app-render/create-component-tree.tsx

Lines changed: 1 addition & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type {
2626
UseCachePageComponentProps,
2727
} from '../use-cache/use-cache-wrapper'
2828
import { DEFAULT_SEGMENT_KEY } from '../../shared/lib/segment'
29+
import { getConventionPathByType } from './segment-explorer-path'
2930

3031
/**
3132
* Use the provided loader tree to create the React Component tree.
@@ -1102,53 +1103,3 @@ async function createBoundaryConventionElement({
11021103

11031104
return wrappedElement
11041105
}
1105-
1106-
export function normalizeConventionFilePath(
1107-
projectDir: string,
1108-
conventionPath: string | undefined
1109-
) {
1110-
const cwd = process.env.NEXT_RUNTIME === 'edge' ? '' : process.cwd()
1111-
const nextInternalPrefixRegex =
1112-
/^(.*[\\/])?next[\\/]dist[\\/]client[\\/]components[\\/]builtin[\\/]/
1113-
1114-
let relativePath = (conventionPath || '')
1115-
// remove turbopack [project] prefix
1116-
.replace(/^\[project\][\\/]/, '')
1117-
// remove the project root from the path
1118-
.replace(projectDir, '')
1119-
// remove cwd prefix
1120-
.replace(cwd, '')
1121-
// remove /(src/)?app/ dir prefix
1122-
.replace(/^([\\/])*(src[\\/])?app[\\/]/, '')
1123-
1124-
// If it's internal file only keep the filename, strip nextjs internal prefix
1125-
if (nextInternalPrefixRegex.test(relativePath)) {
1126-
relativePath = relativePath.replace(nextInternalPrefixRegex, '')
1127-
}
1128-
1129-
return relativePath
1130-
}
1131-
1132-
function getConventionPathByType(
1133-
tree: LoaderTree,
1134-
dir: string,
1135-
conventionType:
1136-
| 'layout'
1137-
| 'template'
1138-
| 'page'
1139-
| 'not-found'
1140-
| 'error'
1141-
| 'loading'
1142-
| 'forbidden'
1143-
| 'unauthorized'
1144-
| 'defaultPage'
1145-
) {
1146-
const modules = tree[2]
1147-
const conventionPath = modules[conventionType]
1148-
? modules[conventionType][1]
1149-
: undefined
1150-
if (conventionPath) {
1151-
return normalizeConventionFilePath(dir, conventionPath)
1152-
}
1153-
return undefined
1154-
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { LoaderTree } from '../lib/app-dir-module'
2+
3+
export function normalizeConventionFilePath(
4+
projectDir: string,
5+
conventionPath: string | undefined
6+
) {
7+
const cwd = process.env.NEXT_RUNTIME === 'edge' ? '' : process.cwd()
8+
const nextInternalPrefixRegex =
9+
/^(.*[\\/])?next[\\/]dist[\\/]client[\\/]components[\\/]builtin[\\/]/
10+
11+
let relativePath = (conventionPath || '')
12+
// remove turbopack [project] prefix
13+
.replace(/^\[project\][\\/]/, '')
14+
// remove the project root from the path
15+
.replace(projectDir, '')
16+
// remove cwd prefix
17+
.replace(cwd, '')
18+
// remove /(src/)?app/ dir prefix
19+
.replace(/^([\\/])*(src[\\/])?app[\\/]/, '')
20+
21+
// If it's internal file only keep the filename, strip nextjs internal prefix
22+
if (nextInternalPrefixRegex.test(relativePath)) {
23+
relativePath = relativePath.replace(nextInternalPrefixRegex, '')
24+
}
25+
26+
return relativePath
27+
}
28+
29+
export function getConventionPathByType(
30+
tree: LoaderTree,
31+
dir: string,
32+
conventionType:
33+
| 'layout'
34+
| 'template'
35+
| 'page'
36+
| 'not-found'
37+
| 'error'
38+
| 'loading'
39+
| 'forbidden'
40+
| 'unauthorized'
41+
| 'defaultPage'
42+
) {
43+
const modules = tree[2]
44+
const conventionPath = modules[conventionType]
45+
? modules[conventionType][1]
46+
: undefined
47+
if (conventionPath) {
48+
return normalizeConventionFilePath(dir, conventionPath)
49+
}
50+
return undefined
51+
}

test/development/app-dir/segment-explorer/app/global-error.tsx renamed to test/development/app-dir/segment-explorer-globals/app/global-error.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
export default function GlobalError() {
44
return (
55
<html>
6-
<head>
7-
<title>Global Error</title>
8-
</head>
96
<body>
107
<h1>Global Error</h1>
8+
<p>This is a global error</p>
119
</body>
1210
</html>
1311
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export default function GlobalNotFound() {
2+
return (
3+
<html>
4+
<body>
5+
<h1>404</h1>
6+
<p>Not Found Page</p>
7+
</body>
8+
</html>
9+
)
10+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const metadata = {
2+
title: 'Next.js',
3+
description: 'Generated by Next.js',
4+
}
5+
6+
export default function RootLayout({
7+
children,
8+
}: {
9+
children: React.ReactNode
10+
}) {
11+
return (
12+
<html lang="en">
13+
<body>{children}</body>
14+
</html>
15+
)
16+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import Link from 'next/link'
2+
3+
const hrefs = [
4+
'/parallel-routes',
5+
'/blog/~/grid',
6+
'/soft-navigation/a',
7+
'/file-segments',
8+
'/parallel-default',
9+
'/parallel-default/subroute',
10+
]
11+
12+
export default function Page() {
13+
return (
14+
<div>
15+
<h1>Segment Explorer</h1>
16+
<p>Examples</p>
17+
<div>
18+
{hrefs.map((href) => (
19+
<div key={href}>
20+
<Link href={href}>{href}</Link>
21+
</div>
22+
))}
23+
</div>
24+
</div>
25+
)
26+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.error-title {
2+
color: red;
3+
font-size: 24px;
4+
font-weight: bold;
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use client'
2+
3+
import './error.css'
4+
5+
export default function CustomError() {
6+
return <h2 className="error-title">Custom Error</h2>
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use client'
2+
3+
export default function Page() {
4+
if (typeof window !== 'undefined') {
5+
throw new Error('Error on client')
6+
}
7+
return <p>page</p>
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use client'
2+
3+
export default function Page() {
4+
if (typeof window !== 'undefined') {
5+
throw new Error('Error on client')
6+
}
7+
return <p>page</p>
8+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @type {import('next').NextConfig}
3+
*/
4+
const nextConfig = {
5+
experimental: {
6+
devtoolSegmentExplorer: true,
7+
authInterrupts: true,
8+
globalNotFound: true,
9+
},
10+
}
11+
12+
module.exports = nextConfig
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
import { openDevToolsIndicatorPopover } from 'next-test-utils'
3+
import { Playwright } from 'next-webdriver'
4+
5+
async function getSegmentExplorerContent(browser: Playwright) {
6+
// open the devtool button
7+
await openDevToolsIndicatorPopover(browser)
8+
9+
// open the segment explorer
10+
await browser.elementByCss('[data-segment-explorer]').click()
11+
12+
// wait for the segment explorer to be visible
13+
await browser.waitForElementByCss('[data-nextjs-devtool-segment-explorer]')
14+
15+
const content = await browser.elementByCss(
16+
'[data-nextjs-devtool-segment-explorer]'
17+
)
18+
return content.text()
19+
}
20+
21+
describe('segment-explorer - globals', () => {
22+
const { next } = nextTestSetup({
23+
files: __dirname,
24+
})
25+
26+
it('should show global-error segment', async () => {
27+
const browser = await next.browser('/runtime-error')
28+
expect(await getSegmentExplorerContent(browser)).toMatchInlineSnapshot(`
29+
"app/
30+
global-error.tsx"
31+
`)
32+
})
33+
34+
it('should display parallel routes default page when present', async () => {
35+
const browser = await next.browser('/404-not-found')
36+
expect(await getSegmentExplorerContent(browser)).toMatchInlineSnapshot(`
37+
"app/
38+
global-not-found.tsx"
39+
`)
40+
})
41+
})

0 commit comments

Comments
 (0)