Skip to content

Commit 700fecf

Browse files
authored
cli: build partial entries --debug-build-paths arg (#85052)
Add `--debug-build-paths` CLI option to next build for selective route building. This enables building only specific routes for faster development and debugging. ### Changes - Add `--debug-build-paths=<patterns>` CLI option - Support comma-separated paths and glob patterns - Filter collected routes after normal file system scanning - Works with both app router and pages router ### Usage #### Build specific routes ```sh next build --debug-build-paths="app/page.tsx,app/dashboard/page.tsx" next build --debug-build-paths="pages/index.tsx,pages/about.tsx" ``` #### Use glob patterns ```sh next build --debug-build-paths "app/**/page.tsx" next build --debug-build-paths "pages/*.tsx" ``` #### Mix both routers ``` next build --debug-build-paths "app/page.tsx,pages/index.tsx" ```
1 parent 09772c3 commit 700fecf

File tree

14 files changed

+340
-5
lines changed

14 files changed

+340
-5
lines changed

packages/next/errors.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -888,5 +888,7 @@
888888
"887": "`cacheLife()` is only available with the `cacheComponents` config.",
889889
"888": "Unknown \\`cacheLife()\\` profile \"%s\" is not configured in next.config.js\\nmodule.exports = {\n cacheLife: {\n \"%s\": ...\\n }\n}",
890890
"889": "Unknown \\`cacheLife()\\` profile \"%s\" is not configured in next.config.js\\nmodule.exports = {\n cacheLife: {\n \"%s\": ...\\n }\n}",
891-
"890": "Received an underlying cookies object that does not match either `cookies` or `mutableCookies`"
891+
"890": "Received an underlying cookies object that does not match either `cookies` or `mutableCookies`",
892+
"891": "Failed to read build paths file \"%s\": %s",
893+
"892": "Failed to resolve glob pattern \"%s\": %s"
892894
}

