Repository: vercel/next.js
Pull request: Fix `--debug-build-paths` bracket escaping for glob patterns

Always escape Next.js dynamic route brackets (e.g., `[slug]`) in `--debug-build-paths` patterns instead of checking filesystem existence.

The previous approach failed when glob wildcards like `**` preceded bracket expressions (e.g., `app/**/[slug]/page.tsx`) because `fs.existsSync` cannot resolve paths containing glob characters. Since users of this flag are always referring to actual Next.js routes, brackets should always be treated as literal directory names rather than glob character classes.

Changed files:
filename: packages/next/src/lib/resolve-build-paths.ts
patch: @@ -13,31 +13,16 @@ interface ResolvedBuildPaths {
 }
 
 /**
- * Escapes bracket expressions that correspond to existing directories.
- * This allows Next.js dynamic routes like [slug] to work with glob patterns.
+ * Escapes Next.js dynamic route bracket expressions so glob treats them as
+ * literal directory names rather than character classes.
  *
  * e.g., "app/blog/[slug]/** /page.tsx" → "app/blog/\[slug\]/** /page.tsx"
- *       (if app/blog/[slug] directory exists)
  */
-function escapeExistingBrackets(pattern: string, projectDir: string): string {
-  // Match bracket expressions: [name], [...name], [[...name]]
-  const bracketRegex = /\[\[?\.\.\.[^\]]+\]?\]|\[[^\]]+\]/g
-  let lastIndex = 0
-  let result = ''
-  let match: RegExpExecArray | null
-
-  while ((match = bracketRegex.exec(pattern)) !== null) {
-    const pathPrefix = pattern.slice(0, match.index + match[0].length)
-    const exists = fs.existsSync(path.join(projectDir, pathPrefix))
-
-    result += pattern.slice(lastIndex, match.index)
-    result += exists
-      ? match[0].replace(/\[/g, '\\[').replace(/\]/g, '\\]')
-      : match[0]
-    lastIndex = match.index + match[0].length
-  }
-
-  return result + pattern.slice(lastIndex)
+function escapeBrackets(pattern: string): string {
+  // Match Next.js dynamic route patterns: [name], [...name], [[...name]]
+  return pattern.replace(/\[\[?\.\.\.[^\]]+\]?\]|\[[^\]]+\]/g, (match) =>
+    match.replace(/\[/g, '\\[').replace(/\]/g, '\\]')
+  )
 }
 
 /**
@@ -56,8 +41,8 @@ export async function resolveBuildPaths(
     if (!trimmed) continue
 
     try {
-      // Escape brackets that correspond to existing Next.js dynamic route directories
-      const escapedPattern = escapeExistingBrackets(trimmed, projectDir)
+      // Escape brackets for Next.js dynamic route directories
+      const escapedPattern = escapeBrackets(trimmed)
       const matches = (await glob(escapedPattern, {
         cwd: projectDir,
       })) as string[]

filename: test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts
patch: @@ -67,8 +67,7 @@ describe('Cache Components Errors', () => {
         '--experimental-build-mode',
         'generate',
         '--debug-build-paths',
-        // Escape square brackets for pathnames with dynamic segments.
-        `app${pathname.replace(/([[\]])/g, '\\$1')}/page.tsx`,
+        `app${pathname}/page.tsx`,
       ]
 
       if (isDebugPrerender) {

filename: test/production/debug-build-path/debug-build-paths.test.ts
patch: @@ -101,6 +101,24 @@ describe('debug-build-paths', () => {
       expect(buildResult.cliOutput).not.toContain('Route (pages)')
     })
 
+    it('should match dynamic routes with glob before brackets like app/**/[slug]/page.tsx', async () => {
+      const buildResult = await next.build({
+        args: ['--debug-build-paths', 'app/**/[slug]/page.tsx'],
+      })
+      expect(buildResult.exitCode).toBe(0)
+      expect(buildResult.cliOutput).toBeDefined()
+
+      // Should build the blog/[slug] route
+      expect(buildResult.cliOutput).toContain('Route (app)')
+      expect(buildResult.cliOutput).toContain('/blog/[slug]')
+      // Should not build other app routes
+      expect(buildResult.cliOutput).not.toMatch(/○ \/\n/)
+      expect(buildResult.cliOutput).not.toContain('○ /about')
+      expect(buildResult.cliOutput).not.toContain('○ /dashboard')
+      // Should not build pages routes
+      expect(buildResult.cliOutput).not.toContain('Route (pages)')
+    })
+
     it('should match hybrid pattern with literal [slug] and glob **', async () => {
       // Test pattern: app/blog/[slug]/**/page.tsx
       // [slug] should be treated as literal directory (exists on disk)