Skip to content

Commit b46d599

Browse files
authored
Add negation pattern support to --debug-build-paths (vercel#88654)
Patterns prefixed with `!` now exclude matching files from the `next build --debug-build-paths` option. Using only negation patterns will build everything except the excluded paths. Examples: - `--debug-build-paths 'app/**,!app/[lang]/**'` - build all app routes except `[lang]` - `--debug-build-paths '!app/admin/**'` - build everything except admin routes
1 parent dc59391 commit b46d599

File tree

3 files changed

+104
-20
lines changed

3 files changed

+104
-20
lines changed

packages/next/src/bin/next.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ program
183183
)
184184
.option(
185185
'--debug-build-paths <patterns>',
186-
'Comma-separated glob patterns or explicit paths for selective builds. Examples: "app/*", "app/page.tsx", "app/**/page.tsx"'
186+
'Comma-separated glob patterns or explicit paths for selective builds. Use "!" prefix to exclude. Examples: "app/*", "app/page.tsx", "app/**/page.tsx", "app/**,!app/[slug]/**"'
187187
)
188188
.option(
189189
'--experimental-cpu-prof',

packages/next/src/lib/resolve-build-paths.ts

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ function escapeBrackets(pattern: string): string {
2828
/**
2929
* Resolves glob patterns and explicit paths to actual file paths.
3030
* Categorizes them into App Router and Pages Router paths.
31+
*
32+
* Supports negation patterns prefixed with "!" to exclude paths.
33+
* e.g., "app/**,!app/[lang]/page.js" includes all App Router paths except
34+
* app/[lang]/page.js
3135
*/
3236
export async function resolveBuildPaths(
3337
patterns: string[],
@@ -36,33 +40,52 @@ export async function resolveBuildPaths(
3640
const appPaths: Set<string> = new Set()
3741
const pagePaths: Set<string> = new Set()
3842

43+
const includePatterns: string[] = []
44+
const excludePatterns: string[] = []
45+
3946
for (const pattern of patterns) {
4047
const trimmed = pattern.trim()
4148
if (!trimmed) continue
4249

43-
try {
44-
// Escape brackets for Next.js dynamic route directories
45-
const escapedPattern = escapeBrackets(trimmed)
46-
const matches = (await glob(escapedPattern, {
47-
cwd: projectDir,
48-
})) as string[]
50+
if (trimmed.startsWith('!')) {
51+
excludePatterns.push(escapeBrackets(trimmed.slice(1)))
52+
} else {
53+
includePatterns.push(escapeBrackets(trimmed))
54+
}
55+
}
4956

50-
if (matches.length === 0) {
51-
Log.warn(`Pattern "${trimmed}" did not match any files`)
52-
}
57+
// Default to matching all files when only negation patterns are provided.
58+
if (includePatterns.length === 0 && excludePatterns.length > 0) {
59+
includePatterns.push('**')
60+
}
61+
62+
// Combine patterns using brace expansion: {pattern1,pattern2}
63+
const combinedPattern =
64+
includePatterns.length === 1
65+
? includePatterns[0]
66+
: `{${includePatterns.join(',')}}`
67+
68+
try {
69+
const matches = (await glob(combinedPattern, {
70+
cwd: projectDir,
71+
ignore: excludePatterns,
72+
})) as string[]
73+
74+
if (matches.length === 0) {
75+
Log.warn(`Pattern "${patterns.join(',')}" did not match any files`)
76+
}
5377

54-
for (const file of matches) {
55-
if (!fs.statSync(path.join(projectDir, file)).isDirectory()) {
56-
categorizeAndAddPath(file, appPaths, pagePaths)
57-
}
78+
for (const file of matches) {
79+
if (!fs.statSync(path.join(projectDir, file)).isDirectory()) {
80+
categorizeAndAddPath(file, appPaths, pagePaths)
5881
}
59-
} catch (error) {
60-
throw new Error(
61-
`Failed to resolve pattern "${trimmed}": ${
62-
isError(error) ? error.message : String(error)
63-
}`
64-
)
6582
}
83+
} catch (error) {
84+
throw new Error(
85+
`Failed to resolve pattern "${patterns.join(',')}": ${
86+
isError(error) ? error.message : String(error)
87+
}`
88+
)
6689
}
6790

6891
return {

test/production/debug-build-path/debug-build-paths.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,67 @@ describe('debug-build-paths', () => {
162162
// Should not build pages routes
163163
expect(buildResult.cliOutput).not.toContain('Route (pages)')
164164
})
165+
166+
it('should exclude paths matching negation patterns', async () => {
167+
const buildResult = await next.build({
168+
args: [
169+
'--debug-build-paths',
170+
'app/**/page.tsx,!app/with-type-error/**',
171+
],
172+
})
173+
expect(buildResult.exitCode).toBe(0)
174+
175+
expect(buildResult.cliOutput).toContain('Route (app)')
176+
expect(buildResult.cliOutput).toContain('○ /')
177+
expect(buildResult.cliOutput).toContain('○ /about')
178+
expect(buildResult.cliOutput).toContain('○ /dashboard')
179+
expect(buildResult.cliOutput).toContain('/blog/[slug]')
180+
expect(buildResult.cliOutput).not.toContain('/with-type-error')
181+
})
182+
183+
it('should exclude dynamic route paths with negation', async () => {
184+
const buildResult = await next.build({
185+
args: [
186+
'--debug-build-paths',
187+
'app/blog/**/page.tsx,!app/blog/[slug]/comments/**',
188+
],
189+
})
190+
expect(buildResult.exitCode).toBe(0)
191+
192+
expect(buildResult.cliOutput).toContain('Route (app)')
193+
expect(buildResult.cliOutput).toContain('/blog/[slug]')
194+
expect(buildResult.cliOutput).not.toContain('/blog/[slug]/comments')
195+
})
196+
197+
it('should support multiple negation patterns', async () => {
198+
const buildResult = await next.build({
199+
args: [
200+
'--debug-build-paths',
201+
'app/**/page.tsx,!app/with-type-error/**,!app/dashboard/**',
202+
],
203+
})
204+
expect(buildResult.exitCode).toBe(0)
205+
206+
expect(buildResult.cliOutput).toContain('Route (app)')
207+
expect(buildResult.cliOutput).toContain('○ /')
208+
expect(buildResult.cliOutput).toContain('○ /about')
209+
expect(buildResult.cliOutput).not.toContain('/with-type-error')
210+
expect(buildResult.cliOutput).not.toContain('○ /dashboard')
211+
})
212+
213+
it('should build everything except excluded paths when only negation patterns are provided', async () => {
214+
const buildResult = await next.build({
215+
args: ['--debug-build-paths', '!app/with-type-error/**'],
216+
})
217+
expect(buildResult.exitCode).toBe(0)
218+
219+
expect(buildResult.cliOutput).toContain('Route (app)')
220+
expect(buildResult.cliOutput).toContain('Route (pages)')
221+
expect(buildResult.cliOutput).toContain('○ /')
222+
expect(buildResult.cliOutput).toContain('○ /about')
223+
expect(buildResult.cliOutput).toContain('○ /foo')
224+
expect(buildResult.cliOutput).not.toContain('/with-type-error')
225+
})
165226
})
166227

167228
describe('typechecking with debug-build-paths', () => {

0 commit comments

Comments
 (0)