vinext 0.0.0 → 0.0.1

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 (272) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/dist/build/static-export.d.ts +78 -0
  4. package/dist/build/static-export.d.ts.map +1 -0
  5. package/dist/build/static-export.js +553 -0
  6. package/dist/build/static-export.js.map +1 -0
  7. package/dist/check.d.ts +52 -0
  8. package/dist/check.d.ts.map +1 -0
  9. package/dist/check.js +483 -0
  10. package/dist/check.js.map +1 -0
  11. package/dist/cli.d.ts +15 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +565 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/client/entry.d.ts +2 -0
  16. package/dist/client/entry.d.ts.map +1 -0
  17. package/dist/client/entry.js +85 -0
  18. package/dist/client/entry.js.map +1 -0
  19. package/dist/cloudflare/index.d.ts +8 -0
  20. package/dist/cloudflare/index.d.ts.map +1 -0
  21. package/dist/cloudflare/index.js +8 -0
  22. package/dist/cloudflare/index.js.map +1 -0
  23. package/dist/cloudflare/kv-cache-handler.d.ts +68 -0
  24. package/dist/cloudflare/kv-cache-handler.d.ts.map +1 -0
  25. package/dist/cloudflare/kv-cache-handler.js +304 -0
  26. package/dist/cloudflare/kv-cache-handler.js.map +1 -0
  27. package/dist/cloudflare/tpr.d.ts +78 -0
  28. package/dist/cloudflare/tpr.d.ts.map +1 -0
  29. package/dist/cloudflare/tpr.js +672 -0
  30. package/dist/cloudflare/tpr.js.map +1 -0
  31. package/dist/config/config-matchers.d.ts +106 -0
  32. package/dist/config/config-matchers.d.ts.map +1 -0
  33. package/dist/config/config-matchers.js +499 -0
  34. package/dist/config/config-matchers.js.map +1 -0
  35. package/dist/config/next-config.d.ts +153 -0
  36. package/dist/config/next-config.d.ts.map +1 -0
  37. package/dist/config/next-config.js +274 -0
  38. package/dist/config/next-config.js.map +1 -0
  39. package/dist/deploy.d.ts +87 -0
  40. package/dist/deploy.d.ts.map +1 -0
  41. package/dist/deploy.js +644 -0
  42. package/dist/deploy.js.map +1 -0
  43. package/dist/index.d.ts +156 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +3287 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/init.d.ts +55 -0
  48. package/dist/init.d.ts.map +1 -0
  49. package/dist/init.js +201 -0
  50. package/dist/init.js.map +1 -0
  51. package/dist/routing/app-router.d.ts +96 -0
  52. package/dist/routing/app-router.d.ts.map +1 -0
  53. package/dist/routing/app-router.js +815 -0
  54. package/dist/routing/app-router.js.map +1 -0
  55. package/dist/routing/pages-router.d.ts +52 -0
  56. package/dist/routing/pages-router.d.ts.map +1 -0
  57. package/dist/routing/pages-router.js +239 -0
  58. package/dist/routing/pages-router.js.map +1 -0
  59. package/dist/server/api-handler.d.ts +18 -0
  60. package/dist/server/api-handler.d.ts.map +1 -0
  61. package/dist/server/api-handler.js +169 -0
  62. package/dist/server/api-handler.js.map +1 -0
  63. package/dist/server/app-dev-server.d.ts +42 -0
  64. package/dist/server/app-dev-server.d.ts.map +1 -0
  65. package/dist/server/app-dev-server.js +2718 -0
  66. package/dist/server/app-dev-server.js.map +1 -0
  67. package/dist/server/app-router-entry.d.ts +18 -0
  68. package/dist/server/app-router-entry.d.ts.map +1 -0
  69. package/dist/server/app-router-entry.js +34 -0
  70. package/dist/server/app-router-entry.js.map +1 -0
  71. package/dist/server/dev-server.d.ts +40 -0
  72. package/dist/server/dev-server.d.ts.map +1 -0
  73. package/dist/server/dev-server.js +758 -0
  74. package/dist/server/dev-server.js.map +1 -0
  75. package/dist/server/html.d.ts +22 -0
  76. package/dist/server/html.d.ts.map +1 -0
  77. package/dist/server/html.js +29 -0
  78. package/dist/server/html.js.map +1 -0
  79. package/dist/server/image-optimization.d.ts +56 -0
  80. package/dist/server/image-optimization.d.ts.map +1 -0
  81. package/dist/server/image-optimization.js +103 -0
  82. package/dist/server/image-optimization.js.map +1 -0
  83. package/dist/server/instrumentation.d.ts +68 -0
  84. package/dist/server/instrumentation.d.ts.map +1 -0
  85. package/dist/server/instrumentation.js +90 -0
  86. package/dist/server/instrumentation.js.map +1 -0
  87. package/dist/server/isr-cache.d.ts +61 -0
  88. package/dist/server/isr-cache.d.ts.map +1 -0
  89. package/dist/server/isr-cache.js +134 -0
  90. package/dist/server/isr-cache.js.map +1 -0
  91. package/dist/server/metadata-routes.d.ts +103 -0
  92. package/dist/server/metadata-routes.d.ts.map +1 -0
  93. package/dist/server/metadata-routes.js +270 -0
  94. package/dist/server/metadata-routes.js.map +1 -0
  95. package/dist/server/middleware.d.ts +77 -0
  96. package/dist/server/middleware.d.ts.map +1 -0
  97. package/dist/server/middleware.js +228 -0
  98. package/dist/server/middleware.js.map +1 -0
  99. package/dist/server/prod-server.d.ts +78 -0
  100. package/dist/server/prod-server.d.ts.map +1 -0
  101. package/dist/server/prod-server.js +712 -0
  102. package/dist/server/prod-server.js.map +1 -0
  103. package/dist/shims/amp.d.ts +17 -0
  104. package/dist/shims/amp.d.ts.map +1 -0
  105. package/dist/shims/amp.js +21 -0
  106. package/dist/shims/amp.js.map +1 -0
  107. package/dist/shims/app.d.ts +12 -0
  108. package/dist/shims/app.d.ts.map +1 -0
  109. package/dist/shims/app.js +2 -0
  110. package/dist/shims/app.js.map +1 -0
  111. package/dist/shims/cache-runtime.d.ts +68 -0
  112. package/dist/shims/cache-runtime.d.ts.map +1 -0
  113. package/dist/shims/cache-runtime.js +437 -0
  114. package/dist/shims/cache-runtime.js.map +1 -0
  115. package/dist/shims/cache.d.ts +243 -0
  116. package/dist/shims/cache.d.ts.map +1 -0
  117. package/dist/shims/cache.js +415 -0
  118. package/dist/shims/cache.js.map +1 -0
  119. package/dist/shims/client-only.d.ts +18 -0
  120. package/dist/shims/client-only.d.ts.map +1 -0
  121. package/dist/shims/client-only.js +18 -0
  122. package/dist/shims/client-only.js.map +1 -0
  123. package/dist/shims/config.d.ts +27 -0
  124. package/dist/shims/config.d.ts.map +1 -0
  125. package/dist/shims/config.js +30 -0
  126. package/dist/shims/config.js.map +1 -0
  127. package/dist/shims/constants.d.ts +13 -0
  128. package/dist/shims/constants.d.ts.map +1 -0
  129. package/dist/shims/constants.js +13 -0
  130. package/dist/shims/constants.js.map +1 -0
  131. package/dist/shims/document.d.ts +33 -0
  132. package/dist/shims/document.d.ts.map +1 -0
  133. package/dist/shims/document.js +32 -0
  134. package/dist/shims/document.js.map +1 -0
  135. package/dist/shims/dynamic.d.ts +33 -0
  136. package/dist/shims/dynamic.d.ts.map +1 -0
  137. package/dist/shims/dynamic.js +148 -0
  138. package/dist/shims/dynamic.js.map +1 -0
  139. package/dist/shims/error-boundary.d.ts +33 -0
  140. package/dist/shims/error-boundary.d.ts.map +1 -0
  141. package/dist/shims/error-boundary.js +88 -0
  142. package/dist/shims/error-boundary.js.map +1 -0
  143. package/dist/shims/error.d.ts +16 -0
  144. package/dist/shims/error.d.ts.map +1 -0
  145. package/dist/shims/error.js +45 -0
  146. package/dist/shims/error.js.map +1 -0
  147. package/dist/shims/fetch-cache.d.ts +61 -0
  148. package/dist/shims/fetch-cache.d.ts.map +1 -0
  149. package/dist/shims/fetch-cache.js +307 -0
  150. package/dist/shims/fetch-cache.js.map +1 -0
  151. package/dist/shims/font-google.d.ts +122 -0
  152. package/dist/shims/font-google.d.ts.map +1 -0
  153. package/dist/shims/font-google.js +387 -0
  154. package/dist/shims/font-google.js.map +1 -0
  155. package/dist/shims/font-local.d.ts +61 -0
  156. package/dist/shims/font-local.d.ts.map +1 -0
  157. package/dist/shims/font-local.js +303 -0
  158. package/dist/shims/font-local.js.map +1 -0
  159. package/dist/shims/form.d.ts +30 -0
  160. package/dist/shims/form.d.ts.map +1 -0
  161. package/dist/shims/form.js +78 -0
  162. package/dist/shims/form.js.map +1 -0
  163. package/dist/shims/head-state.d.ts +11 -0
  164. package/dist/shims/head-state.d.ts.map +1 -0
  165. package/dist/shims/head-state.js +47 -0
  166. package/dist/shims/head-state.js.map +1 -0
  167. package/dist/shims/head.d.ts +28 -0
  168. package/dist/shims/head.d.ts.map +1 -0
  169. package/dist/shims/head.js +148 -0
  170. package/dist/shims/head.js.map +1 -0
  171. package/dist/shims/headers.d.ts +150 -0
  172. package/dist/shims/headers.d.ts.map +1 -0
  173. package/dist/shims/headers.js +412 -0
  174. package/dist/shims/headers.js.map +1 -0
  175. package/dist/shims/image-config.d.ts +30 -0
  176. package/dist/shims/image-config.d.ts.map +1 -0
  177. package/dist/shims/image-config.js +91 -0
  178. package/dist/shims/image-config.js.map +1 -0
  179. package/dist/shims/image.d.ts +63 -0
  180. package/dist/shims/image.d.ts.map +1 -0
  181. package/dist/shims/image.js +284 -0
  182. package/dist/shims/image.js.map +1 -0
  183. package/dist/shims/internal/api-utils.d.ts +12 -0
  184. package/dist/shims/internal/api-utils.d.ts.map +1 -0
  185. package/dist/shims/internal/api-utils.js +7 -0
  186. package/dist/shims/internal/api-utils.js.map +1 -0
  187. package/dist/shims/internal/app-router-context.d.ts +21 -0
  188. package/dist/shims/internal/app-router-context.d.ts.map +1 -0
  189. package/dist/shims/internal/app-router-context.js +15 -0
  190. package/dist/shims/internal/app-router-context.js.map +1 -0
  191. package/dist/shims/internal/cookies.d.ts +9 -0
  192. package/dist/shims/internal/cookies.d.ts.map +1 -0
  193. package/dist/shims/internal/cookies.js +9 -0
  194. package/dist/shims/internal/cookies.js.map +1 -0
  195. package/dist/shims/internal/router-context.d.ts +2 -0
  196. package/dist/shims/internal/router-context.d.ts.map +1 -0
  197. package/dist/shims/internal/router-context.js +9 -0
  198. package/dist/shims/internal/router-context.js.map +1 -0
  199. package/dist/shims/internal/utils.d.ts +48 -0
  200. package/dist/shims/internal/utils.d.ts.map +1 -0
  201. package/dist/shims/internal/utils.js +35 -0
  202. package/dist/shims/internal/utils.js.map +1 -0
  203. package/dist/shims/internal/work-unit-async-storage.d.ts +12 -0
  204. package/dist/shims/internal/work-unit-async-storage.d.ts.map +1 -0
  205. package/dist/shims/internal/work-unit-async-storage.js +13 -0
  206. package/dist/shims/internal/work-unit-async-storage.js.map +1 -0
  207. package/dist/shims/layout-segment-context.d.ts +21 -0
  208. package/dist/shims/layout-segment-context.d.ts.map +1 -0
  209. package/dist/shims/layout-segment-context.js +27 -0
  210. package/dist/shims/layout-segment-context.js.map +1 -0
  211. package/dist/shims/legacy-image.d.ts +52 -0
  212. package/dist/shims/legacy-image.d.ts.map +1 -0
  213. package/dist/shims/legacy-image.js +46 -0
  214. package/dist/shims/legacy-image.js.map +1 -0
  215. package/dist/shims/link.d.ts +48 -0
  216. package/dist/shims/link.d.ts.map +1 -0
  217. package/dist/shims/link.js +395 -0
  218. package/dist/shims/link.js.map +1 -0
  219. package/dist/shims/metadata.d.ts +184 -0
  220. package/dist/shims/metadata.d.ts.map +1 -0
  221. package/dist/shims/metadata.js +472 -0
  222. package/dist/shims/metadata.js.map +1 -0
  223. package/dist/shims/navigation-state.d.ts +14 -0
  224. package/dist/shims/navigation-state.d.ts.map +1 -0
  225. package/dist/shims/navigation-state.js +77 -0
  226. package/dist/shims/navigation-state.js.map +1 -0
  227. package/dist/shims/navigation.d.ts +201 -0
  228. package/dist/shims/navigation.d.ts.map +1 -0
  229. package/dist/shims/navigation.js +672 -0
  230. package/dist/shims/navigation.js.map +1 -0
  231. package/dist/shims/og.d.ts +20 -0
  232. package/dist/shims/og.d.ts.map +1 -0
  233. package/dist/shims/og.js +19 -0
  234. package/dist/shims/og.js.map +1 -0
  235. package/dist/shims/router-state.d.ts +11 -0
  236. package/dist/shims/router-state.d.ts.map +1 -0
  237. package/dist/shims/router-state.js +56 -0
  238. package/dist/shims/router-state.js.map +1 -0
  239. package/dist/shims/router.d.ts +103 -0
  240. package/dist/shims/router.d.ts.map +1 -0
  241. package/dist/shims/router.js +536 -0
  242. package/dist/shims/router.js.map +1 -0
  243. package/dist/shims/script.d.ts +58 -0
  244. package/dist/shims/script.d.ts.map +1 -0
  245. package/dist/shims/script.js +163 -0
  246. package/dist/shims/script.js.map +1 -0
  247. package/dist/shims/server-only.d.ts +19 -0
  248. package/dist/shims/server-only.d.ts.map +1 -0
  249. package/dist/shims/server-only.js +19 -0
  250. package/dist/shims/server-only.js.map +1 -0
  251. package/dist/shims/server.d.ts +178 -0
  252. package/dist/shims/server.d.ts.map +1 -0
  253. package/dist/shims/server.js +377 -0
  254. package/dist/shims/server.js.map +1 -0
  255. package/dist/shims/web-vitals.d.ts +24 -0
  256. package/dist/shims/web-vitals.d.ts.map +1 -0
  257. package/dist/shims/web-vitals.js +17 -0
  258. package/dist/shims/web-vitals.js.map +1 -0
  259. package/dist/utils/hash.d.ts +6 -0
  260. package/dist/utils/hash.d.ts.map +1 -0
  261. package/dist/utils/hash.js +20 -0
  262. package/dist/utils/hash.js.map +1 -0
  263. package/dist/utils/project.d.ts +36 -0
  264. package/dist/utils/project.d.ts.map +1 -0
  265. package/dist/utils/project.js +112 -0
  266. package/dist/utils/project.js.map +1 -0
  267. package/dist/utils/query.d.ts +10 -0
  268. package/dist/utils/query.d.ts.map +1 -0
  269. package/dist/utils/query.js +27 -0
  270. package/dist/utils/query.js.map +1 -0
  271. package/package.json +65 -7
  272. package/index.js +0 -1
