diff --git a/packages/nextjs/src/config/manifest/createRouteManifest.ts b/packages/nextjs/src/config/manifest/createRouteManifest.ts index 4df71b389c8b..1e905d858f73 100644 --- a/packages/nextjs/src/config/manifest/createRouteManifest.ts +++ b/packages/nextjs/src/config/manifest/createRouteManifest.ts @@ -77,10 +77,16 @@ function buildRegexForDynamicRoute(routePath: string): { regex: string; paramNam let pattern: string; if (hasOptionalCatchall) { - // For optional catchall, make the trailing slash and segments optional - // This allows matching both /catchall and /catchall/anything - const staticParts = regexSegments.join('/'); - pattern = `^/${staticParts}(?:/(.*))?$`; + if (regexSegments.length === 0) { + // If the optional catchall happens at the root, accept any path starting + // with a slash. Need capturing group for parameter extraction. + pattern = '^/(.*)$'; + } else { + // For optional catchall, make the trailing slash and segments optional + // This allows matching both /catchall and /catchall/anything + const staticParts = regexSegments.join('/'); + pattern = `^/${staticParts}(?:/(.*))?$`; + } } else { pattern = `^/${regexSegments.join('/')}$`; } diff --git a/packages/nextjs/test/config/manifest/suites/catchall-at-root/app/[[...path]]/page.tsx b/packages/nextjs/test/config/manifest/suites/catchall-at-root/app/[[...path]]/page.tsx new file mode 100644 index 000000000000..5d33b5d14573 --- /dev/null +++ b/packages/nextjs/test/config/manifest/suites/catchall-at-root/app/[[...path]]/page.tsx @@ -0,0 +1 @@ +// beep diff --git a/packages/nextjs/test/config/manifest/suites/catchall-at-root/app/page.tsx b/packages/nextjs/test/config/manifest/suites/catchall-at-root/app/page.tsx new file mode 100644 index 000000000000..2145a5eea70d --- /dev/null +++ b/packages/nextjs/test/config/manifest/suites/catchall-at-root/app/page.tsx @@ -0,0 +1 @@ +// Ciao diff --git a/packages/nextjs/test/config/manifest/suites/catchall-at-root/catchall-at-root.test.ts b/packages/nextjs/test/config/manifest/suites/catchall-at-root/catchall-at-root.test.ts new file mode 100644 index 000000000000..b7108b6f6f23 --- /dev/null +++ b/packages/nextjs/test/config/manifest/suites/catchall-at-root/catchall-at-root.test.ts @@ -0,0 +1,30 @@ +import path from 'path'; +import { describe, expect, test } from 'vitest'; +import { createRouteManifest } from '../../../../../src/config/manifest/createRouteManifest'; + +describe('catchall', () => { + const manifest = createRouteManifest({ appDirPath: path.join(__dirname, 'app') }); + + test('should generate a manifest with catchall route', () => { + expect(manifest).toEqual({ + staticRoutes: [{ path: '/' }], + dynamicRoutes: [ + { + path: '/:path*?', + regex: '^/(.*)$', + paramNames: ['path'], + }, + ], + }); + }); + + test('should generate correct pattern for catchall route', () => { + const catchallRoute = manifest.dynamicRoutes.find(route => route.path === '/:path*?'); + const regex = new RegExp(catchallRoute?.regex ?? ''); + expect(regex.test('/123')).toBe(true); + expect(regex.test('/abc')).toBe(true); + expect(regex.test('/123/456')).toBe(true); + expect(regex.test('/123/abc/789')).toBe(true); + expect(regex.test('/')).toBe(true); + }); +});