packages/next/src/bin/next.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ program
154154
'--experimental-next-config-strip-types',
155155
'Use Node.js native TypeScript resolution for next.config.(ts|mts)'
156156
)
157+
.option(
158+
'--debug-build-paths <patterns>',
159+
'Comma-separated glob patterns or explicit paths for selective builds. Examples: "app/*", "app/page.tsx", "app/**/page.tsx"'
160+
)
157161
.action((directory: string, options: NextBuildOptions) => {
158162
if (options.experimentalNextConfigStripTypes) {
159163
process.env.__NEXT_NODE_NATIVE_TS_LOADER_ENABLED = 'true'

packages/next/src/build/index.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -910,7 +910,9 @@ export default async function build(
910910
appDirOnly = false,
911911
bundler = Bundler.Turbopack,
912912
experimentalBuildMode: 'default' | 'compile' | 'generate' | 'generate-env',
913-
traceUploadUrl: string | undefined
913+
traceUploadUrl: string | undefined,
914+
debugBuildAppPaths?: string[],
915+
debugBuildPagePaths?: string[]
914916
): Promise<void> {
915917
const isCompileMode = experimentalBuildMode === 'compile'
916918
const isGenerateMode = experimentalBuildMode === 'generate'
@@ -1166,6 +1168,20 @@ export default async function build(
11661168
.traceAsyncFn(() => collectPagesFiles(pagesDir, validFileMatcher))
11671169
: []
11681170

1171+
// Apply debug build paths filter if specified
1172+
// If debugBuildPagePaths is defined (even if empty), only build specified pages
1173+
if (debugBuildPagePaths !== undefined) {
1174+
if (debugBuildPagePaths.length > 0) {
1175+
const debugPathsSet = new Set(debugBuildPagePaths)
1176+
pagesPaths = pagesPaths.filter((pagePath) =>
1177+
debugPathsSet.has(pagePath)
1178+
)
1179+
} else {
1180+
// Empty array means build no pages
1181+
pagesPaths = []
1182+
}
1183+
}
1184+
11691185
const middlewareDetectionRegExp = new RegExp(
11701186
`^${MIDDLEWARE_FILENAME}\\.(?:${config.pageExtensions.join('|')})$`
11711187
)
@@ -1275,7 +1291,7 @@ export default async function build(
12751291
let layoutPaths: string[]
12761292

12771293
if (Boolean(process.env.NEXT_PRIVATE_APP_PATHS)) {
1278-
// used for testing?
1294+
// used for testing
12791295
appPaths = providedAppPaths
12801296
layoutPaths = []
12811297
} else {
@@ -1286,6 +1302,20 @@ export default async function build(
12861302

12871303
appPaths = result.appPaths
12881304
layoutPaths = result.layoutPaths
1305+
1306+
// Apply debug build paths filter if specified
1307+
// If debugBuildAppPaths is defined (even if empty), only build specified app paths
1308+
if (debugBuildAppPaths !== undefined) {
1309+
if (debugBuildAppPaths.length > 0) {
1310+
const debugPathsSet = new Set(debugBuildAppPaths)
1311+
appPaths = appPaths.filter((appPath) =>
1312+
debugPathsSet.has(appPath)
1313+
)
1314+
} else {
1315+
// Empty array means build no app paths
1316+
appPaths = []
1317+
}
1318+
}
12891319
// Note: defaultPaths are not used in the build process, only for slot detection in generating route types
12901320
}
12911321

packages/next/src/cli/next-build.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import { getProjectDir } from '../lib/get-project-dir'
1111
import { enableMemoryDebuggingMode } from '../lib/memory/startup'
1212
import { disableMemoryDebuggingMode } from '../lib/memory/shutdown'
1313
import { parseBundlerArgs } from '../lib/bundler'
14+
import {
15+
resolveBuildPaths,
16+
parseBuildPathsInput,
17+
} from '../lib/resolve-build-paths'
1418

1519
export type NextBuildOptions = {
1620
debug?: boolean
@@ -26,9 +30,10 @@ export type NextBuildOptions = {
2630
experimentalBuildMode: 'default' | 'compile' | 'generate' | 'generate-env'
2731
experimentalUploadTrace?: string
2832
experimentalNextConfigStripTypes?: boolean
33+
debugBuildPaths?: string
2934
}
3035

31-
const nextBuild = (options: NextBuildOptions, directory?: string) => {
36+
const nextBuild = async (options: NextBuildOptions, directory?: string) => {
3237
process.on('SIGTERM', () => process.exit(143))
3338
process.on('SIGINT', () => process.exit(130))
3439

@@ -41,6 +46,7 @@ const nextBuild = (options: NextBuildOptions, directory?: string) => {
4146
experimentalAppOnly,
4247
experimentalBuildMode,
4348
experimentalUploadTrace,
49+
debugBuildPaths,
4450
} = options
4551

4652
let traceUploadUrl: string | undefined
@@ -81,6 +87,27 @@ const nextBuild = (options: NextBuildOptions, directory?: string) => {
8187

8288
const bundler = parseBundlerArgs(options)
8389

90+
// Resolve selective build paths
91+
let resolvedAppPaths: string[] | undefined
92+
let resolvedPagePaths: string[] | undefined
93+
94+
if (debugBuildPaths) {
95+
try {
96+
const patterns = parseBuildPathsInput(debugBuildPaths)
97+
98+
if (patterns.length > 0) {
99+
const resolved = await resolveBuildPaths(patterns, dir)
100+
// Pass empty arrays to indicate "build nothing" vs undefined for "build everything"
101+
resolvedAppPaths = resolved.appPaths
102+
resolvedPagePaths = resolved.pagePaths
103+
}
104+
} catch (err) {
105+
printAndExit(
106+
`Failed to resolve build paths: ${isError(err) ? err.message : String(err)}`
107+
)
108+
}
109+
}
110+
84111
return build(
85112
dir,
86113
profile,
@@ -90,7 +117,9 @@ const nextBuild = (options: NextBuildOptions, directory?: string) => {
90117
experimentalAppOnly,
91118
bundler,
92119
experimentalBuildMode,
93-
traceUploadUrl
120+
traceUploadUrl,
121+
resolvedAppPaths,
122+
resolvedPagePaths
94123
)
95124
.catch((err) => {
96125
if (experimentalDebugMemoryUsage) {
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { promisify } from 'util'
2+
import globOriginal from 'next/dist/compiled/glob'
3+
import * as Log from '../build/output/log'
4+
import path from 'path'
5+
import fs from 'fs'
6+
import isError from './is-error'
7+
8+
const glob = promisify(globOriginal)
9+
10+
interface ResolvedBuildPaths {
11+
appPaths: string[]
12+
pagePaths: string[]
13+
}
14+
15+
/**
16+
* Resolves glob patterns and explicit paths to actual file paths
17+
* Categorizes them into App Router and Pages Router paths
18+
*
19+
* @param patterns - Array of glob patterns or explicit paths
20+
* @param projectDir - Root project directory
21+
* @returns Object with categorized app and page paths
22+
*/
23+
export async function resolveBuildPaths(
24+
patterns: string[],
25+
projectDir: string
26+
): Promise<ResolvedBuildPaths> {
27+
const appPaths: Set<string> = new Set()
28+
const pagePaths: Set<string> = new Set()
29+
30+
for (const pattern of patterns) {
31+
const trimmed = pattern.trim()
32+
33+
if (!trimmed) {
34+
continue
35+
}
36+
37+
// Detect if pattern is glob pattern (contains glob special chars)
38+
const isGlobPattern = /[*?[\]{}!]/.test(trimmed)
39+
40+
if (isGlobPattern) {
41+
try {
42+
// Resolve glob pattern
43+
const matches = (await glob(trimmed, {
44+
cwd: projectDir,
45+
})) as string[]
46+
47+
if (matches.length === 0) {
48+
Log.warn(`Glob pattern "${trimmed}" did not match any files`)
49+
}
50+
51+
for (const file of matches) {
52+
// Skip directories, only process files
53+
if (!fs.statSync(path.join(projectDir, file)).isDirectory()) {
54+
categorizeAndAddPath(file, appPaths, pagePaths)
55+
}
56+
}
57+
} catch (error) {
58+
throw new Error(
59+
`Failed to resolve glob pattern "${trimmed}": ${
60+
isError(error) ? error.message : String(error)
61+
}`
62+
)
63+
}
64+
} else {
65+
// Explicit path - categorize based on prefix
66+
categorizeAndAddPath(trimmed, appPaths, pagePaths, projectDir)
67+
}
68+
}
69+
70+
return {
71+
appPaths: Array.from(appPaths).sort(),
72+
pagePaths: Array.from(pagePaths).sort(),
73+
}
74+
}
75+
76+
/**
77+
* Categorizes a file path to either app or pages router based on its prefix,
78+
* and normalizes it to the format expected by Next.js internal build system.
79+
*
80+
* The internal build system expects:
81+
* - App router: paths with leading slash (e.g., "/page.tsx", "/dashboard/page.tsx")
82+
* - Pages router: paths with leading slash (e.g., "/index.tsx", "/about.tsx")
83+
*
84+
* Examples:
85+
* - "app/page.tsx" → appPaths.add("/page.tsx")
86+
* - "app/dashboard/page.tsx" → appPaths.add("/dashboard/page.tsx")
87+
* - "pages/index.tsx" → pagePaths.add("/index.tsx")
88+
* - "pages/about.tsx" → pagePaths.add("/about.tsx")
89+
* - "/page.tsx" → appPaths.add("/page.tsx") (already in app router format)
90+
*/
91+
function categorizeAndAddPath(
92+
filePath: string,
93+
appPaths: Set<string>,
94+
pagePaths: Set<string>,
95+
projectDir?: string
96+
): void {
97+
// Normalize path separators to forward slashes (Windows compatibility)
98+
const normalized = filePath.replace(/\\/g, '/')
99+
100+
// Skip non-file entries (like directories without extensions)
101+
if (normalized.endsWith('/')) {
102+
return
103+
}
104+
105+
if (normalized.startsWith('app/')) {
106+
// App router path: remove 'app/' prefix and ensure leading slash
107+
// "app/page.tsx" → "/page.tsx"
108+
// "app/dashboard/page.tsx" → "/dashboard/page.tsx"
109+
const withoutPrefix = normalized.slice(4) // Remove "app/"
110+
appPaths.add('/' + withoutPrefix)
111+
} else if (normalized.startsWith('pages/')) {
112+
// Pages router path: remove 'pages/' prefix and add leading slash
113+
// "pages/index.tsx" → "/index.tsx"
114+
// "pages/about.tsx" → "/about.tsx"
115+
const withoutPrefix = normalized.slice(6) // Remove "pages/"
116+
pagePaths.add('/' + withoutPrefix)
117+
} else if (normalized.startsWith('/')) {
118+
// Leading slash suggests app router format (already in correct format)
119+
// "/page.tsx" → "/page.tsx" (no change needed)
120+
appPaths.add(normalized)
121+
} else {
122+
// No obvious prefix - try to detect based on file existence
123+
if (projectDir) {
124+
const appPath = path.join(projectDir, 'app', normalized)
125+
const pagesPath = path.join(projectDir, 'pages', normalized)
126+
127+
if (fs.existsSync(appPath)) {
128+
appPaths.add('/' + normalized)
129+
} else if (fs.existsSync(pagesPath)) {
130+
pagePaths.add('/' + normalized)
131+
} else {
132+
// Default to pages router for paths without clear indicator
133+
pagePaths.add('/' + normalized)
134+
}
135+
} else {
136+
// Without projectDir context, default to pages router
137+
pagePaths.add('/' + normalized)
138+
}
139+
}
140+
}
141+
142+
/**
143+
* Parse build paths from comma-separated format
144+
* Supports:
145+
* - Comma-separated values: "app/page.tsx,app/about/page.tsx"
146+
*
147+
* @param input - String input to parse
148+
* @returns Array of path patterns
149+
*/
150+
export function parseBuildPathsInput(input: string): string[] {
151+
// Comma-separated values
152+
return input
153+
.split(',')
154+
.map((p) => p.trim())
155+
.filter((p) => p.length > 0)
156+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function About() {
2+
return <h1>App About</h1>
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Dashboard() {
2+
return <h1>App Dashboard</h1>
3+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from 'react'
2+
3+
export const metadata = {
4+
title: 'Debug Build Paths Test',
5+
}
6+
7+
export default function RootLayout({
8+
children,
9+
}: {
10+
children: React.ReactNode
11+
}) {
12+
return (
13+
<html lang="en">
14+
<body>{children}</body>
15+
</html>
16+
)
17+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Home() {
2+
return <h1>App Home</h1>
3+
}

0 commit comments

Comments
 (0)