Repository: vercel/next.js
Pull request: Add negation pattern support to `--debug-build-paths`

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

Changed files:
filename: packages/next/src/bin/next.ts
patch: @@ -183,7 +183,7 @@ program
   )
   .option(
     '--debug-build-paths <patterns>',
-    'Comma-separated glob patterns or explicit paths for selective builds. Examples: "app/*", "app/page.tsx", "app/**/page.tsx"'
+    '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]/**"'
   )
   .option(
     '--experimental-cpu-prof',

filename: packages/next/src/lib/resolve-build-paths.ts
patch: @@ -28,6 +28,10 @@ function escapeBrackets(pattern: string): string {
 /**
  * Resolves glob patterns and explicit paths to actual file paths.
  * Categorizes them into App Router and Pages Router paths.
+ *
+ * Supports negation patterns prefixed with "!" to exclude paths.
+ * e.g., "app/**,!app/[lang]/page.js" includes all App Router paths except
+ * app/[lang]/page.js
  */
 export async function resolveBuildPaths(
   patterns: string[],
@@ -36,33 +40,52 @@ export async function resolveBuildPaths(
   const appPaths: Set<string> = new Set()
   const pagePaths: Set<string> = new Set()
 
+  const includePatterns: string[] = []
+  const excludePatterns: string[] = []
+
   for (const pattern of patterns) {
     const trimmed = pattern.trim()
     if (!trimmed) continue
 
-    try {
-      // Escape brackets for Next.js dynamic route directories
-      const escapedPattern = escapeBrackets(trimmed)
-      const matches = (await glob(escapedPattern, {
-        cwd: projectDir,
-      })) as string[]
+    if (trimmed.startsWith('!')) {
+      excludePatterns.push(escapeBrackets(trimmed.slice(1)))
+    } else {
+      includePatterns.push(escapeBrackets(trimmed))
+    }
+  }
 
-      if (matches.length === 0) {
-        Log.warn(`Pattern "${trimmed}" did not match any files`)
-      }
+  // Default to matching all files when only negation patterns are provided.
+  if (includePatterns.length === 0 && excludePatterns.length > 0) {
+    includePatterns.push('**')
+  }
+
+  // Combine patterns using brace expansion: {pattern1,pattern2}
+  const combinedPattern =
+    includePatterns.length === 1
+      ? includePatterns[0]
+      : `{${includePatterns.join(',')}}`
+
+  try {
+    const matches = (await glob(combinedPattern, {
+      cwd: projectDir,
+      ignore: excludePatterns,
+    })) as string[]
+
+    if (matches.length === 0) {
+      Log.warn(`Pattern "${patterns.join(',')}" did not match any files`)
+    }
 
-      for (const file of matches) {
-        if (!fs.statSync(path.join(projectDir, file)).isDirectory()) {
-          categorizeAndAddPath(file, appPaths, pagePaths)
-        }
+    for (const file of matches) {
+      if (!fs.statSync(path.join(projectDir, file)).isDirectory()) {
+        categorizeAndAddPath(file, appPaths, pagePaths)
       }
-    } catch (error) {
-      throw new Error(
-        `Failed to resolve pattern "${trimmed}": ${
-          isError(error) ? error.message : String(error)
-        }`
-      )
     }
+  } catch (error) {
+    throw new Error(
+      `Failed to resolve pattern "${patterns.join(',')}": ${
+        isError(error) ? error.message : String(error)
+      }`
+    )
   }
 
   return {

filename: test/production/debug-build-path/debug-build-paths.test.ts
patch: @@ -162,6 +162,67 @@ describe('debug-build-paths', () => {
       // Should not build pages routes
       expect(buildResult.cliOutput).not.toContain('Route (pages)')
     })
+
+    it('should exclude paths matching negation patterns', async () => {
+      const buildResult = await next.build({
+        args: [
+          '--debug-build-paths',
+          'app/**/page.tsx,!app/with-type-error/**',
+        ],
+      })
+      expect(buildResult.exitCode).toBe(0)
+
+      expect(buildResult.cliOutput).toContain('Route (app)')
+      expect(buildResult.cliOutput).toContain('○ /')
+      expect(buildResult.cliOutput).toContain('○ /about')
+      expect(buildResult.cliOutput).toContain('○ /dashboard')
+      expect(buildResult.cliOutput).toContain('/blog/[slug]')
+      expect(buildResult.cliOutput).not.toContain('/with-type-error')
+    })
+
+    it('should exclude dynamic route paths with negation', async () => {
+      const buildResult = await next.build({
+        args: [
+          '--debug-build-paths',
+          'app/blog/**/page.tsx,!app/blog/[slug]/comments/**',
+        ],
+      })
+      expect(buildResult.exitCode).toBe(0)
+
+      expect(buildResult.cliOutput).toContain('Route (app)')
+      expect(buildResult.cliOutput).toContain('/blog/[slug]')
+      expect(buildResult.cliOutput).not.toContain('/blog/[slug]/comments')
+    })
+
+    it('should support multiple negation patterns', async () => {
+      const buildResult = await next.build({
+        args: [
+          '--debug-build-paths',
+          'app/**/page.tsx,!app/with-type-error/**,!app/dashboard/**',
+        ],
+      })
+      expect(buildResult.exitCode).toBe(0)
+
+      expect(buildResult.cliOutput).toContain('Route (app)')
+      expect(buildResult.cliOutput).toContain('○ /')
+      expect(buildResult.cliOutput).toContain('○ /about')
+      expect(buildResult.cliOutput).not.toContain('/with-type-error')
+      expect(buildResult.cliOutput).not.toContain('○ /dashboard')
+    })
+
+    it('should build everything except excluded paths when only negation patterns are provided', async () => {
+      const buildResult = await next.build({
+        args: ['--debug-build-paths', '!app/with-type-error/**'],
+      })
+      expect(buildResult.exitCode).toBe(0)
+
+      expect(buildResult.cliOutput).toContain('Route (app)')
+      expect(buildResult.cliOutput).toContain('Route (pages)')
+      expect(buildResult.cliOutput).toContain('○ /')
+      expect(buildResult.cliOutput).toContain('○ /about')
+      expect(buildResult.cliOutput).toContain('○ /foo')
+      expect(buildResult.cliOutput).not.toContain('/with-type-error')
+    })
   })
 
   describe('typechecking with debug-build-paths', () => {