vinext 0.0.21 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/build/static-export.d.ts.map +1 -1
  2. package/dist/build/static-export.js +9 -7
  3. package/dist/build/static-export.js.map +1 -1
  4. package/dist/config/next-config.d.ts +4 -1
  5. package/dist/config/next-config.d.ts.map +1 -1
  6. package/dist/config/next-config.js +10 -5
  7. package/dist/config/next-config.js.map +1 -1
  8. package/dist/deploy.d.ts.map +1 -1
  9. package/dist/deploy.js +17 -4
  10. package/dist/deploy.js.map +1 -1
  11. package/dist/index.d.ts +25 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +381 -38
  14. package/dist/index.js.map +1 -1
  15. package/dist/routing/app-router.d.ts +2 -1
  16. package/dist/routing/app-router.d.ts.map +1 -1
  17. package/dist/routing/app-router.js +74 -107
  18. package/dist/routing/app-router.js.map +1 -1
  19. package/dist/routing/file-matcher.d.ts +24 -0
  20. package/dist/routing/file-matcher.d.ts.map +1 -0
  21. package/dist/routing/file-matcher.js +75 -0
  22. package/dist/routing/file-matcher.js.map +1 -0
  23. package/dist/routing/pages-router.d.ts +3 -2
  24. package/dist/routing/pages-router.d.ts.map +1 -1
  25. package/dist/routing/pages-router.js +25 -44
  26. package/dist/routing/pages-router.js.map +1 -1
  27. package/dist/routing/utils.d.ts +25 -0
  28. package/dist/routing/utils.d.ts.map +1 -0
  29. package/dist/routing/utils.js +70 -0
  30. package/dist/routing/utils.js.map +1 -0
  31. package/dist/server/app-dev-server.d.ts.map +1 -1
  32. package/dist/server/app-dev-server.js +123 -47
  33. package/dist/server/app-dev-server.js.map +1 -1
  34. package/dist/server/dev-server.d.ts +2 -1
  35. package/dist/server/dev-server.d.ts.map +1 -1
  36. package/dist/server/dev-server.js +93 -18
  37. package/dist/server/dev-server.js.map +1 -1
  38. package/dist/server/prod-server.d.ts.map +1 -1
  39. package/dist/server/prod-server.js +33 -2
  40. package/dist/server/prod-server.js.map +1 -1
  41. package/dist/server/request-log.d.ts +34 -0
  42. package/dist/server/request-log.d.ts.map +1 -0
  43. package/dist/server/request-log.js +65 -0
  44. package/dist/server/request-log.js.map +1 -0
  45. package/dist/shims/cache-runtime.d.ts.map +1 -1
  46. package/dist/shims/cache-runtime.js +5 -1
  47. package/dist/shims/cache-runtime.js.map +1 -1
  48. package/dist/shims/cache.d.ts +7 -1
  49. package/dist/shims/cache.d.ts.map +1 -1
  50. package/dist/shims/cache.js +30 -5
  51. package/dist/shims/cache.js.map +1 -1
  52. package/dist/shims/head.d.ts +11 -0
  53. package/dist/shims/head.d.ts.map +1 -1
  54. package/dist/shims/head.js +21 -0
  55. package/dist/shims/head.js.map +1 -1
  56. package/dist/shims/headers.d.ts +8 -0
  57. package/dist/shims/headers.d.ts.map +1 -1
  58. package/dist/shims/headers.js +41 -0
  59. package/dist/shims/headers.js.map +1 -1
  60. package/dist/shims/metadata.d.ts +1 -0
  61. package/dist/shims/metadata.d.ts.map +1 -1
  62. package/dist/shims/metadata.js +5 -1
  63. package/dist/shims/metadata.js.map +1 -1
  64. package/dist/shims/script.d.ts.map +1 -1
  65. package/dist/shims/script.js +7 -1
  66. package/dist/shims/script.js.map +1 -1
  67. package/dist/shims/server.d.ts.map +1 -1
  68. package/dist/shims/server.js +2 -1
  69. package/dist/shims/server.js.map +1 -1
  70. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"pages-router.js","sourceRoot":"","sources":["../../src/routing/pages-router.ts"],"names":[],"mappings":"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","sourcesContent":["import { glob } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport interface Route {\n /** URL pattern, e.g. \"/\" or \"/about\" or \"/posts/:id\" */\n pattern: string;\n /** Absolute file path to the page component */\n filePath: string;\n /** Whether this is a dynamic route */\n isDynamic: boolean;\n /** Parameter names for dynamic segments */\n params: string[];\n}\n\n// Route cache — invalidated when pages directory changes\nconst routeCache = new Map<string, { routes: Route[]; promise: Promise<Route[]> }>();\n\n/**\n * Invalidate cached routes for a given pages directory.\n * Called by the file watcher when pages are added/removed.\n */\nexport function invalidateRouteCache(pagesDir: string): void {\n routeCache.delete(`pages:${pagesDir}`);\n routeCache.delete(`api:${pagesDir}`);\n}\n\n/**\n * Scan the pages/ directory and return a list of routes.\n * Results are cached — call invalidateRouteCache() when files change.\n *\n * Follows Next.js Pages Router conventions:\n * - pages/index.tsx -> /\n * - pages/about.tsx -> /about\n * - pages/posts/[id].tsx -> /posts/:id\n * - pages/[...slug].tsx -> /:slug+\n * - Ignores _app.tsx, _document.tsx, _error.tsx, files starting with _\n * - Ignores pages/api/ (handled separately later)\n */\nexport async function pagesRouter(pagesDir: string): Promise<Route[]> {\n const cacheKey = `pages:${pagesDir}`;\n const cached = routeCache.get(cacheKey);\n if (cached) return cached.promise;\n\n const promise = scanPageRoutes(pagesDir);\n routeCache.set(cacheKey, { routes: [], promise });\n const routes = await promise;\n routeCache.set(cacheKey, { routes, promise });\n return routes;\n}\n\nasync function scanPageRoutes(pagesDir: string): Promise<Route[]> {\n const routes: Route[] = [];\n\n // Use function form of exclude for Node < 22.14 compatibility (string arrays require >= 22.14)\n for await (const file of glob(\"**/*.{tsx,ts,jsx,js}\", { cwd: pagesDir, exclude: (name: string) => name === \"api\" || name.startsWith(\"_\") })) {\n const route = fileToRoute(file, pagesDir);\n if (route) routes.push(route);\n }\n\n // Sort: static routes first, then dynamic, then catch-all\n routes.sort((a, b) => {\n const diff = routePrecedence(a.pattern) - routePrecedence(b.pattern);\n return diff !== 0 ? diff : a.pattern.localeCompare(b.pattern);\n });\n\n return routes;\n}\n\n/**\n * Convert a file path relative to pages/ into a Route.\n */\nfunction fileToRoute(file: string, pagesDir: string): Route | null {\n // Remove extension\n const withoutExt = file.replace(/\\.(tsx?|jsx?)$/, \"\");\n\n // Convert to URL segments\n const segments = withoutExt.split(path.sep);\n\n // Handle index files: pages/index.tsx -> /\n const lastSegment = segments[segments.length - 1];\n if (lastSegment === \"index\") {\n segments.pop();\n }\n\n const params: string[] = [];\n let isDynamic = false;\n\n // Convert Next.js dynamic segments to URL patterns\n const urlSegments = segments.map((segment) => {\n // Catch-all: [...slug] -> :slug+ (param names may contain hyphens)\n const catchAllMatch = segment.match(/^\\[\\.\\.\\.([\\w-]+)\\]$/);\n if (catchAllMatch) {\n isDynamic = true;\n params.push(catchAllMatch[1]);\n return `:${catchAllMatch[1]}+`;\n }\n\n // Optional catch-all: [[...slug]] -> :slug* (param names may contain hyphens)\n const optionalCatchAllMatch = segment.match(/^\\[\\[\\.\\.\\.([\\w-]+)\\]\\]$/);\n if (optionalCatchAllMatch) {\n isDynamic = true;\n params.push(optionalCatchAllMatch[1]);\n return `:${optionalCatchAllMatch[1]}*`;\n }\n\n // Dynamic segment: [id] -> :id (param names may contain hyphens)\n const dynamicMatch = segment.match(/^\\[([\\w-]+)\\]$/);\n if (dynamicMatch) {\n isDynamic = true;\n params.push(dynamicMatch[1]);\n return `:${dynamicMatch[1]}`;\n }\n\n return segment;\n });\n\n const pattern = \"/\" + urlSegments.join(\"/\");\n\n return {\n pattern: pattern === \"/\" ? \"/\" : pattern,\n filePath: path.join(pagesDir, file),\n isDynamic,\n params,\n };\n}\n\n/**\n * Route precedence — lower score is higher priority.\n * Matches Next.js specificity rules:\n * 1. Static routes first (scored by segment count, more = more specific)\n * 2. Dynamic segments penalized by position\n * 3. Catch-all comes after dynamic\n * 4. Optional catch-all last\n * 5. Lexicographic tiebreaker for determinism\n */\nfunction routePrecedence(pattern: string): number {\n const parts = pattern.split(\"/\").filter(Boolean);\n let score = 0;\n for (let i = 0; i < parts.length; i++) {\n const p = parts[i];\n if (p.endsWith(\"+\")) {\n score += 10000 + i; // catch-all: high penalty\n } else if (p.endsWith(\"*\")) {\n score += 20000 + i; // optional catch-all: highest penalty\n } else if (p.startsWith(\":\")) {\n score += 100 + i; // dynamic: moderate penalty by position\n }\n // static segments contribute nothing (better specificity)\n }\n return score;\n}\n\n/**\n * Match a URL path against a route pattern.\n * Returns the matched params or null if no match.\n */\nexport function matchRoute(\n url: string,\n routes: Route[],\n): { route: Route; params: Record<string, string | string[]> } | null {\n // Normalize: strip query string and trailing slash\n const pathname = url.split(\"?\")[0];\n let normalizedUrl =\n pathname === \"/\" ? \"/\" : pathname.replace(/\\/$/, \"\");\n try { normalizedUrl = decodeURIComponent(normalizedUrl); } catch { /* malformed percent-encoding — match as-is */ }\n\n for (const route of routes) {\n const params = matchPattern(normalizedUrl, route.pattern);\n if (params !== null) {\n return { route, params };\n }\n }\n\n return null;\n}\n\n/**\n * Scan the pages/api/ directory and return API routes.\n * Results are cached — call invalidateRouteCache() when files change.\n *\n * Follows Next.js conventions:\n * - pages/api/hello.ts -> /api/hello\n * - pages/api/users/[id].ts -> /api/users/:id\n */\nexport async function apiRouter(pagesDir: string): Promise<Route[]> {\n const cacheKey = `api:${pagesDir}`;\n const cached = routeCache.get(cacheKey);\n if (cached) return cached.promise;\n\n const promise = scanApiRoutes(pagesDir);\n routeCache.set(cacheKey, { routes: [], promise });\n const routes = await promise;\n routeCache.set(cacheKey, { routes, promise });\n return routes;\n}\n\nasync function scanApiRoutes(pagesDir: string): Promise<Route[]> {\n const apiDir = path.join(pagesDir, \"api\");\n let files: string[];\n try {\n files = [];\n for await (const file of glob(\"**/*.{ts,tsx,js,jsx}\", { cwd: apiDir })) {\n files.push(file);\n }\n } catch {\n files = [];\n }\n\n const routes: Route[] = [];\n\n for (const file of files) {\n // Reuse fileToRoute but pretend the file is under a virtual \"api/\" prefix\n const route = fileToRoute(path.join(\"api\", file), pagesDir);\n if (route) {\n routes.push(route);\n }\n }\n\n // Sort same as page routes\n routes.sort((a, b) => {\n const diff = routePrecedence(a.pattern) - routePrecedence(b.pattern);\n return diff !== 0 ? diff : a.pattern.localeCompare(b.pattern);\n });\n\n return routes;\n}\n\nfunction matchPattern(\n url: string,\n pattern: string,\n): Record<string, string | string[]> | null {\n const urlParts = url.split(\"/\").filter(Boolean);\n const patternParts = pattern.split(\"/\").filter(Boolean);\n\n const params: Record<string, string | string[]> = Object.create(null);\n\n for (let i = 0; i < patternParts.length; i++) {\n const pp = patternParts[i];\n\n // Catch-all: :slug+\n if (pp.endsWith(\"+\")) {\n const paramName = pp.slice(1, -1);\n const remaining = urlParts.slice(i);\n if (remaining.length === 0) return null;\n params[paramName] = remaining;\n return params;\n }\n\n // Optional catch-all: :slug*\n if (pp.endsWith(\"*\")) {\n const paramName = pp.slice(1, -1);\n const remaining = urlParts.slice(i);\n params[paramName] = remaining;\n return params;\n }\n\n // Dynamic segment: :id\n if (pp.startsWith(\":\")) {\n const paramName = pp.slice(1);\n if (i >= urlParts.length) return null;\n params[paramName] = urlParts[i];\n continue;\n }\n\n // Static segment\n if (i >= urlParts.length || urlParts[i] !== pp) return null;\n }\n\n // All pattern parts matched - check url doesn't have extra segments\n if (urlParts.length !== patternParts.length) return null;\n\n return params;\n}\n\n/**\n * Convert internal route pattern (e.g., \"/posts/:id\", \"/docs/:slug+\")\n * to Next.js bracket format (e.g., \"/posts/[id]\", \"/docs/[...slug]\").\n * Used for __NEXT_DATA__.page which apps expect in Next.js format.\n */\nexport function patternToNextFormat(pattern: string): string {\n return pattern\n .replace(/:([\\w-]+)\\*/g, \"[[...$1]]\") // optional catch-all :slug* -> [[...slug]]\n .replace(/:([\\w-]+)\\+/g, \"[...$1]\") // catch-all :slug+ -> [...slug]\n .replace(/:([\\w-]+)/g, \"[$1]\"); // dynamic :id -> [id]\n}\n"]}