@@ -0,0 +1,815 @@
1
+ /**
2
+ * App Router file-system routing.
3
+ *
4
+ * Scans the app/ directory following Next.js App Router conventions:
5
+ * - app/page.tsx -> /
6
+ * - app/about/page.tsx -> /about
7
+ * - app/blog/[slug]/page.tsx -> /blog/:slug
8
+ * - app/[...catchAll]/page.tsx -> /:catchAll+
9
+ * - app/route.ts -> / (API route)
10
+ * - app/(group)/page.tsx -> / (route groups are transparent)
11
+ * - Layouts: app/layout.tsx wraps all children
12
+ * - Loading: app/loading.tsx -> Suspense fallback
13
+ * - Error: app/error.tsx -> ErrorBoundary
14
+ * - Not Found: app/not-found.tsx
15
+ */
16
+ import { glob } from "glob";
17
+ import path from "node:path";
18
+ import fs from "node:fs";
19
+ // Cache for app routes
20
+ let cachedRoutes = null;
21
+ let cachedAppDir = null;
22
+ export function invalidateAppRouteCache() {
23
+ cachedRoutes = null;
24
+ cachedAppDir = null;
25
+ }
26
+ /**
27
+ * Scan the app/ directory and return a list of routes.
28
+ */
29
+ export async function appRouter(appDir) {
30
+ if (cachedRoutes && cachedAppDir === appDir)
31
+ return cachedRoutes;
32
+ // Find all page.tsx and route.ts files, excluding @slot directories
33
+ // (slot pages are not standalone routes — they're rendered as props of their parent layout)
34
+ const allPageFiles = await glob("**/page.{tsx,ts,jsx,js}", { cwd: appDir });
35
+ const pageFiles = allPageFiles.filter((f) => !f.split(path.sep).some((s) => s.startsWith("@")));
36
+ const allRouteFiles = await glob("**/route.{tsx,ts,jsx,js}", { cwd: appDir });
37
+ const routeFiles = allRouteFiles.filter((f) => !f.split(path.sep).some((s) => s.startsWith("@")));
38
+ const routes = [];
39
+ // Process page files
40
+ for (const file of pageFiles) {
41
+ const route = fileToAppRoute(file, appDir, "page");
42
+ if (route)
43
+ routes.push(route);
44
+ }
45
+ // Process route handler files (API routes)
46
+ for (const file of routeFiles) {
47
+ const route = fileToAppRoute(file, appDir, "route");
48
+ if (route)
49
+ routes.push(route);
50
+ }
51
+ // Discover sub-routes created by nested pages within parallel slots.
52
+ // In Next.js, pages nested inside @slot directories create additional URL routes.
53
+ // For example, @audience/demographics/page.tsx at app/parallel-routes/ creates
54
+ // a route at /parallel-routes/demographics.
55
+ const slotSubRoutes = discoverSlotSubRoutes(routes, appDir);
56
+ routes.push(...slotSubRoutes);
57
+ // Sort: static routes first, then dynamic, then catch-all
58
+ routes.sort((a, b) => {
59
+ const diff = routePrecedence(a.pattern) - routePrecedence(b.pattern);
60
+ return diff !== 0 ? diff : a.pattern.localeCompare(b.pattern);
61
+ });
62
+ cachedRoutes = routes;
63
+ cachedAppDir = appDir;
64
+ return routes;
65
+ }
66
+ /**
67
+ * Discover sub-routes created by nested pages within parallel slots.
68
+ *
69
+ * In Next.js, pages nested inside @slot directories create additional URL routes.
70
+ * For example, given:
71
+ * app/parallel-routes/@audience/demographics/page.tsx
72
+ * This creates a route at /parallel-routes/demographics where:
73
+ * - children slot → parent's default.tsx
74
+ * - @audience slot → @audience/demographics/page.tsx (matched)
75
+ * - other slots → their default.tsx (fallback)
76
+ */
77
+ function discoverSlotSubRoutes(routes, _appDir) {
78
+ const syntheticRoutes = [];
79
+ const existingPatterns = new Set(routes.map((r) => r.pattern));
80
+ for (const parentRoute of routes) {
81
+ if (parentRoute.parallelSlots.length === 0)
82
+ continue;
83
+ if (!parentRoute.pagePath)
84
+ continue;
85
+ const parentPageDir = path.dirname(parentRoute.pagePath);
86
+ // Collect sub-paths from all slots.
87
+ // Map: relative sub-path (e.g., "demographics") -> Map<slotName, pagePath>
88
+ const subPathMap = new Map();
89
+ for (const slot of parentRoute.parallelSlots) {
90
+ const slotDir = path.join(parentPageDir, `@${slot.name}`);
91
+ if (!fs.existsSync(slotDir))
92
+ continue;
93
+ const subPages = findSlotSubPages(slotDir);
94
+ for (const { relativePath, pagePath } of subPages) {
95
+ if (!subPathMap.has(relativePath)) {
96
+ subPathMap.set(relativePath, new Map());
97
+ }
98
+ subPathMap.get(relativePath).set(slot.name, pagePath);
99
+ }
100
+ }
101
+ if (subPathMap.size === 0)
102
+ continue;
103
+ // Find the default.tsx for the children slot at the parent directory
104
+ const childrenDefault = findFile(parentPageDir, "default");
105
+ for (const [subPath, slotPages] of subPathMap) {
106
+ // Convert sub-path segments to URL pattern parts
107
+ const subSegments = subPath.split(path.sep);
108
+ const urlParts = [];
109
+ const subParams = [];
110
+ let subIsDynamic = false;
111
+ for (const seg of subSegments) {
112
+ // Route groups are transparent
113
+ if (seg.startsWith("(") && seg.endsWith(")"))
114
+ continue;
115
+ const catchAllMatch = seg.match(/^\[\.\.\.(\w+)\]$/);
116
+ if (catchAllMatch) {
117
+ subIsDynamic = true;
118
+ subParams.push(catchAllMatch[1]);
119
+ urlParts.push(`:${catchAllMatch[1]}+`);
120
+ continue;
121
+ }
122
+ const optionalCatchAllMatch = seg.match(/^\[\[\.\.\.(\w+)\]\]$/);
123
+ if (optionalCatchAllMatch) {
124
+ subIsDynamic = true;
125
+ subParams.push(optionalCatchAllMatch[1]);
126
+ urlParts.push(`:${optionalCatchAllMatch[1]}*`);
127
+ continue;
128
+ }
129
+ const dynamicMatch = seg.match(/^\[(\w+)\]$/);
130
+ if (dynamicMatch) {
131
+ subIsDynamic = true;
132
+ subParams.push(dynamicMatch[1]);
133
+ urlParts.push(`:${dynamicMatch[1]}`);
134
+ continue;
135
+ }
136
+ urlParts.push(seg);
137
+ }
138
+ const subUrlPath = urlParts.join("/");
139
+ const pattern = parentRoute.pattern === "/"
140
+ ? "/" + subUrlPath
141
+ : parentRoute.pattern + "/" + subUrlPath;
142
+ // Skip if this pattern already exists as a regular route
143
+ if (existingPatterns.has(pattern))
144
+ continue;
145
+ if (syntheticRoutes.some((r) => r.pattern === pattern))
146
+ continue;
147
+ // Build parallel slots for this sub-route: matching slots get the sub-page,
148
+ // non-matching slots get null pagePath (rendering falls back to defaultPath)
149
+ const subSlots = parentRoute.parallelSlots.map((slot) => ({
150
+ ...slot,
151
+ pagePath: slotPages.get(slot.name) || null,
152
+ }));
153
+ syntheticRoutes.push({
154
+ pattern,
155
+ pagePath: childrenDefault, // children slot uses parent's default.tsx as page
156
+ routePath: null,
157
+ layouts: parentRoute.layouts,
158
+ templates: parentRoute.templates,
159
+ parallelSlots: subSlots,
160
+ loadingPath: parentRoute.loadingPath,
161
+ errorPath: parentRoute.errorPath,
162
+ layoutErrorPaths: parentRoute.layoutErrorPaths,
163
+ notFoundPath: parentRoute.notFoundPath,
164
+ notFoundPaths: parentRoute.notFoundPaths,
165
+ forbiddenPath: parentRoute.forbiddenPath,
166
+ unauthorizedPath: parentRoute.unauthorizedPath,
167
+ layoutSegmentDepths: parentRoute.layoutSegmentDepths,
168
+ isDynamic: parentRoute.isDynamic || subIsDynamic,
169
+ params: [...parentRoute.params, ...subParams],
170
+ });
171
+ }
172
+ }
173
+ return syntheticRoutes;
174
+ }
175
+ /**
176
+ * Find all page files in subdirectories of a parallel slot directory.
177
+ * Returns relative paths (from the slot dir) and absolute page paths.
178
+ * Skips the root page.tsx (already handled as the slot's main page)
179
+ * and intercepting route directories.
180
+ */
181
+ function findSlotSubPages(slotDir) {
182
+ const results = [];
183
+ function scan(dir) {
184
+ if (!fs.existsSync(dir))
185
+ return;
186
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
187
+ for (const entry of entries) {
188
+ if (!entry.isDirectory())
189
+ continue;
190
+ // Skip intercepting route directories
191
+ if (matchInterceptConvention(entry.name))
192
+ continue;
193
+ // Skip private folders (prefixed with _)
194
+ if (entry.name.startsWith("_"))
195
+ continue;
196
+ const subDir = path.join(dir, entry.name);
197
+ const page = findFile(subDir, "page");
198
+ if (page) {
199
+ const relativePath = path.relative(slotDir, subDir);
200
+ results.push({ relativePath, pagePath: page });
201
+ }
202
+ // Continue scanning deeper for nested sub-pages
203
+ scan(subDir);
204
+ }
205
+ }
206
+ scan(slotDir);
207
+ return results;
208
+ }
209
+ /**
210
+ * Convert a file path relative to app/ into an AppRoute.
211
+ */
212
+ function fileToAppRoute(file, appDir, type) {
213
+ // Remove the filename (page.tsx or route.ts)
214
+ const dir = path.dirname(file);
215
+ const segments = dir === "." ? [] : dir.split(path.sep);
216
+ const params = [];
217
+ let isDynamic = false;
218
+ // Convert segments to URL pattern, stripping route groups and parallel slots
219
+ const urlSegments = [];
220
+ for (const segment of segments) {
221
+ // Route groups: (group) -> skip (transparent in URL)
222
+ if (segment.startsWith("(") && segment.endsWith(")")) {
223
+ continue;
224
+ }
225
+ // Parallel slots: @slot -> skip (invisible in URL, content passed as layout props)
226
+ if (segment.startsWith("@")) {
227
+ continue;
228
+ }
229
+ // Catch-all: [...slug]
230
+ const catchAllMatch = segment.match(/^\[\.\.\.(\w+)\]$/);
231
+ if (catchAllMatch) {
232
+ isDynamic = true;
233
+ params.push(catchAllMatch[1]);
234
+ urlSegments.push(`:${catchAllMatch[1]}+`);
235
+ continue;
236
+ }
237
+ // Optional catch-all: [[...slug]]
238
+ const optionalCatchAllMatch = segment.match(/^\[\[\.\.\.(\w+)\]\]$/);
239
+ if (optionalCatchAllMatch) {
240
+ isDynamic = true;
241
+ params.push(optionalCatchAllMatch[1]);
242
+ urlSegments.push(`:${optionalCatchAllMatch[1]}*`);
243
+ continue;
244
+ }
245
+ // Dynamic segment: [id]
246
+ const dynamicMatch = segment.match(/^\[(\w+)\]$/);
247
+ if (dynamicMatch) {
248
+ isDynamic = true;
249
+ params.push(dynamicMatch[1]);
250
+ urlSegments.push(`:${dynamicMatch[1]}`);
251
+ continue;
252
+ }
253
+ try {
254
+ urlSegments.push(decodeURIComponent(segment));
255
+ }
256
+ catch {
257
+ urlSegments.push(segment);
258
+ }
259
+ }
260
+ const pattern = "/" + urlSegments.join("/");
261
+ // Discover layouts and templates from root to leaf
262
+ const layouts = discoverLayouts(segments, appDir);
263
+ const templates = discoverTemplates(segments, appDir);
264
+ // Compute the URL segment depth for each layout.
265
+ // Each layout corresponds to a directory level. We need to count how many
266
+ // of the filesystem segments up to that layout's level contribute URL segments
267
+ // (i.e., are not route groups or parallel slots).
268
+ const layoutSegmentDepths = computeLayoutSegmentDepths(segments, appDir, layouts);
269
+ // Discover per-layout error boundaries (aligned with layouts array).
270
+ // In Next.js, each segment independently wraps its children with an ErrorBoundary.
271
+ // This array enables interleaving error boundaries with layouts in the rendering.
272
+ const layoutErrorPaths = discoverLayoutAlignedErrors(segments, appDir);
273
+ // Discover loading, error in the route's directory
274
+ const routeDir = dir === "." ? appDir : path.join(appDir, dir);
275
+ const loadingPath = findFile(routeDir, "loading");
276
+ const errorPath = findFile(routeDir, "error");
277
+ // Discover not-found/forbidden/unauthorized: walk from route directory up to root (nearest wins).
278
+ const notFoundPath = discoverBoundaryFile(segments, appDir, "not-found");
279
+ const forbiddenPath = discoverBoundaryFile(segments, appDir, "forbidden");
280
+ const unauthorizedPath = discoverBoundaryFile(segments, appDir, "unauthorized");
281
+ // Discover per-layout not-found files (one per layout directory).
282
+ // These are used for per-layout NotFoundBoundary to match Next.js behavior where
283
+ // notFound() thrown from a layout is caught by the parent layout's boundary.
284
+ const notFoundPaths = discoverBoundaryFilePerLayout(layouts, "not-found");
285
+ // Discover parallel slots (@team, @analytics, etc.).
286
+ // Slots at the route's own directory use page.tsx; slots at ancestor directories
287
+ // (inherited from parent layouts) use default.tsx as fallback.
288
+ const parallelSlots = discoverInheritedParallelSlots(segments, appDir, routeDir);
289
+ return {
290
+ pattern: pattern === "/" ? "/" : pattern,
291
+ pagePath: type === "page" ? path.join(appDir, file) : null,
292
+ routePath: type === "route" ? path.join(appDir, file) : null,
293
+ layouts,
294
+ templates,
295
+ parallelSlots,
296
+ loadingPath,
297
+ errorPath,
298
+ layoutErrorPaths,
299
+ notFoundPath,
300
+ notFoundPaths,
301
+ forbiddenPath,
302
+ unauthorizedPath,
303
+ layoutSegmentDepths,
304
+ isDynamic,
305
+ params,
306
+ };
307
+ }
308
+ /**
309
+ * Compute the URL segment depth for each layout in the layouts array.
310
+ * Root layout = 0, then each directory level that contributes a URL segment
311
+ * increments the depth. Route groups and parallel slots don't contribute.
312
+ */
313
+ function computeLayoutSegmentDepths(segments, appDir, layouts) {
314
+ // Build a map: layout file path → depth in URL segments
315
+ // Walk the segments directory-by-directory, tracking cumulative URL depth
316
+ const depthMap = new Map();
317
+ // Root layout (at appDir) always has depth 0
318
+ const rootLayout = findFile(appDir, "layout");
319
+ if (rootLayout)
320
+ depthMap.set(rootLayout, 0);
321
+ let urlDepth = 0;
322
+ let currentDir = appDir;
323
+ for (const segment of segments) {
324
+ currentDir = path.join(currentDir, segment);
325
+ // Count URL-visible segments (skip route groups and parallel slots)
326
+ const isRouteGroup = segment.startsWith("(") && segment.endsWith(")");
327
+ const isParallelSlot = segment.startsWith("@");
328
+ if (!isRouteGroup && !isParallelSlot) {
329
+ urlDepth++;
330
+ }
331
+ const layout = findFile(currentDir, "layout");
332
+ if (layout) {
333
+ depthMap.set(layout, urlDepth);
334
+ }
335
+ }
336
+ // Map the ordered layouts array to their depths
337
+ return layouts.map((layoutPath) => depthMap.get(layoutPath) ?? 0);
338
+ }
339
+ /**
340
+ * Discover all layout files from root to the given directory.
341
+ * Each level of the directory tree may have a layout.tsx.
342
+ */
343
+ function discoverLayouts(segments, appDir) {
344
+ const layouts = [];
345
+ // Check root layout
346
+ const rootLayout = findFile(appDir, "layout");
347
+ if (rootLayout)
348
+ layouts.push(rootLayout);
349
+ // Check each directory level
350
+ let currentDir = appDir;
351
+ for (const segment of segments) {
352
+ currentDir = path.join(currentDir, segment);
353
+ const layout = findFile(currentDir, "layout");
354
+ if (layout)
355
+ layouts.push(layout);
356
+ }
357
+ return layouts;
358
+ }
359
+ /**
360
+ * Discover all template files from root to the given directory.
361
+ * Each level of the directory tree may have a template.tsx.
362
+ * Templates are like layouts but re-mount on navigation.
363
+ */
364
+ function discoverTemplates(segments, appDir) {
365
+ const templates = [];
366
+ // Check root template
367
+ const rootTemplate = findFile(appDir, "template");
368
+ if (rootTemplate)
369
+ templates.push(rootTemplate);
370
+ // Check each directory level
371
+ let currentDir = appDir;
372
+ for (const segment of segments) {
373
+ currentDir = path.join(currentDir, segment);
374
+ const template = findFile(currentDir, "template");
375
+ if (template)
376
+ templates.push(template);
377
+ }
378
+ return templates;
379
+ }
380
+ /**
381
+ * Discover error.tsx files aligned with the layouts array.
382
+ * Walks the same directory levels as discoverLayouts and, for each level
383
+ * that contributes a layout entry, checks whether error.tsx also exists.
384
+ * Returns an array of the same length as discoverLayouts() would return,
385
+ * with the error path (or null) at each corresponding layout level.
386
+ *
387
+ * This enables interleaving ErrorBoundary components with layouts in the
388
+ * rendering tree, matching Next.js behavior where each segment independently
389
+ * wraps its children with an error boundary.
390
+ */
391
+ function discoverLayoutAlignedErrors(segments, appDir) {
392
+ const errors = [];
393
+ // Root level (only if root has a layout — matching discoverLayouts logic)
394
+ const rootLayout = findFile(appDir, "layout");
395
+ if (rootLayout) {
396
+ errors.push(findFile(appDir, "error"));
397
+ }
398
+ // Check each directory level
399
+ let currentDir = appDir;
400
+ for (const segment of segments) {
401
+ currentDir = path.join(currentDir, segment);
402
+ const layout = findFile(currentDir, "layout");
403
+ if (layout) {
404
+ errors.push(findFile(currentDir, "error"));
405
+ }
406
+ }
407
+ return errors;
408
+ }
409
+ /**
410
+ * Discover the nearest boundary file (not-found, forbidden, unauthorized)
411
+ * by walking from the route's directory up to the app root.
412
+ * Returns the first (closest) file found, or null.
413
+ */
414
+ function discoverBoundaryFile(segments, appDir, fileName) {
415
+ // Build all directory paths from leaf to root
416
+ const dirs = [];
417
+ let dir = appDir;
418
+ dirs.push(dir);
419
+ for (const segment of segments) {
420
+ dir = path.join(dir, segment);
421
+ dirs.push(dir);
422
+ }
423
+ // Walk from leaf (last) to root (first)
424
+ for (let i = dirs.length - 1; i >= 0; i--) {
425
+ const f = findFile(dirs[i], fileName);
426
+ if (f)
427
+ return f;
428
+ }
429
+ return null;
430
+ }
431
+ /**
432
+ * Discover boundary files (not-found, forbidden, unauthorized) at each layout directory.
433
+ * Returns an array aligned with the layouts array, where each entry is the boundary
434
+ * file at that layout's directory, or null if none exists there.
435
+ *
436
+ * This is used for per-layout error boundaries. In Next.js, each layout level
437
+ * has its own boundary that wraps the layout's children. When notFound() is thrown
438
+ * from a layout, it propagates up to the parent layout's boundary.
439
+ */
440
+ function discoverBoundaryFilePerLayout(layouts, fileName) {
441
+ return layouts.map((layoutPath) => {
442
+ const layoutDir = path.dirname(layoutPath);
443
+ return findFile(layoutDir, fileName);
444
+ });
445
+ }
446
+ /**
447
+ * Discover parallel slots inherited from ancestor directories.
448
+ *
449
+ * In Next.js, parallel slots belong to the layout that defines them. When a
450
+ * child route is rendered, its parent layout's slots must still be present.
451
+ * If the child doesn't have matching content in a slot, the slot's default.tsx
452
+ * is rendered instead.
453
+ *
454
+ * Walk from appDir through each segment to the route's directory. At each level
455
+ * that has @slot dirs, collect them. Slots at the route's own directory level
456
+ * use page.tsx; slots at ancestor levels use default.tsx only.
457
+ */
458
+ function discoverInheritedParallelSlots(segments, appDir, routeDir) {
459
+ const slotMap = new Map();
460
+ // Walk from appDir through each segment, tracking layout indices.
461
+ // layoutIndex tracks which position in the route's layouts[] array corresponds
462
+ // to a given directory. Only directories with a layout.tsx file increment.
463
+ let currentDir = appDir;
464
+ const dirsToCheck = [];
465
+ let layoutIdx = findFile(appDir, "layout") ? 0 : -1;
466
+ dirsToCheck.push({ dir: appDir, layoutIdx: Math.max(layoutIdx, 0) });
467
+ for (const segment of segments) {
468
+ currentDir = path.join(currentDir, segment);
469
+ if (findFile(currentDir, "layout")) {
470
+ layoutIdx++;
471
+ }
472
+ dirsToCheck.push({ dir: currentDir, layoutIdx: Math.max(layoutIdx, 0) });
473
+ }
474
+ for (const { dir, layoutIdx: lvlLayoutIdx } of dirsToCheck) {
475
+ const isOwnDir = dir === routeDir;
476
+ const slotsAtLevel = discoverParallelSlots(dir, appDir);
477
+ for (const slot of slotsAtLevel) {
478
+ if (isOwnDir) {
479
+ // At the route's own directory: use page.tsx (normal behavior)
480
+ slot.layoutIndex = lvlLayoutIdx;
481
+ slotMap.set(slot.name, slot);
482
+ }
483
+ else {
484
+ // At an ancestor directory: use default.tsx as the page, not page.tsx
485
+ // (the slot's page.tsx is for the parent route, not this child route)
486
+ const inheritedSlot = {
487
+ ...slot,
488
+ pagePath: null, // Don't use ancestor's page.tsx
489
+ layoutIndex: lvlLayoutIdx,
490
+ // defaultPath, loadingPath, errorPath, interceptingRoutes remain
491
+ };
492
+ // Only inherit if we haven't seen this slot at a closer level
493
+ if (!slotMap.has(slot.name)) {
494
+ slotMap.set(slot.name, inheritedSlot);
495
+ }
496
+ }
497
+ }
498
+ }
499
+ return Array.from(slotMap.values());
500
+ }
501
+ /**
502
+ * Discover parallel route slots (@team, @analytics, etc.) in a directory.
503
+ * Returns a ParallelSlot for each @-prefixed subdirectory that has a page or default component.
504
+ */
505
+ function discoverParallelSlots(dir, appDir) {
506
+ if (!fs.existsSync(dir))
507
+ return [];
508
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
509
+ const slots = [];
510
+ for (const entry of entries) {
511
+ if (!entry.isDirectory() || !entry.name.startsWith("@"))
512
+ continue;
513
+ const slotName = entry.name.slice(1); // "@team" -> "team"
514
+ const slotDir = path.join(dir, entry.name);
515
+ const pagePath = findFile(slotDir, "page");
516
+ const defaultPath = findFile(slotDir, "default");
517
+ const interceptingRoutes = discoverInterceptingRoutes(slotDir, dir, appDir);
518
+ // Only include slots that have at least a page, default, or intercepting route
519
+ if (!pagePath && !defaultPath && interceptingRoutes.length === 0)
520
+ continue;
521
+ slots.push({
522
+ name: slotName,
523
+ pagePath,
524
+ defaultPath,
525
+ layoutPath: findFile(slotDir, "layout"),
526
+ loadingPath: findFile(slotDir, "loading"),
527
+ errorPath: findFile(slotDir, "error"),
528
+ interceptingRoutes,
529
+ layoutIndex: -1, // Will be set by discoverInheritedParallelSlots
530
+ });
531
+ }
532
+ return slots;
533
+ }
534
+ /**
535
+ * The interception convention prefix patterns.
536
+ * (.) — same level, (..) — one level up, (..)(..)" — two levels up, (...) — root
537
+ */
538
+ const INTERCEPT_PATTERNS = [
539
+ { prefix: "(...)", convention: "..." },
540
+ { prefix: "(..)(..)", convention: "../.." },
541
+ { prefix: "(..)", convention: ".." },
542
+ { prefix: "(.)", convention: "." },
543
+ ];
544
+ /**
545
+ * Discover intercepting routes inside a parallel slot directory.
546
+ *
547
+ * Intercepting routes use conventions like (.)photo, (..)feed, (...), etc.
548
+ * They intercept navigation to another route and render within the slot instead.
549
+ *
550
+ * @param slotDir - The parallel slot directory (e.g. app/feed/@modal)
551
+ * @param routeDir - The directory of the route that owns this slot (e.g. app/feed)
552
+ * @param appDir - The root app directory
553
+ */
554
+ function discoverInterceptingRoutes(slotDir, routeDir, appDir) {
555
+ if (!fs.existsSync(slotDir))
556
+ return [];
557
+ const results = [];
558
+ // Recursively scan for page files inside intercepting directories
559
+ scanForInterceptingPages(slotDir, routeDir, appDir, results);
560
+ return results;
561
+ }
562
+ /**
563
+ * Recursively scan a directory tree for page.tsx files that are inside
564
+ * intercepting route directories.
565
+ */
566
+ function scanForInterceptingPages(currentDir, routeDir, appDir, results) {
567
+ if (!fs.existsSync(currentDir))
568
+ return;
569
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
570
+ for (const entry of entries) {
571
+ if (!entry.isDirectory())
572
+ continue;
573
+ // Check if this directory name starts with an interception convention
574
+ const interceptMatch = matchInterceptConvention(entry.name);
575
+ if (interceptMatch) {
576
+ // This directory is the start of an intercepting route
577
+ // e.g. "(.)photos" means intercept same-level "photos" route
578
+ const restOfName = entry.name.slice(interceptMatch.prefix.length);
579
+ const interceptDir = path.join(currentDir, entry.name);
580
+ // Find page files within this intercepting directory tree
581
+ collectInterceptingPages(interceptDir, interceptDir, interceptMatch.convention, restOfName, routeDir, appDir, results);
582
+ }
583
+ else {
584
+ // Regular subdirectory — keep scanning for intercepting dirs
585
+ scanForInterceptingPages(path.join(currentDir, entry.name), routeDir, appDir, results);
586
+ }
587
+ }
588
+ }
589
+ /**
590
+ * Match a directory name against interception convention prefixes.
591
+ */
592
+ function matchInterceptConvention(name) {
593
+ for (const pattern of INTERCEPT_PATTERNS) {
594
+ if (name.startsWith(pattern.prefix)) {
595
+ return pattern;
596
+ }
597
+ }
598
+ return null;
599
+ }
600
+ /**
601
+ * Collect page.tsx files inside an intercepting route directory tree
602
+ * and compute their target URL patterns.
603
+ */
604
+ function collectInterceptingPages(currentDir, interceptRoot, convention, interceptSegment, routeDir, appDir, results) {
605
+ // Check for page.tsx in current directory
606
+ const page = findFile(currentDir, "page");
607
+ if (page) {
608
+ const targetPattern = computeInterceptTarget(convention, interceptSegment, currentDir, interceptRoot, routeDir, appDir);
609
+ if (targetPattern) {
610
+ results.push({
611
+ convention,
612
+ targetPattern: targetPattern.pattern,
613
+ pagePath: page,
614
+ params: targetPattern.params,
615
+ });
616
+ }
617
+ }
618
+ // Recurse into subdirectories for nested intercepting routes
619
+ if (!fs.existsSync(currentDir))
620
+ return;
621
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
622
+ for (const entry of entries) {
623
+ if (!entry.isDirectory())
624
+ continue;
625
+ collectInterceptingPages(path.join(currentDir, entry.name), interceptRoot, convention, interceptSegment, routeDir, appDir, results);
626
+ }
627
+ }
628
+ /**
629
+ * Compute the target URL pattern for an intercepting route.
630
+ *
631
+ * - (.) same level: resolve relative to routeDir
632
+ * - (..) one level up: resolve relative to parent of routeDir
633
+ * - (..)(..)" two levels up: resolve relative to grandparent of routeDir
634
+ * - (...) root: resolve from appDir
635
+ */
636
+ function computeInterceptTarget(convention, interceptSegment, currentDir, interceptRoot, routeDir, appDir) {
637
+ // Determine the base directory for target resolution
638
+ let baseDir;
639
+ switch (convention) {
640
+ case ".":
641
+ baseDir = routeDir;
642
+ break;
643
+ case "..":
644
+ baseDir = path.dirname(routeDir);
645
+ break;
646
+ case "../..":
647
+ baseDir = path.dirname(path.dirname(routeDir));
648
+ break;
649
+ case "...":
650
+ baseDir = appDir;
651
+ break;
652
+ default:
653
+ return null;
654
+ }
655
+ // Build the target URL segments from baseDir relative to appDir
656
+ const baseParts = path
657
+ .relative(appDir, baseDir)
658
+ .split(path.sep)
659
+ .filter(Boolean);
660
+ // Add the intercept segment and any nested path segments
661
+ const nestedParts = path
662
+ .relative(interceptRoot, currentDir)
663
+ .split(path.sep)
664
+ .filter(Boolean);
665
+ const allSegments = [...baseParts, interceptSegment, ...nestedParts];
666
+ // Convert segments to URL pattern
667
+ const urlSegments = [];
668
+ const params = [];
669
+ for (const segment of allSegments) {
670
+ if (segment === ".")
671
+ continue;
672
+ // Route groups and @ slots are transparent
673
+ if (segment.startsWith("(") && segment.endsWith(")"))
674
+ continue;
675
+ if (segment.startsWith("@"))
676
+ continue;
677
+ // Dynamic segments
678
+ const catchAllMatch = segment.match(/^\[\.\.\.(\w+)\]$/);
679
+ if (catchAllMatch) {
680
+ params.push(catchAllMatch[1]);
681
+ urlSegments.push(`:${catchAllMatch[1]}+`);
682
+ continue;
683
+ }
684
+ const optionalCatchAllMatch = segment.match(/^\[\[\.\.\.(\w+)\]\]$/);
685
+ if (optionalCatchAllMatch) {
686
+ params.push(optionalCatchAllMatch[1]);
687
+ urlSegments.push(`:${optionalCatchAllMatch[1]}*`);
688
+ continue;
689
+ }
690
+ const dynamicMatch = segment.match(/^\[(\w+)\]$/);
691
+ if (dynamicMatch) {
692
+ params.push(dynamicMatch[1]);
693
+ urlSegments.push(`:${dynamicMatch[1]}`);
694
+ continue;
695
+ }
696
+ // Decode URL-encoded directory names (e.g., %5Fsites -> _sites)
697
+ try {
698
+ urlSegments.push(decodeURIComponent(segment));
699
+ }
700
+ catch {
701
+ urlSegments.push(segment);
702
+ }
703
+ }
704
+ const pattern = "/" + urlSegments.join("/");
705
+ return { pattern: pattern === "/" ? "/" : pattern, params };
706
+ }
707
+ /**
708
+ * Find a file by name (without extension) in a directory.
709
+ * Checks .tsx, .ts, .jsx, .js extensions.
710
+ */
711
+ function findFile(dir, name) {
712
+ const extensions = [".tsx", ".ts", ".jsx", ".js"];
713
+ for (const ext of extensions) {
714
+ const filePath = path.join(dir, name + ext);
715
+ if (fs.existsSync(filePath))
716
+ return filePath;
717
+ }
718
+ return null;
719
+ }
720
+ /**
721
+ * Match a URL against App Router routes.
722
+ */
723
+ export function matchAppRoute(url, routes) {
724
+ const pathname = url.split("?")[0];
725
+ let normalizedUrl = pathname === "/" ? "/" : pathname.replace(/\/$/, "");
726
+ try {
727
+ normalizedUrl = decodeURIComponent(normalizedUrl);
728
+ }
729
+ catch {
730
+ /* malformed percent-encoding — match as-is */
731
+ }
732
+ for (const route of routes) {
733
+ const params = matchPattern(normalizedUrl, route.pattern);
734
+ if (params !== null) {
735
+ return { route, params };
736
+ }
737
+ }
738
+ return null;
739
+ }
740
+ function matchPattern(url, pattern) {
741
+ const urlParts = url.split("/").filter(Boolean);
742
+ const patternParts = pattern.split("/").filter(Boolean);
743
+ const params = Object.create(null);
744
+ for (let i = 0; i < patternParts.length; i++) {
745
+ const pp = patternParts[i];
746
+ if (pp.endsWith("+")) {
747
+ const paramName = pp.slice(1, -1);
748
+ const remaining = urlParts.slice(i);
749
+ if (remaining.length === 0)
750
+ return null;
751
+ params[paramName] = remaining;
752
+ return params;
753
+ }
754
+ if (pp.endsWith("*")) {
755
+ const paramName = pp.slice(1, -1);
756
+ const remaining = urlParts.slice(i);
757
+ params[paramName] = remaining;
758
+ return params;
759
+ }
760
+ if (pp.startsWith(":")) {
761
+ const paramName = pp.slice(1);
762
+ if (i >= urlParts.length)
763
+ return null;
764
+ params[paramName] = urlParts[i];
765
+ continue;
766
+ }
767
+ if (i >= urlParts.length || urlParts[i] !== pp)
768
+ return null;
769
+ }
770
+ if (urlParts.length !== patternParts.length)
771
+ return null;
772
+ return params;
773
+ }
774
+ /**
775
+ * Route precedence — lower score is higher priority.
776
+ * Matches Next.js specificity rules:
777
+ * 1. Static routes first (scored by segment count, more = more specific)
778
+ * 2. Dynamic segments penalized by position
779
+ * 3. Catch-all comes after dynamic
780
+ * 4. Optional catch-all last
781
+ * 5. Lexicographic tiebreaker for determinism
782
+ *
783
+ * Key insight: routes with static prefix segments should have higher priority
784
+ * than catch-all routes without them. E.g., /_sites/:subdomain/:slug* should
785
+ * match before /:slug* because "_sites" must match exactly.
786
+ */
787
+ function routePrecedence(pattern) {
788
+ const parts = pattern.split("/").filter(Boolean);
789
+ let score = 0;
790
+ let staticPrefixCount = 0;
791
+ // Count static prefix segments (before first dynamic/catch-all)
792
+ for (const p of parts) {
793
+ if (p.startsWith(":") || p.endsWith("+") || p.endsWith("*"))
794
+ break;
795
+ staticPrefixCount++;
796
+ }
797
+ // Static prefix segments dramatically reduce score (increase priority).
798
+ // Each static prefix segment gives -10000 priority boost.
799
+ score -= staticPrefixCount * 10000;
800
+ for (let i = 0; i < parts.length; i++) {
801
+ const p = parts[i];
802
+ if (p.endsWith("+")) {
803
+ score += 1000 + i; // catch-all: moderate penalty
804
+ }
805
+ else if (p.endsWith("*")) {
806
+ score += 2000 + i; // optional catch-all: high penalty
807
+ }
808
+ else if (p.startsWith(":")) {
809
+ score += 100 + i; // dynamic: small penalty by position
810
+ }
811
+ // static segments after first dynamic don't contribute extra
812
+ }
813
+ return score;
814
+ }
815
+ //# sourceMappingURL=app-router.js.map