1
+ {"version":3,"file":"pages-router.js","sourceRoot":"","sources":["../../src/routing/pages-router.ts"],"names":[],"mappings":"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","sourcesContent":["import path from \"node:path\";\nimport { routePrecedence } from \"./utils.js\";\nimport {\n createValidFileMatcher,\n scanWithExtensions,\n type ValidFileMatcher,\n} from \"./file-matcher.js\";\n\nexport interface Route {\n /** URL pattern, e.g. \"/\" or \"/about\" or \"/posts/:id\" */\n pattern: string;\n /** Absolute file path to the page component */\n filePath: string;\n /** Whether this is a dynamic route */\n isDynamic: boolean;\n /** Parameter names for dynamic segments */\n params: string[];\n}\n\n// Route cache — invalidated when pages directory changes\nconst routeCache = new Map<string, { routes: Route[]; promise: Promise<Route[]> }>();\n\n/**\n * Invalidate cached routes for a given pages directory.\n * Called by the file watcher when pages are added/removed.\n */\nexport function invalidateRouteCache(pagesDir: string): void {\n for (const key of routeCache.keys()) {\n if (key.startsWith(`pages:${pagesDir}:`) || key.startsWith(`api:${pagesDir}:`)) {\n routeCache.delete(key);\n }\n }\n}\n\n/**\n * Scan the pages/ directory and return a list of routes.\n * Results are cached — call invalidateRouteCache() when files change.\n *\n * Follows Next.js Pages Router conventions:\n * - pages/index.tsx -> /\n * - pages/about.tsx -> /about\n * - pages/posts/[id].tsx -> /posts/:id\n * - pages/[...slug].tsx -> /:slug+\n * - Ignores _app.tsx, _document.tsx, _error.tsx, files starting with _\n * - Ignores pages/api/ (handled separately later)\n */\nexport async function pagesRouter(\n pagesDir: string,\n pageExtensions?: readonly string[],\n matcher?: ValidFileMatcher,\n): Promise<Route[]> {\n matcher ??= createValidFileMatcher(pageExtensions);\n const cacheKey = `pages:${pagesDir}:${JSON.stringify(matcher.extensions)}`;\n const cached = routeCache.get(cacheKey);\n if (cached) return cached.promise;\n\n const promise = scanPageRoutes(pagesDir, matcher);\n routeCache.set(cacheKey, { routes: [], promise });\n const routes = await promise;\n routeCache.set(cacheKey, { routes, promise });\n return routes;\n}\n\nasync function scanPageRoutes(\n pagesDir: string,\n matcher: ValidFileMatcher,\n): Promise<Route[]> {\n const routes: Route[] = [];\n\n // Use function form of exclude for Node < 22.14 compatibility (string arrays require >= 22.14)\n for await (const file of scanWithExtensions(\n \"**/*\",\n pagesDir,\n matcher.extensions,\n (name: string) => name === \"api\" || name.startsWith(\"_\"),\n )) {\n const route = fileToRoute(file, pagesDir, matcher);\n if (route) routes.push(route);\n }\n\n // Sort: static routes first, then dynamic, then catch-all\n routes.sort((a, b) => {\n const diff = routePrecedence(a.pattern) - routePrecedence(b.pattern);\n return diff !== 0 ? diff : a.pattern.localeCompare(b.pattern);\n });\n\n return routes;\n}\n\n/**\n * Convert a file path relative to pages/ into a Route.\n */\nfunction fileToRoute(\n file: string,\n pagesDir: string,\n matcher: ValidFileMatcher,\n): Route | null {\n // Remove extension\n const withoutExt = matcher.stripExtension(file);\n if (withoutExt === file) return null;\n\n // Convert to URL segments\n const segments = withoutExt.split(path.sep);\n\n // Handle index files: pages/index.tsx -> /\n const lastSegment = segments[segments.length - 1];\n if (lastSegment === \"index\") {\n segments.pop();\n }\n\n const params: string[] = [];\n let isDynamic = false;\n\n // Convert Next.js dynamic segments to URL patterns\n const urlSegments = segments.map((segment) => {\n // Catch-all: [...slug] -> :slug+ (param names may contain hyphens)\n const catchAllMatch = segment.match(/^\\[\\.\\.\\.([\\w-]+)\\]$/);\n if (catchAllMatch) {\n isDynamic = true;\n params.push(catchAllMatch[1]);\n return `:${catchAllMatch[1]}+`;\n }\n\n // Optional catch-all: [[...slug]] -> :slug* (param names may contain hyphens)\n const optionalCatchAllMatch = segment.match(/^\\[\\[\\.\\.\\.([\\w-]+)\\]\\]$/);\n if (optionalCatchAllMatch) {\n isDynamic = true;\n params.push(optionalCatchAllMatch[1]);\n return `:${optionalCatchAllMatch[1]}*`;\n }\n\n // Dynamic segment: [id] -> :id (param names may contain hyphens)\n const dynamicMatch = segment.match(/^\\[([\\w-]+)\\]$/);\n if (dynamicMatch) {\n isDynamic = true;\n params.push(dynamicMatch[1]);\n return `:${dynamicMatch[1]}`;\n }\n\n return segment;\n });\n\n const pattern = \"/\" + urlSegments.join(\"/\");\n\n return {\n pattern: pattern === \"/\" ? \"/\" : pattern,\n filePath: path.join(pagesDir, file),\n isDynamic,\n params,\n };\n}\n\n/**\n * Match a URL path against a route pattern.\n * Returns the matched params or null if no match.\n */\nexport function matchRoute(\n url: string,\n routes: Route[],\n): { route: Route; params: Record<string, string | string[]> } | null {\n // Normalize: strip query string and trailing slash\n const pathname = url.split(\"?\")[0];\n let normalizedUrl =\n pathname === \"/\" ? \"/\" : pathname.replace(/\\/$/, \"\");\n try { normalizedUrl = decodeURIComponent(normalizedUrl); } catch { /* malformed percent-encoding — match as-is */ }\n\n for (const route of routes) {\n const params = matchPattern(normalizedUrl, route.pattern);\n if (params !== null) {\n return { route, params };\n }\n }\n\n return null;\n}\n\n/**\n * Scan the pages/api/ directory and return API routes.\n * Results are cached — call invalidateRouteCache() when files change.\n *\n * Follows Next.js conventions:\n * - pages/api/hello.ts -> /api/hello\n * - pages/api/users/[id].ts -> /api/users/:id\n */\nexport async function apiRouter(\n pagesDir: string,\n pageExtensions?: readonly string[],\n matcher?: ValidFileMatcher,\n): Promise<Route[]> {\n matcher ??= createValidFileMatcher(pageExtensions);\n const cacheKey = `api:${pagesDir}:${JSON.stringify(matcher.extensions)}`;\n const cached = routeCache.get(cacheKey);\n if (cached) return cached.promise;\n\n const promise = scanApiRoutes(pagesDir, matcher);\n routeCache.set(cacheKey, { routes: [], promise });\n const routes = await promise;\n routeCache.set(cacheKey, { routes, promise });\n return routes;\n}\n\nasync function scanApiRoutes(\n pagesDir: string,\n matcher: ValidFileMatcher,\n): Promise<Route[]> {\n const apiDir = path.join(pagesDir, \"api\");\n let files: string[];\n try {\n files = [];\n for await (const file of scanWithExtensions(\n \"**/*\",\n apiDir,\n matcher.extensions,\n )) {\n files.push(file);\n }\n } catch {\n files = [];\n }\n\n const routes: Route[] = [];\n\n for (const file of files) {\n // Reuse fileToRoute but pretend the file is under a virtual \"api/\" prefix\n const route = fileToRoute(path.join(\"api\", file), pagesDir, matcher);\n if (route) {\n routes.push(route);\n }\n }\n\n // Sort same as page routes\n routes.sort((a, b) => {\n const diff = routePrecedence(a.pattern) - routePrecedence(b.pattern);\n return diff !== 0 ? diff : a.pattern.localeCompare(b.pattern);\n });\n\n return routes;\n}\n\nfunction matchPattern(\n url: string,\n pattern: string,\n): Record<string, string | string[]> | null {\n const urlParts = url.split(\"/\").filter(Boolean);\n const patternParts = pattern.split(\"/\").filter(Boolean);\n\n const params: Record<string, string | string[]> = Object.create(null);\n\n for (let i = 0; i < patternParts.length; i++) {\n const pp = patternParts[i];\n\n // Catch-all: :slug+\n if (pp.endsWith(\"+\")) {\n const paramName = pp.slice(1, -1);\n const remaining = urlParts.slice(i);\n if (remaining.length === 0) return null;\n params[paramName] = remaining;\n return params;\n }\n\n // Optional catch-all: :slug*\n if (pp.endsWith(\"*\")) {\n const paramName = pp.slice(1, -1);\n const remaining = urlParts.slice(i);\n params[paramName] = remaining;\n return params;\n }\n\n // Dynamic segment: :id\n if (pp.startsWith(\":\")) {\n const paramName = pp.slice(1);\n if (i >= urlParts.length) return null;\n params[paramName] = urlParts[i];\n continue;\n }\n\n // Static segment\n if (i >= urlParts.length || urlParts[i] !== pp) return null;\n }\n\n // All pattern parts matched - check url doesn't have extra segments\n if (urlParts.length !== patternParts.length) return null;\n\n return params;\n}\n\n/**\n * Convert internal route pattern (e.g., \"/posts/:id\", \"/docs/:slug+\")\n * to Next.js bracket format (e.g., \"/posts/[id]\", \"/docs/[...slug]\").\n * Used for __NEXT_DATA__.page which apps expect in Next.js format.\n */\nexport function patternToNextFormat(pattern: string): string {\n return pattern\n .replace(/:([\\w-]+)\\*/g, \"[[...$1]]\") // optional catch-all :slug* -> [[...slug]]\n .replace(/:([\\w-]+)\\+/g, \"[...$1]\") // catch-all :slug+ -> [...slug]\n .replace(/:([\\w-]+)/g, \"[$1]\"); // dynamic :id -> [id]\n}\n"]}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Route precedence — lower score is higher priority.
3
+ * Matches Next.js specificity rules:
4
+ * 1. Static routes first (scored by segment count, more = more specific)
5
+ * 2. Dynamic segments penalized by position
6
+ * 3. Catch-all comes after dynamic
7
+ * 4. Optional catch-all last
8
+ * 5. Lexicographic tiebreaker for determinism
9
+ *
10
+ * Key insight: routes with a static prefix before a dynamic/catch-all segment
11
+ * should have higher priority than bare dynamic/catch-all routes at the same
12
+ * depth. E.g., /_sites/:subdomain should match before /:subdomain, and
13
+ * /_sites/:subdomain/:slug* should match before /:slug*.
14
+ *
15
+ * The static-prefix reduction uses a small value (-50 per segment) so that:
16
+ * - It beats the per-dynamic-segment penalty (100), placing prefix routes
17
+ * above their no-prefix equivalents.
18
+ * - It never goes negative, so purely-static routes (score 0) always win.
19
+ * - It is small enough that infix-static bonuses (-500) and catch-all
20
+ * penalties (1000+) are not swamped, preserving their relative ordering.
21
+ * E.g. /:locale/blog/:path+ (with infix "blog") correctly beats /:locale/:path+
22
+ * even when both share the same "locale-test" static prefix.
23
+ */
24
+ export declare function routePrecedence(pattern: string): number;
25
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/routing/utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA+CvD"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Route precedence — lower score is higher priority.
3
+ * Matches Next.js specificity rules:
4
+ * 1. Static routes first (scored by segment count, more = more specific)
5
+ * 2. Dynamic segments penalized by position
6
+ * 3. Catch-all comes after dynamic
7
+ * 4. Optional catch-all last
8
+ * 5. Lexicographic tiebreaker for determinism
9
+ *
10
+ * Key insight: routes with a static prefix before a dynamic/catch-all segment
11
+ * should have higher priority than bare dynamic/catch-all routes at the same
12
+ * depth. E.g., /_sites/:subdomain should match before /:subdomain, and
13
+ * /_sites/:subdomain/:slug* should match before /:slug*.
14
+ *
15
+ * The static-prefix reduction uses a small value (-50 per segment) so that:
16
+ * - It beats the per-dynamic-segment penalty (100), placing prefix routes
17
+ * above their no-prefix equivalents.
18
+ * - It never goes negative, so purely-static routes (score 0) always win.
19
+ * - It is small enough that infix-static bonuses (-500) and catch-all
20
+ * penalties (1000+) are not swamped, preserving their relative ordering.
21
+ * E.g. /:locale/blog/:path+ (with infix "blog") correctly beats /:locale/:path+
22
+ * even when both share the same "locale-test" static prefix.
23
+ */
24
+ export function routePrecedence(pattern) {
25
+ const parts = pattern.split("/").filter(Boolean);
26
+ let score = 0;
27
+ let staticPrefixCount = 0;
28
+ for (const p of parts) {
29
+ if (p.startsWith(":") || p.endsWith("+") || p.endsWith("*"))
30
+ break;
31
+ staticPrefixCount++;
32
+ }
33
+ for (let i = 0; i < parts.length; i++) {
34
+ const p = parts[i];
35
+ if (p.endsWith("+")) {
36
+ score += 1000 + i; // catch-all: moderate penalty
37
+ }
38
+ else if (p.endsWith("*")) {
39
+ score += 2000 + i; // optional catch-all: high penalty
40
+ }
41
+ else if (p.startsWith(":")) {
42
+ score += 100 + i; // dynamic: small penalty by position
43
+ }
44
+ else if (i >= staticPrefixCount) {
45
+ // Static segment interleaved after a dynamic segment (infix static).
46
+ // Boost priority — more specific than a bare catch-all.
47
+ // The -500 compounds for each infix static segment, so routes with more
48
+ // static infixes score lower (higher priority) than those with fewer.
49
+ // E.g. /:a/x/y/:b+ (-1000) beats /:a/x/:b+ (-500) beats /:a/:b+ (0).
50
+ // This is intentional: more static constraints = more specific route.
51
+ score -= 500;
52
+ }
53
+ // Static prefix segments (i < staticPrefixCount) are handled below.
54
+ }
55
+ // Apply a small reduction per static-prefix segment for routes that also
56
+ // contain dynamic segments. This ensures /_sites/:subdomain sorts above
57
+ // /:subdomain, and /_sites/:slug* sorts above /:slug*, while keeping the
58
+ // final score positive (so purely-static routes at score=0 always win).
59
+ //
60
+ // 50 is deliberately smaller than the dynamic-segment penalty (100) so
61
+ // one static prefix segment is enough to beat one bare dynamic segment,
62
+ // and smaller than the infix-static bonus (500) so that infix ordering is
63
+ // not disturbed between two routes that share the same prefix.
64
+ const isDynamic = parts.some((p) => p.startsWith(":") || p.endsWith("+") || p.endsWith("*"));
65
+ if (isDynamic && staticPrefixCount > 0) {
66
+ score -= staticPrefixCount * 50;
67
+ }
68
+ return score;
69
+ }
70
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/routing/utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACjD,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,IAAI,iBAAiB,GAAG,CAAC,CAAC;IAC1B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,MAAM;QACnE,iBAAiB,EAAE,CAAC;IACtB,CAAC;IAED,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACnB,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACpB,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,8BAA8B;QACnD,CAAC;aAAM,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,mCAAmC;QACxD,CAAC;aAAM,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7B,KAAK,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,qCAAqC;QACzD,CAAC;aAAM,IAAI,CAAC,IAAI,iBAAiB,EAAE,CAAC;YAClC,qEAAqE;YACrE,wDAAwD;YACxD,wEAAwE;YACxE,sEAAsE;YACtE,qEAAqE;YACrE,sEAAsE;YACtE,KAAK,IAAI,GAAG,CAAC;QACf,CAAC;QACD,oEAAoE;IACtE,CAAC;IAED,yEAAyE;IACzE,wEAAwE;IACxE,yEAAyE;IACzE,wEAAwE;IACxE,EAAE;IACF,uEAAuE;IACvE,wEAAwE;IACxE,0EAA0E;IAC1E,+DAA+D;IAC/D,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAC1B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAC/D,CAAC;IACF,IAAI,SAAS,IAAI,iBAAiB,GAAG,CAAC,EAAE,CAAC;QACvC,KAAK,IAAI,iBAAiB,GAAG,EAAE,CAAC;IAClC,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["/**\n * Route precedence — lower score is higher priority.\n * Matches Next.js specificity rules:\n * 1. Static routes first (scored by segment count, more = more specific)\n * 2. Dynamic segments penalized by position\n * 3. Catch-all comes after dynamic\n * 4. Optional catch-all last\n * 5. Lexicographic tiebreaker for determinism\n *\n * Key insight: routes with a static prefix before a dynamic/catch-all segment\n * should have higher priority than bare dynamic/catch-all routes at the same\n * depth. E.g., /_sites/:subdomain should match before /:subdomain, and\n * /_sites/:subdomain/:slug* should match before /:slug*.\n *\n * The static-prefix reduction uses a small value (-50 per segment) so that:\n * - It beats the per-dynamic-segment penalty (100), placing prefix routes\n * above their no-prefix equivalents.\n * - It never goes negative, so purely-static routes (score 0) always win.\n * - It is small enough that infix-static bonuses (-500) and catch-all\n * penalties (1000+) are not swamped, preserving their relative ordering.\n * E.g. /:locale/blog/:path+ (with infix \"blog\") correctly beats /:locale/:path+\n * even when both share the same \"locale-test\" static prefix.\n */\nexport function routePrecedence(pattern: string): number {\n const parts = pattern.split(\"/\").filter(Boolean);\n let score = 0;\n\n let staticPrefixCount = 0;\n for (const p of parts) {\n if (p.startsWith(\":\") || p.endsWith(\"+\") || p.endsWith(\"*\")) break;\n staticPrefixCount++;\n }\n\n for (let i = 0; i < parts.length; i++) {\n const p = parts[i];\n if (p.endsWith(\"+\")) {\n score += 1000 + i; // catch-all: moderate penalty\n } else if (p.endsWith(\"*\")) {\n score += 2000 + i; // optional catch-all: high penalty\n } else if (p.startsWith(\":\")) {\n score += 100 + i; // dynamic: small penalty by position\n } else if (i >= staticPrefixCount) {\n // Static segment interleaved after a dynamic segment (infix static).\n // Boost priority — more specific than a bare catch-all.\n // The -500 compounds for each infix static segment, so routes with more\n // static infixes score lower (higher priority) than those with fewer.\n // E.g. /:a/x/y/:b+ (-1000) beats /:a/x/:b+ (-500) beats /:a/:b+ (0).\n // This is intentional: more static constraints = more specific route.\n score -= 500;\n }\n // Static prefix segments (i < staticPrefixCount) are handled below.\n }\n\n // Apply a small reduction per static-prefix segment for routes that also\n // contain dynamic segments. This ensures /_sites/:subdomain sorts above\n // /:subdomain, and /_sites/:slug* sorts above /:slug*, while keeping the\n // final score positive (so purely-static routes at score=0 always win).\n //\n // 50 is deliberately smaller than the dynamic-segment penalty (100) so\n // one static prefix segment is enough to beat one bare dynamic segment,\n // and smaller than the infix-static bonus (500) so that infix ordering is\n // not disturbed between two routes that share the same prefix.\n const isDynamic = parts.some(\n (p) => p.startsWith(\":\") || p.endsWith(\"+\") || p.endsWith(\"*\"),\n );\n if (isDynamic && staticPrefixCount > 0) {\n score -= staticPrefixCount * 50;\n }\n\n return score;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"app-dev-server.d.ts","sourceRoot":"","sources":["../../src/server/app-dev-server.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAKtF;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,QAAQ,CAAC,EAAE;QACT,WAAW,EAAE,WAAW,EAAE,CAAC;QAC3B,UAAU,EAAE,WAAW,EAAE,CAAC;QAC1B,QAAQ,EAAE,WAAW,EAAE,CAAC;KACzB,CAAC;IACF,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,4GAA4G;IAC5G,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uGAAuG;IACvG,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,QAAQ,EAAE,EAClB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,EAC9B,cAAc,CAAC,EAAE,iBAAiB,EAAE,EACpC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,EAC/B,QAAQ,CAAC,EAAE,MAAM,EACjB,aAAa,CAAC,EAAE,OAAO,EACvB,MAAM,CAAC,EAAE,eAAe,GACvB,MAAM,CAurER;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAibzC;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CA4S7C"}
1
+ {"version":3,"file":"app-dev-server.d.ts","sourceRoot":"","sources":["../../src/server/app-dev-server.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAKtF;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,QAAQ,CAAC,EAAE;QACT,WAAW,EAAE,WAAW,EAAE,CAAC;QAC3B,UAAU,EAAE,WAAW,EAAE,CAAC;QAC1B,QAAQ,EAAE,WAAW,EAAE,CAAC;KACzB,CAAC;IACF,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,4GAA4G;IAC5G,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uGAAuG;IACvG,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,QAAQ,EAAE,EAClB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,EAC9B,cAAc,CAAC,EAAE,iBAAiB,EAAE,EACpC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,EAC/B,QAAQ,CAAC,EAAE,MAAM,EACjB,aAAa,CAAC,EAAE,OAAO,EACvB,MAAM,CAAC,EAAE,eAAe,GACvB,MAAM,CAgvER;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAibzC;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CA+T7C"}
@@ -397,7 +397,7 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
397
397
  }
398
398
  }
399
399
  const resolvedMetadata = metadataList.length > 0 ? mergeMetadata(metadataList) : null;
400
- const resolvedViewport = viewportList.length > 0 ? mergeViewport(viewportList) : null;
400
+ const resolvedViewport = mergeViewport(viewportList);
401
401
 
402
402
  // Build element: metadata head + noindex meta + boundary component wrapped in layouts
403
403
  // Always include charset and default viewport for parity with Next.js.
@@ -405,8 +405,7 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
405
405
  const noindexMeta = createElement("meta", { name: "robots", content: "noindex" });
406
406
  const headElements = [charsetMeta, noindexMeta];
407
407
  if (resolvedMetadata) headElements.push(createElement(MetadataHead, { metadata: resolvedMetadata }));
408
- const effectiveViewport = resolvedViewport ?? { width: "device-width", initialScale: 1 };
409
- headElements.push(createElement(ViewportHead, { viewport: effectiveViewport }));
408
+ headElements.push(createElement(ViewportHead, { viewport: resolvedViewport }));
410
409
  let element = createElement(Fragment, null, ...headElements, createElement(BoundaryComponent));
411
410
  if (isRscRequest) {
412
411
  // For RSC requests (client-side navigation), wrap the element with the same
@@ -671,7 +670,7 @@ async function buildPageElement(route, params, opts, searchParams) {
671
670
  if (pageVp) viewportList.push(pageVp);
672
671
  }
673
672
  const resolvedMetadata = metadataList.length > 0 ? mergeMetadata(metadataList) : null;
674
- const resolvedViewport = viewportList.length > 0 ? mergeViewport(viewportList) : null;
673
+ const resolvedViewport = mergeViewport(viewportList);
675
674
 
676
675
  // Build nested layout tree from outermost to innermost.
677
676
  // Next.js 16 passes params/searchParams as Promises (async pattern)
@@ -712,9 +711,7 @@ async function buildPageElement(route, params, opts, searchParams) {
712
711
  // Always emit <meta charset="utf-8"> — Next.js includes this on every page
713
712
  headElements.push(createElement("meta", { charSet: "utf-8" }));
714
713
  if (resolvedMetadata) headElements.push(createElement(MetadataHead, { metadata: resolvedMetadata }));
715
- // Default viewport to standard responsive settings when none is exported
716
- const effectiveViewport = resolvedViewport ?? { width: "device-width", initialScale: 1 };
717
- headElements.push(createElement(ViewportHead, { viewport: effectiveViewport }));
714
+ headElements.push(createElement(ViewportHead, { viewport: resolvedViewport }));
718
715
  element = createElement(Fragment, null, ...headElements, element);
719
716
  }
720
717
 
@@ -1231,18 +1228,41 @@ export default async function handler(request) {
1231
1228
  _runWithPrivateCache(() =>
1232
1229
  runWithFetchCache(async () => {
1233
1230
  const __reqCtx = __buildRequestContext(request);
1234
- const response = await _handleRequest(request, __reqCtx);
1231
+ // Per-request container for middleware state. Passed into
1232
+ // _handleRequest which fills in .headers and .status;
1233
+ // avoids module-level variables that race on Workers.
1234
+ const _mwCtx = { headers: null, status: null };
1235
+ const response = await _handleRequest(request, __reqCtx, _mwCtx);
1235
1236
  // Apply custom headers from next.config.js to non-redirect responses.
1236
1237
  // Skip redirects (3xx) because Response.redirect() creates immutable headers,
1237
1238
  // and Next.js doesn't apply custom headers to redirects anyway.
1238
- if (__configHeaders.length && response && response.headers && !(response.status >= 300 && response.status < 400)) {
1239
- const url = new URL(request.url);
1240
- let pathname;
1241
- try { pathname = __normalizePath(decodeURIComponent(url.pathname)); } catch { pathname = url.pathname; }
1242
- ${bp ? `if (pathname.startsWith(${JSON.stringify(bp)})) pathname = pathname.slice(${JSON.stringify(bp)}.length) || "/";` : ""}
1243
- const extraHeaders = __applyConfigHeaders(pathname, __reqCtx);
1244
- for (const h of extraHeaders) {
1245
- response.headers.set(h.key, h.value);
1239
+ if (response && response.headers && !(response.status >= 300 && response.status < 400)) {
1240
+ if (__configHeaders.length) {
1241
+ const url = new URL(request.url);
1242
+ let pathname;
1243
+ try { pathname = __normalizePath(decodeURIComponent(url.pathname)); } catch { pathname = url.pathname; }
1244
+ ${bp ? `if (pathname.startsWith(${JSON.stringify(bp)})) pathname = pathname.slice(${JSON.stringify(bp)}.length) || "/";` : ""}
1245
+ const extraHeaders = __applyConfigHeaders(pathname, __reqCtx);
1246
+ for (const h of extraHeaders) {
1247
+ // Use append() for headers where multiple values must coexist
1248
+ // (Vary, Set-Cookie). Using set() on these would destroy
1249
+ // existing values like "Vary: RSC, Accept" which are critical
1250
+ // for correct CDN caching behavior.
1251
+ const lk = h.key.toLowerCase();
1252
+ if (lk === "vary" || lk === "set-cookie") {
1253
+ response.headers.append(h.key, h.value);
1254
+ } else {
1255
+ response.headers.set(h.key, h.value);
1256
+ }
1257
+ }
1258
+ }
1259
+ // Merge middleware response headers into the final response.
1260
+ // This runs at the top level so every response path (route
1261
+ // handlers, server actions, metadata, errors, etc.) gets them.
1262
+ if (_mwCtx.headers) {
1263
+ for (const [key, value] of _mwCtx.headers) {
1264
+ response.headers.append(key, value);
1265
+ }
1246
1266
  }
1247
1267
  }
1248
1268
  return response;
@@ -1253,7 +1273,13 @@ export default async function handler(request) {
1253
1273
  );
1254
1274
  }
1255
1275
 
1256
- async function _handleRequest(request, __reqCtx) {
1276
+ async function _handleRequest(request, __reqCtx, _mwCtx) {
1277
+ const __reqStart = process.env.NODE_ENV !== "production" ? performance.now() : 0;
1278
+ let __compileEnd;
1279
+ let __renderEnd;
1280
+ // __reqStart is included in the timing header so the Node logging middleware
1281
+ // can compute true compile time as: handlerStart - middlewareStart.
1282
+ // Format: "handlerStart,compileMs,renderMs" - all as integers (ms). Dev-only.
1257
1283
  const url = new URL(request.url);
1258
1284
 
1259
1285
  // ── Cross-origin request protection ─────────────────────────────────
@@ -1300,7 +1326,11 @@ async function _handleRequest(request, __reqCtx) {
1300
1326
 
1301
1327
  // ── Apply redirects from next.config.js ───────────────────────────────
1302
1328
  if (__configRedirects.length) {
1303
- const __redir = __applyConfigRedirects(pathname, __reqCtx);
1329
+ // Strip .rsc suffix before matching redirect rules - RSC (client-side nav) requests
1330
+ // arrive as /some/path.rsc but redirect patterns are defined without it (e.g.
1331
+ // /some/path). Without this, soft-nav fetches bypass all config redirects.
1332
+ const __redirPathname = pathname.endsWith(".rsc") ? pathname.slice(0, -4) : pathname;
1333
+ const __redir = __applyConfigRedirects(__redirPathname, __reqCtx);
1304
1334
  if (__redir) {
1305
1335
  const __redirDest = __sanitizeDestination(
1306
1336
  __basePath && !__redir.destination.startsWith(__basePath)
@@ -1316,7 +1346,9 @@ async function _handleRequest(request, __reqCtx) {
1316
1346
 
1317
1347
  // ── Apply beforeFiles rewrites from next.config.js ────────────────────
1318
1348
  if (__configRewrites.beforeFiles && __configRewrites.beforeFiles.length) {
1319
- const __rewritten = __applyConfigRewrites(pathname, __configRewrites.beforeFiles, __reqCtx);
1349
+ // Strip .rsc suffix before matching rewrite rules — same reason as redirects above.
1350
+ const __rewritePathname = pathname.endsWith(".rsc") ? pathname.slice(0, -4) : pathname;
1351
+ const __rewritten = __applyConfigRewrites(__rewritePathname, __configRewrites.beforeFiles, __reqCtx);
1320
1352
  if (__rewritten) {
1321
1353
  if (__isExternalUrl(__rewritten)) {
1322
1354
  setHeadersContext(null);
@@ -1330,10 +1362,9 @@ async function _handleRequest(request, __reqCtx) {
1330
1362
  const isRscRequest = pathname.endsWith(".rsc") || request.headers.get("accept")?.includes("text/x-component");
1331
1363
  let cleanPathname = pathname.replace(/\\.rsc$/, "");
1332
1364
 
1333
- // Middleware response headers to merge into the final response
1334
- let _middlewareResponseHeaders = null;
1335
- // Custom status code from middleware rewrite (e.g. NextResponse.rewrite(url, { status: 403 }))
1336
- let _middlewareRewriteStatus = null;
1365
+ // Middleware response headers and custom rewrite status are stored in
1366
+ // _mwCtx (per-request container) so handler() can merge them into
1367
+ // every response path without module-level state that races on Workers.
1337
1368
 
1338
1369
  ${middlewarePath ? `
1339
1370
  // Run proxy/middleware if present and path matches.
@@ -1367,10 +1398,10 @@ async function _handleRequest(request, __reqCtx) {
1367
1398
  // headers are kept so applyMiddlewareRequestHeaders() can unpack them;
1368
1399
  // the blanket strip loop after that call removes every remaining
1369
1400
  // x-middleware-* header before the set is merged into the response.
1370
- _middlewareResponseHeaders = new Headers();
1401
+ _mwCtx.headers = new Headers();
1371
1402
  for (const [key, value] of mwResponse.headers) {
1372
1403
  if (key !== "x-middleware-next" && key !== "x-middleware-rewrite") {
1373
- _middlewareResponseHeaders.append(key, value);
1404
+ _mwCtx.headers.append(key, value);
1374
1405
  }
1375
1406
  }
1376
1407
  } else {
@@ -1385,13 +1416,13 @@ async function _handleRequest(request, __reqCtx) {
1385
1416
  cleanPathname = rewriteParsed.pathname;
1386
1417
  // Capture custom status code from rewrite (e.g. NextResponse.rewrite(url, { status: 403 }))
1387
1418
  if (mwResponse.status !== 200) {
1388
- _middlewareRewriteStatus = mwResponse.status;
1419
+ _mwCtx.status = mwResponse.status;
1389
1420
  }
1390
1421
  // Also save any other headers from the rewrite response
1391
- _middlewareResponseHeaders = new Headers();
1422
+ _mwCtx.headers = new Headers();
1392
1423
  for (const [key, value] of mwResponse.headers) {
1393
1424
  if (key !== "x-middleware-next" && key !== "x-middleware-rewrite") {
1394
- _middlewareResponseHeaders.append(key, value);
1425
+ _mwCtx.headers.append(key, value);
1395
1426
  }
1396
1427
  }
1397
1428
  } else {
@@ -1411,11 +1442,11 @@ async function _handleRequest(request, __reqCtx) {
1411
1442
  // request headers. Strip ALL x-middleware-* headers from the set that will
1412
1443
  // be merged into the outgoing HTTP response — this prefix is reserved for
1413
1444
  // internal routing signals and must never reach clients.
1414
- if (_middlewareResponseHeaders) {
1415
- applyMiddlewareRequestHeaders(_middlewareResponseHeaders);
1416
- for (const key of [..._middlewareResponseHeaders.keys()]) {
1445
+ if (_mwCtx.headers) {
1446
+ applyMiddlewareRequestHeaders(_mwCtx.headers);
1447
+ for (const key of [..._mwCtx.headers.keys()]) {
1417
1448
  if (key.startsWith("x-middleware-")) {
1418
- _middlewareResponseHeaders.delete(key);
1449
+ _mwCtx.headers.delete(key);
1419
1450
  }
1420
1451
  }
1421
1452
  }
@@ -2098,6 +2129,9 @@ async function _handleRequest(request, __reqCtx) {
2098
2129
  console.error = _origConsoleError;
2099
2130
  }
2100
2131
 
2132
+ // Mark end of compile phase: route matching, middleware, tree building are done.
2133
+ if (process.env.NODE_ENV !== "production") __compileEnd = performance.now();
2134
+
2101
2135
  // Render to RSC stream
2102
2136
  const rscStream = renderToReadableStream(element, { onError: rscOnError });
2103
2137
 
@@ -2120,13 +2154,23 @@ async function _handleRequest(request, __reqCtx) {
2120
2154
  } else if (revalidateSeconds) {
2121
2155
  responseHeaders["Cache-Control"] = "s-maxage=" + revalidateSeconds + ", stale-while-revalidate";
2122
2156
  }
2123
- // Merge middleware response headers into the RSC response
2124
- if (_middlewareResponseHeaders) {
2125
- for (const [key, value] of _middlewareResponseHeaders) {
2126
- responseHeaders[key] = value;
2127
- }
2128
- }
2129
- return new Response(rscStream, { status: _middlewareRewriteStatus || 200, headers: responseHeaders });
2157
+ // Middleware response headers are merged by the handler() wrapper.
2158
+ // Attach internal timing header so the dev server middleware can log it.
2159
+ // Format: "handlerStart,compileMs,renderMs"
2160
+ // handlerStart - absolute performance.now() when _handleRequest began,
2161
+ // used by the logging middleware to compute true compile
2162
+ // time as (handlerStart - middlewareReqStart).
2163
+ // compileMs - time inside the handler before renderToReadableStream.
2164
+ // -1 sentinel means compile time is not measured.
2165
+ // renderMs - -1 sentinel for RSC-only (soft-nav) responses, since
2166
+ // rendering is handled asynchronously by the client. The
2167
+ // logging middleware computes render time as totalMs - compileMs.
2168
+ if (process.env.NODE_ENV !== "production") {
2169
+ const handlerStart = Math.round(__reqStart);
2170
+ const compileMs = __compileEnd !== undefined ? Math.round(__compileEnd - __reqStart) : -1;
2171
+ responseHeaders["x-vinext-timing"] = handlerStart + "," + compileMs + ",-1";
2172
+ }
2173
+ return new Response(rscStream, { status: _mwCtx.status || 200, headers: responseHeaders });
2130
2174
  }
2131
2175
 
2132
2176
  // Collect font data from RSC environment before passing to SSR
@@ -2152,6 +2196,8 @@ async function _handleRequest(request, __reqCtx) {
2152
2196
  try {
2153
2197
  const ssrEntry = await import.meta.viteRsc.loadModule("ssr", "index");
2154
2198
  htmlStream = await ssrEntry.handleSsr(rscStream, _getNavigationContext(), fontData);
2199
+ // Shell render complete; Suspense boundaries stream asynchronously
2200
+ if (process.env.NODE_ENV !== "production") __renderEnd = performance.now();
2155
2201
  } catch (ssrErr) {
2156
2202
  const specialResponse = await handleRenderError(ssrErr);
2157
2203
  if (specialResponse) return specialResponse;
@@ -2176,16 +2222,27 @@ async function _handleRequest(request, __reqCtx) {
2176
2222
  if (fontLinkHeader) {
2177
2223
  response.headers.set("Link", fontLinkHeader);
2178
2224
  }
2179
- // Merge middleware response headers into the final response
2180
- if (_middlewareResponseHeaders) {
2181
- for (const [key, value] of _middlewareResponseHeaders) {
2182
- response.headers.append(key, value);
2183
- }
2225
+ // Middleware response headers are merged by the handler() wrapper.
2226
+ // Attach internal timing header so the dev server middleware can log it.
2227
+ // Format: "handlerStart,compileMs,renderMs"
2228
+ // handlerStart - absolute performance.now() when _handleRequest began,
2229
+ // used by the logging middleware to compute true compile
2230
+ // time as (handlerStart - middlewareReqStart).
2231
+ // compileMs - time inside the handler before renderToReadableStream.
2232
+ // renderMs - time from renderToReadableStream to handleSsr completion,
2233
+ // or -1 sentinel if not measured (falls back to totalMs - compileMs).
2234
+ if (process.env.NODE_ENV !== "production") {
2235
+ const handlerStart = Math.round(__reqStart);
2236
+ const compileMs = __compileEnd !== undefined ? Math.round(__compileEnd - __reqStart) : -1;
2237
+ const renderMs = __renderEnd !== undefined && __compileEnd !== undefined
2238
+ ? Math.round(__renderEnd - __compileEnd)
2239
+ : -1;
2240
+ response.headers.set("x-vinext-timing", handlerStart + "," + compileMs + "," + renderMs);
2184
2241
  }
2185
2242
  // Apply custom status code from middleware rewrite
2186
- if (_middlewareRewriteStatus) {
2243
+ if (_mwCtx.status) {
2187
2244
  return new Response(response.body, {
2188
- status: _middlewareRewriteStatus,
2245
+ status: _mwCtx.status,
2189
2246
  headers: response.headers,
2190
2247
  });
2191
2248
  }
@@ -2932,7 +2989,12 @@ async function main() {
2932
2989
  // Checks the prefetch cache (populated by <Link> IntersectionObserver and
2933
2990
  // router.prefetch()) before making a network request. This makes navigation
2934
2991
  // near-instant for prefetched routes.
2935
- window.__VINEXT_RSC_NAVIGATE__ = async function navigateRsc(href) {
2992
+ window.__VINEXT_RSC_NAVIGATE__ = async function navigateRsc(href, __redirectDepth) {
2993
+ if ((__redirectDepth || 0) > 10) {
2994
+ console.error("[vinext] Too many RSC redirects — aborting navigation to prevent infinite loop.");
2995
+ window.location.href = href;
2996
+ return;
2997
+ }
2936
2998
  try {
2937
2999
  const url = new URL(href, window.location.origin);
2938
3000
  const rscUrl = toRscUrl(url.pathname + url.search);
@@ -2958,6 +3020,20 @@ async function main() {
2958
3020
  });
2959
3021
  }
2960
3022
 
3023
+ // Detect if fetch followed a redirect: compare the final response URL to
3024
+ // what we requested. If they differ, the server issued a 3xx — push the
3025
+ // canonical destination URL into history before rendering.
3026
+ const __finalUrl = new URL(navResponse.url);
3027
+ const __requestedUrl = new URL(rscUrl, window.location.origin);
3028
+ if (__finalUrl.pathname !== __requestedUrl.pathname) {
3029
+ // Strip .rsc suffix from the final URL to get the page path for history.
3030
+ // Use replaceState instead of pushState: the caller (navigateImpl) already
3031
+ // pushed the pre-redirect URL; replacing it avoids a stale history entry.
3032
+ const __destPath = __finalUrl.pathname.replace(/\\.rsc$/, "") + __finalUrl.search;
3033
+ window.history.replaceState(null, "", __destPath);
3034
+ return window.__VINEXT_RSC_NAVIGATE__(__destPath, (__redirectDepth || 0) + 1);
3035
+ }
3036
+
2961
3037
  // Update useParams() with route params from the server before re-rendering
2962
3038
  const navParamsHeader = navResponse.headers.get("X-Vinext-Params");
2963
3039
  if (navParamsHeader) {