vinext 1.0.0-beta.1 → 1.0.0-beta.3

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 (234) hide show
  1. package/README.md +10 -10
  2. package/dist/build/client-build-config.js +8 -1
  3. package/dist/build/report.d.ts +8 -1
  4. package/dist/build/report.js +12 -2
  5. package/dist/check.js +19 -2
  6. package/dist/cli.js +3 -2
  7. package/dist/config/config-matchers.d.ts +13 -37
  8. package/dist/config/config-matchers.js +35 -122
  9. package/dist/config/next-config.d.ts +15 -5
  10. package/dist/config/next-config.js +7 -1
  11. package/dist/config/request-context.d.ts +14 -0
  12. package/dist/config/request-context.js +26 -0
  13. package/dist/entries/app-browser-entry.d.ts +4 -10
  14. package/dist/entries/app-browser-entry.js +20 -4
  15. package/dist/entries/app-rsc-entry.js +30 -9
  16. package/dist/entries/app-rsc-manifest.js +36 -0
  17. package/dist/entries/pages-client-entry.js +0 -1
  18. package/dist/entries/pages-server-entry.js +13 -5
  19. package/dist/index.js +194 -101
  20. package/dist/init-cloudflare.d.ts +3 -1
  21. package/dist/init-cloudflare.js +59 -5
  22. package/dist/init.d.ts +6 -4
  23. package/dist/init.js +20 -22
  24. package/dist/plugins/extensionless-dynamic-import.js +23 -18
  25. package/dist/plugins/ignore-dynamic-requests.js +3 -1
  26. package/dist/plugins/import-meta-url.js +17 -6
  27. package/dist/plugins/og-assets.js +1 -1
  28. package/dist/plugins/require-context.js +20 -15
  29. package/dist/plugins/transform-cache.d.ts +19 -0
  30. package/dist/plugins/transform-cache.js +36 -0
  31. package/dist/routing/app-route-graph.d.ts +27 -11
  32. package/dist/routing/app-route-graph.js +158 -22
  33. package/dist/routing/app-router.js +12 -6
  34. package/dist/routing/file-matcher.d.ts +11 -22
  35. package/dist/routing/file-matcher.js +21 -33
  36. package/dist/routing/pages-router.js +23 -29
  37. package/dist/routing/route-matching.d.ts +14 -1
  38. package/dist/routing/route-matching.js +13 -1
  39. package/dist/routing/route-pattern.d.ts +2 -1
  40. package/dist/routing/route-pattern.js +7 -4
  41. package/dist/routing/route-trie.d.ts +5 -1
  42. package/dist/routing/route-trie.js +5 -2
  43. package/dist/server/api-handler.d.ts +2 -0
  44. package/dist/server/api-handler.js +3 -3
  45. package/dist/server/app-browser-entry.js +110 -37
  46. package/dist/server/app-browser-history-controller.d.ts +1 -1
  47. package/dist/server/app-browser-history-controller.js +3 -3
  48. package/dist/server/app-browser-navigation-controller.d.ts +23 -21
  49. package/dist/server/app-browser-navigation-controller.js +2 -2
  50. package/dist/server/app-browser-state.js +1 -1
  51. package/dist/server/app-browser-visible-commit.js +1 -1
  52. package/dist/server/app-fallback-renderer.d.ts +3 -0
  53. package/dist/server/app-fallback-renderer.js +4 -2
  54. package/dist/server/app-middleware.js +2 -1
  55. package/dist/server/app-page-boundary-render.d.ts +12 -1
  56. package/dist/server/app-page-boundary-render.js +84 -2
  57. package/dist/server/app-page-cache-finalizer.d.ts +1 -1
  58. package/dist/server/app-page-cache-finalizer.js +1 -1
  59. package/dist/server/app-page-cache.d.ts +2 -2
  60. package/dist/server/app-page-cache.js +16 -2
  61. package/dist/server/app-page-dispatch.d.ts +19 -2
  62. package/dist/server/app-page-dispatch.js +45 -23
  63. package/dist/server/app-page-element-builder.d.ts +9 -2
  64. package/dist/server/app-page-element-builder.js +144 -33
  65. package/dist/server/app-page-head.d.ts +56 -4
  66. package/dist/server/app-page-head.js +216 -85
  67. package/dist/server/app-page-http-access-fallback-metadata.d.ts +29 -0
  68. package/dist/server/app-page-http-access-fallback-metadata.js +114 -0
  69. package/dist/server/app-page-probe.d.ts +4 -0
  70. package/dist/server/app-page-probe.js +6 -2
  71. package/dist/server/app-page-render.d.ts +2 -2
  72. package/dist/server/app-page-render.js +1 -1
  73. package/dist/server/app-page-request.d.ts +15 -0
  74. package/dist/server/app-page-request.js +134 -14
  75. package/dist/server/app-page-response.js +1 -1
  76. package/dist/server/app-page-route-wiring.d.ts +16 -0
  77. package/dist/server/app-page-route-wiring.js +254 -52
  78. package/dist/server/app-pages-bridge.d.ts +1 -1
  79. package/dist/server/app-pages-bridge.js +2 -1
  80. package/dist/server/app-post-middleware-context.d.ts +1 -1
  81. package/dist/server/app-post-middleware-context.js +1 -1
  82. package/dist/server/app-route-handler-cache.js +4 -0
  83. package/dist/server/app-route-handler-dispatch.js +40 -20
  84. package/dist/server/app-route-handler-execution.d.ts +6 -1
  85. package/dist/server/app-route-handler-execution.js +42 -16
  86. package/dist/server/app-route-handler-policy.d.ts +2 -0
  87. package/dist/server/app-route-handler-policy.js +3 -3
  88. package/dist/server/app-route-handler-response.js +1 -1
  89. package/dist/server/app-route-module-loader.d.ts +9 -1
  90. package/dist/server/app-route-module-loader.js +4 -0
  91. package/dist/server/app-router-entry.js +6 -3
  92. package/dist/server/app-rsc-cache-busting.d.ts +1 -1
  93. package/dist/server/app-rsc-errors.d.ts +1 -1
  94. package/dist/server/app-rsc-errors.js +22 -2
  95. package/dist/server/app-rsc-handler.d.ts +10 -5
  96. package/dist/server/app-rsc-handler.js +73 -33
  97. package/dist/server/app-rsc-request-normalization.d.ts +3 -2
  98. package/dist/server/app-rsc-request-normalization.js +8 -2
  99. package/dist/server/app-rsc-response-finalizer.d.ts +2 -2
  100. package/dist/server/app-rsc-response-finalizer.js +5 -11
  101. package/dist/server/app-rsc-route-matching.d.ts +29 -0
  102. package/dist/server/app-rsc-route-matching.js +93 -11
  103. package/dist/server/app-segment-config.js +13 -3
  104. package/dist/server/app-server-action-execution.d.ts +3 -1
  105. package/dist/server/app-server-action-execution.js +9 -7
  106. package/dist/server/app-ssr-entry.js +6 -6
  107. package/dist/server/app-ssr-stream.d.ts +1 -7
  108. package/dist/server/app-ssr-stream.js +2 -10
  109. package/dist/server/cache-control.d.ts +2 -2
  110. package/dist/server/cache-control.js +2 -1
  111. package/dist/server/config-headers.d.ts +24 -0
  112. package/dist/server/config-headers.js +52 -0
  113. package/dist/server/dev-initial-server-error.d.ts +1 -1
  114. package/dist/server/dev-server.d.ts +3 -1
  115. package/dist/server/dev-server.js +170 -321
  116. package/dist/server/headers.d.ts +2 -2
  117. package/dist/server/headers.js +4 -3
  118. package/dist/server/isr-cache.d.ts +11 -34
  119. package/dist/server/isr-cache.js +33 -43
  120. package/dist/server/isr-decision.d.ts +2 -2
  121. package/dist/server/middleware-matcher-pattern.d.ts +22 -0
  122. package/dist/server/middleware-matcher-pattern.js +219 -0
  123. package/dist/server/middleware-matcher.d.ts +2 -7
  124. package/dist/server/middleware-matcher.js +13 -60
  125. package/dist/server/middleware-path-to-regexp.d.ts +14 -0
  126. package/dist/server/middleware-path-to-regexp.js +228 -0
  127. package/dist/server/middleware-runtime.js +4 -3
  128. package/dist/server/middleware.d.ts +1 -1
  129. package/dist/server/middleware.js +2 -1
  130. package/dist/server/pages-api-route.d.ts +2 -0
  131. package/dist/server/pages-api-route.js +4 -1
  132. package/dist/server/pages-data-route.d.ts +11 -1
  133. package/dist/server/pages-data-route.js +16 -1
  134. package/dist/server/pages-dev-hydration.js +0 -1
  135. package/dist/server/pages-get-initial-props.d.ts +16 -3
  136. package/dist/server/pages-get-initial-props.js +17 -10
  137. package/dist/server/pages-i18n.js +64 -16
  138. package/dist/server/pages-node-compat.d.ts +2 -0
  139. package/dist/server/pages-node-compat.js +7 -3
  140. package/dist/server/pages-page-data.d.ts +64 -10
  141. package/dist/server/pages-page-data.js +349 -80
  142. package/dist/server/pages-page-handler.d.ts +9 -3
  143. package/dist/server/pages-page-handler.js +131 -36
  144. package/dist/server/pages-page-response.d.ts +5 -3
  145. package/dist/server/pages-page-response.js +24 -27
  146. package/dist/server/pages-readiness.d.ts +3 -3
  147. package/dist/server/pages-request-pipeline.d.ts +8 -0
  148. package/dist/server/pages-request-pipeline.js +30 -12
  149. package/dist/server/pages-revalidate.d.ts +1 -1
  150. package/dist/server/pages-revalidate.js +54 -8
  151. package/dist/server/pages-router-entry.d.ts +1 -0
  152. package/dist/server/pages-router-entry.js +16 -4
  153. package/dist/server/prerender-route-params.js +2 -2
  154. package/dist/server/prod-server.d.ts +3 -5
  155. package/dist/server/prod-server.js +72 -35
  156. package/dist/server/request-pipeline.d.ts +15 -29
  157. package/dist/server/request-pipeline.js +24 -59
  158. package/dist/server/revalidation-host.d.ts +11 -0
  159. package/dist/server/revalidation-host.js +19 -0
  160. package/dist/server/rsc-stream-hints.js +122 -18
  161. package/dist/server/worker-revalidation-context.d.ts +13 -0
  162. package/dist/server/worker-revalidation-context.js +29 -0
  163. package/dist/shims/app.d.ts +1 -1
  164. package/dist/shims/cache-handler.d.ts +1 -1
  165. package/dist/shims/cache-handler.js +10 -7
  166. package/dist/shims/cache-request-state.d.ts +27 -1
  167. package/dist/shims/cache-request-state.js +60 -1
  168. package/dist/shims/cache-runtime.d.ts +18 -1
  169. package/dist/shims/cache-runtime.js +26 -8
  170. package/dist/shims/cache.d.ts +5 -5
  171. package/dist/shims/cache.js +27 -11
  172. package/dist/shims/document.d.ts +42 -83
  173. package/dist/shims/document.js +62 -53
  174. package/dist/shims/dynamic.d.ts +5 -25
  175. package/dist/shims/dynamic.js +29 -8
  176. package/dist/shims/error.d.ts +1 -1
  177. package/dist/shims/error.js +2 -2
  178. package/dist/shims/fetch-cache.js +13 -4
  179. package/dist/shims/font-google-base.d.ts +11 -4
  180. package/dist/shims/font-google-base.js +17 -8
  181. package/dist/shims/font-local.d.ts +10 -3
  182. package/dist/shims/font-local.js +41 -10
  183. package/dist/shims/head.d.ts +13 -2
  184. package/dist/shims/head.js +3 -3
  185. package/dist/shims/headers.d.ts +37 -43
  186. package/dist/shims/headers.js +183 -74
  187. package/dist/shims/image.d.ts +27 -37
  188. package/dist/shims/image.js +21 -14
  189. package/dist/shims/internal/cookie-serialize.d.ts +12 -9
  190. package/dist/shims/internal/cookie-serialize.js +10 -7
  191. package/dist/shims/internal/hybrid-client-route-owner.js +3 -1
  192. package/dist/shims/internal/interpolate-as.d.ts +15 -1
  193. package/dist/shims/internal/interpolate-as.js +27 -2
  194. package/dist/shims/internal/utils.d.ts +3 -3
  195. package/dist/shims/legacy-image.d.ts +3 -37
  196. package/dist/shims/legacy-image.js +6 -16
  197. package/dist/shims/link.d.ts +15 -22
  198. package/dist/shims/link.js +34 -13
  199. package/dist/shims/metadata.d.ts +7 -17
  200. package/dist/shims/metadata.js +74 -24
  201. package/dist/shims/navigation-context-state.d.ts +1 -1
  202. package/dist/shims/navigation.d.ts +7 -3
  203. package/dist/shims/navigation.js +25 -14
  204. package/dist/shims/navigation.react-server.js +1 -1
  205. package/dist/shims/next-shims-augmentations.d.ts +1 -0
  206. package/dist/shims/next-shims-public.d.ts +1 -0
  207. package/dist/shims/public-shim-map.json.js +103 -0
  208. package/dist/shims/readonly-url-search-params.d.ts +3 -3
  209. package/dist/shims/readonly-url-search-params.js +3 -3
  210. package/dist/shims/request-context.d.ts +4 -1
  211. package/dist/shims/request-state-types.d.ts +1 -1
  212. package/dist/shims/router.d.ts +55 -21
  213. package/dist/shims/router.js +180 -44
  214. package/dist/shims/script.d.ts +6 -10
  215. package/dist/shims/script.js +7 -4
  216. package/dist/shims/server.d.ts +50 -26
  217. package/dist/shims/server.js +193 -55
  218. package/dist/shims/unified-request-context.d.ts +22 -3
  219. package/dist/shims/unified-request-context.js +84 -1
  220. package/dist/shims/url-safety.d.ts +1 -1
  221. package/dist/shims/url-safety.js +2 -2
  222. package/dist/typegen.d.ts +8 -2
  223. package/dist/typegen.js +76 -15
  224. package/dist/utils/cache-control-metadata.d.ts +2 -1
  225. package/dist/utils/cache-control-metadata.js +5 -1
  226. package/dist/utils/domain-locale.d.ts +2 -2
  227. package/dist/utils/external-url.d.ts +5 -0
  228. package/dist/utils/external-url.js +7 -0
  229. package/dist/utils/html-limited-bots.js +1 -1
  230. package/dist/utils/protocol-headers.d.ts +7 -1
  231. package/dist/utils/protocol-headers.js +7 -1
  232. package/dist/utils/regex-safety.d.ts +8 -0
  233. package/dist/utils/regex-safety.js +737 -0
  234. package/package.json +13 -2
@@ -459,13 +459,16 @@ function discoverSlotSubRoutes(routes, matcher, ghostParents = []) {
459
459
  const subPage = slotPages.get(slot.key);
460
460
  if (subPage !== void 0) {
461
461
  const configLayoutPaths = findSlotConfigLayoutPaths(slot.ownerDir, subPage, matcher);
462
- return {
462
+ const notFoundBoundary = findSlotNotFoundBoundary(slot.ownerDir, subPage, matcher);
463
+ return withSlotLoadingEntries({
463
464
  ...slot,
464
465
  pagePath: subPage,
465
466
  configLayoutPaths,
466
467
  configLayoutTreePositions: findSlotConfigLayoutTreePositions(slot.ownerDir, configLayoutPaths),
468
+ notFoundPath: notFoundBoundary.path,
469
+ notFoundTreePosition: notFoundBoundary.treePosition,
467
470
  routeSegments: rawSegments
468
- };
471
+ }, subPage, matcher);
469
472
  }
470
473
  return slot;
471
474
  });
@@ -545,13 +548,16 @@ function discoverSlotSubRoutes(routes, matcher, ghostParents = []) {
545
548
  const subSlots = parentRoute.parallelSlots.map((slot) => {
546
549
  const subPage = slotPages.get(slot.key);
547
550
  const configLayoutPaths = findSlotConfigLayoutPaths(slot.ownerDir, subPage ?? null, matcher);
548
- return {
551
+ const notFoundBoundary = findSlotNotFoundBoundary(slot.ownerDir, subPage ?? null, matcher);
552
+ return withSlotLoadingEntries({
549
553
  ...slot,
550
554
  pagePath: subPage || null,
551
555
  configLayoutPaths,
552
556
  configLayoutTreePositions: findSlotConfigLayoutTreePositions(slot.ownerDir, configLayoutPaths),
557
+ notFoundPath: notFoundBoundary.path,
558
+ notFoundTreePosition: notFoundBoundary.treePosition,
553
559
  routeSegments: subPage ? rawSegments : null
554
- };
560
+ }, subPage ?? null, matcher);
555
561
  });
556
562
  const newRoute = {
557
563
  ids: createAppRouteSemanticIds({
@@ -574,14 +580,19 @@ function discoverSlotSubRoutes(routes, matcher, ghostParents = []) {
574
580
  ownerTreePath: childrenOwnerTreePath,
575
581
  state: childrenDefault ? "default" : childrenCatchAll ? "active" : "unmatched"
576
582
  },
577
- loadingPath: parentRoute.loadingPath,
583
+ loadingPath: null,
584
+ loadingPaths: parentRoute.loadingPaths,
585
+ loadingTreePositions: parentRoute.loadingTreePositions,
578
586
  errorPath: parentRoute.errorPath,
579
587
  layoutErrorPaths: parentRoute.layoutErrorPaths,
580
588
  notFoundPath: parentRoute.notFoundPath,
589
+ notFoundTreePosition: parentRoute.notFoundTreePosition,
581
590
  notFoundPaths: parentRoute.notFoundPaths,
582
591
  forbiddenPaths: parentRoute.forbiddenPaths,
583
592
  forbiddenPath: parentRoute.forbiddenPath,
593
+ forbiddenTreePosition: parentRoute.forbiddenTreePosition,
584
594
  unauthorizedPath: parentRoute.unauthorizedPath,
595
+ unauthorizedTreePosition: parentRoute.unauthorizedTreePosition,
585
596
  unauthorizedPaths: parentRoute.unauthorizedPaths,
586
597
  routeSegments: [...parentRoute.routeSegments, ...rawSegments],
587
598
  childrenRouteSegments: childrenDefault ? parentRoute.routeSegments : void 0,
@@ -669,6 +680,55 @@ function findSlotConfigLayoutTreePositions(slotDir, layoutPaths) {
669
680
  return relativeDir ? relativeDir.split(path.sep).filter(Boolean).length : 0;
670
681
  });
671
682
  }
683
+ function findSlotLoadingEntries(slotDir, pagePath, matcher) {
684
+ const pageDir = pagePath ? path.dirname(pagePath) : slotDir;
685
+ if (pageDir !== slotDir && !pageDir.startsWith(`${slotDir}${path.sep}`)) return [];
686
+ const relativeDir = path.relative(slotDir, pageDir);
687
+ const segments = relativeDir ? relativeDir.split(path.sep).filter(Boolean) : [];
688
+ const loadings = [];
689
+ let currentDir = slotDir;
690
+ const rootLoading = findFile(currentDir, "loading", matcher);
691
+ if (rootLoading) loadings.push({
692
+ path: rootLoading,
693
+ treePosition: 0
694
+ });
695
+ for (let index = 0; index < segments.length; index++) {
696
+ currentDir = path.join(currentDir, segments[index]);
697
+ const loading = findFile(currentDir, "loading", matcher);
698
+ if (loading) loadings.push({
699
+ path: loading,
700
+ treePosition: index + 1
701
+ });
702
+ }
703
+ return loadings;
704
+ }
705
+ function withSlotLoadingEntries(slot, pagePath, matcher) {
706
+ const loadingEntries = findSlotLoadingEntries(slot.ownerDir, pagePath, matcher);
707
+ return {
708
+ ...slot,
709
+ loadingPaths: loadingEntries.map((entry) => entry.path),
710
+ loadingTreePositions: loadingEntries.map((entry) => entry.treePosition)
711
+ };
712
+ }
713
+ function findSlotNotFoundBoundary(slotDir, pagePath, matcher) {
714
+ let dir = pagePath ? path.dirname(pagePath) : slotDir;
715
+ while (dir === slotDir || dir.startsWith(`${slotDir}${path.sep}`)) {
716
+ const boundaryPath = findFile(dir, "not-found", matcher);
717
+ if (boundaryPath) {
718
+ const relativeDir = path.relative(slotDir, dir);
719
+ return {
720
+ path: boundaryPath,
721
+ treePosition: relativeDir ? relativeDir.split(path.sep).filter(Boolean).length : 0
722
+ };
723
+ }
724
+ if (dir === slotDir) break;
725
+ dir = path.dirname(dir);
726
+ }
727
+ return {
728
+ path: null,
729
+ treePosition: null
730
+ };
731
+ }
672
732
  /**
673
733
  * Find a sibling catch-all page directly under `dir`, i.e. a `[...slug]` or
674
734
  * `[[...slug]]` directory that contains a `page` file. Returns the absolute
@@ -723,6 +783,9 @@ function directoryToAppRoute(dir, appDir, matcher, pagePath, routePath, includeN
723
783
  const templateTreePositions = computeLayoutTreePositions(appDir, templates);
724
784
  const layoutTreePositions = computeLayoutTreePositions(appDir, layouts);
725
785
  const layoutErrorPaths = discoverLayoutAlignedErrors(segments, appDir, matcher);
786
+ const loadingEntries = discoverSegmentLoadings(segments, appDir, matcher);
787
+ const loadingPaths = loadingEntries.map((entry) => entry.path);
788
+ const loadingTreePositions = loadingEntries.map((entry) => entry.treePosition);
726
789
  const errorEntries = discoverSegmentErrors(segments, appDir, matcher);
727
790
  const errorPaths = errorEntries.map((entry) => entry.path);
728
791
  const errorTreePositions = errorEntries.map((entry) => entry.treePosition);
@@ -730,9 +793,12 @@ function directoryToAppRoute(dir, appDir, matcher, pagePath, routePath, includeN
730
793
  const effectivePagePath = pagePath ?? (routePath ? null : findFile(routeDir, "default", matcher));
731
794
  const loadingPath = findFile(routeDir, "loading", matcher);
732
795
  const errorPath = findFile(routeDir, "error", matcher);
733
- const notFoundPath = discoverBoundaryFile(segments, appDir, "not-found", matcher);
734
- const forbiddenPath = discoverBoundaryFile(segments, appDir, "forbidden", matcher);
735
- const unauthorizedPath = discoverBoundaryFile(segments, appDir, "unauthorized", matcher);
796
+ const notFoundEntry = discoverBoundaryFileEntry(segments, appDir, "not-found", matcher);
797
+ const notFoundPath = notFoundEntry?.path ?? null;
798
+ const forbiddenEntry = discoverBoundaryFileEntry(segments, appDir, "forbidden", matcher);
799
+ const forbiddenPath = forbiddenEntry?.path ?? null;
800
+ const unauthorizedEntry = discoverBoundaryFileEntry(segments, appDir, "unauthorized", matcher);
801
+ const unauthorizedPath = unauthorizedEntry?.path ?? null;
736
802
  const notFoundPaths = discoverBoundaryFilePerLayout(layouts, "not-found", matcher);
737
803
  const forbiddenPaths = discoverBoundaryFilePerLayout(layouts, "forbidden", matcher);
738
804
  const unauthorizedPaths = discoverBoundaryFilePerLayout(layouts, "unauthorized", matcher);
@@ -754,15 +820,20 @@ function directoryToAppRoute(dir, appDir, matcher, pagePath, routePath, includeN
754
820
  templates,
755
821
  parallelSlots,
756
822
  loadingPath,
823
+ loadingPaths,
824
+ loadingTreePositions,
757
825
  errorPath,
758
826
  layoutErrorPaths,
759
827
  errorPaths,
760
828
  errorTreePositions,
761
829
  notFoundPath,
830
+ notFoundTreePosition: notFoundEntry?.treePosition ?? null,
762
831
  notFoundPaths,
763
832
  forbiddenPaths,
764
833
  forbiddenPath,
834
+ forbiddenTreePosition: forbiddenEntry?.treePosition ?? null,
765
835
  unauthorizedPath,
836
+ unauthorizedTreePosition: unauthorizedEntry?.treePosition ?? null,
766
837
  unauthorizedPaths,
767
838
  routeSegments: segments,
768
839
  templateTreePositions,
@@ -899,6 +970,31 @@ function discoverSegmentErrors(segments, appDir, matcher) {
899
970
  return errors;
900
971
  }
901
972
  /**
973
+ * Discover loading.tsx files by segment tree position.
974
+ *
975
+ * Loading conventions belong to loader-tree segments rather than layouts. A
976
+ * segment without layout.tsx can therefore provide the boundary that suspends
977
+ * while its child layout renders.
978
+ */
979
+ function discoverSegmentLoadings(segments, appDir, matcher) {
980
+ const loadings = [];
981
+ const rootLoading = findFile(appDir, "loading", matcher);
982
+ if (rootLoading) loadings.push({
983
+ path: rootLoading,
984
+ treePosition: 0
985
+ });
986
+ let currentDir = appDir;
987
+ for (let index = 0; index < segments.length; index++) {
988
+ currentDir = path.join(currentDir, segments[index]);
989
+ const loading = findFile(currentDir, "loading", matcher);
990
+ if (loading) loadings.push({
991
+ path: loading,
992
+ treePosition: index + 1
993
+ });
994
+ }
995
+ return loadings;
996
+ }
997
+ /**
902
998
  * Discover error.tsx files aligned with the layouts array.
903
999
  *
904
1000
  * Route manifests still model layout-owned boundary facts by layout index.
@@ -921,7 +1017,7 @@ function discoverLayoutAlignedErrors(segments, appDir, matcher) {
921
1017
  * by walking from the route's directory up to the app root.
922
1018
  * Returns the first (closest) file found, or null.
923
1019
  */
924
- function discoverBoundaryFile(segments, appDir, fileName, matcher) {
1020
+ function discoverBoundaryFileEntry(segments, appDir, fileName, matcher) {
925
1021
  const dirs = [];
926
1022
  let dir = appDir;
927
1023
  dirs.push(dir);
@@ -931,7 +1027,10 @@ function discoverBoundaryFile(segments, appDir, fileName, matcher) {
931
1027
  }
932
1028
  for (let i = dirs.length - 1; i >= 0; i--) {
933
1029
  const f = findFile(dirs[i], fileName, matcher);
934
- if (f) return f;
1030
+ if (f) return {
1031
+ path: f,
1032
+ treePosition: i
1033
+ };
935
1034
  }
936
1035
  return null;
937
1036
  }
@@ -1000,7 +1099,7 @@ function discoverInheritedParallelSlots(segments, appDir, routeDir, matcher, inc
1000
1099
  slotParamNames = [...ownerUrl?.params ?? [], ...mirror.slotParamNames];
1001
1100
  }
1002
1101
  const configLayoutPaths = findSlotConfigLayoutPaths(slot.ownerDir, mirror?.pagePath ?? null, matcher);
1003
- const inheritedSlot = {
1102
+ const inheritedSlot = withSlotLoadingEntries({
1004
1103
  ...slot,
1005
1104
  pagePath: mirror?.pagePath ?? null,
1006
1105
  configLayoutPaths,
@@ -1009,7 +1108,7 @@ function discoverInheritedParallelSlots(segments, appDir, routeDir, matcher, inc
1009
1108
  routeSegments: mirror?.segments ?? null,
1010
1109
  slotPatternParts,
1011
1110
  slotParamNames
1012
- };
1111
+ }, mirror?.pagePath ?? null, matcher);
1013
1112
  slotMap.set(slot.key, inheritedSlot);
1014
1113
  }
1015
1114
  }
@@ -1172,12 +1271,15 @@ function discoverParallelSlots(dir, appDir, matcher, includeNestedOnlySlots = fa
1172
1271
  const ownerSegments = path.relative(appDir, dir).split(path.sep).filter((segment) => segment.length > 0);
1173
1272
  const ownerTreePath = createAppRouteGraphTreePath(ownerSegments, ownerSegments.length);
1174
1273
  const configLayoutPaths = findSlotConfigLayoutPaths(slotDir, pagePath, matcher);
1274
+ const loadingEntries = findSlotLoadingEntries(slotDir, pagePath, matcher);
1275
+ const notFoundBoundary = findSlotNotFoundBoundary(slotDir, pagePath, matcher);
1175
1276
  slots.push({
1176
1277
  id: createAppRouteGraphSlotId(slotName, ownerTreePath),
1177
1278
  key: `${slotName}@${path.relative(appDir, slotDir)}`,
1178
1279
  name: slotName,
1179
1280
  ownerDir: slotDir,
1180
1281
  ownerTreePath,
1282
+ ownerTreePosition: ownerSegments.length,
1181
1283
  hasPage: pagePath !== null,
1182
1284
  pagePath,
1183
1285
  defaultPath,
@@ -1185,7 +1287,11 @@ function discoverParallelSlots(dir, appDir, matcher, includeNestedOnlySlots = fa
1185
1287
  configLayoutPaths,
1186
1288
  configLayoutTreePositions: findSlotConfigLayoutTreePositions(slotDir, configLayoutPaths),
1187
1289
  loadingPath: findFile(slotDir, "loading", matcher),
1290
+ loadingPaths: loadingEntries.map((loading) => loading.path),
1291
+ loadingTreePositions: loadingEntries.map((loading) => loading.treePosition),
1188
1292
  errorPath: findFile(slotDir, "error", matcher),
1293
+ notFoundPath: notFoundBoundary.path,
1294
+ notFoundTreePosition: notFoundBoundary.treePosition,
1189
1295
  interceptingRoutes,
1190
1296
  layoutIndex: -1,
1191
1297
  routeSegments: pagePath ? [] : null
@@ -1240,7 +1346,7 @@ function isInterceptionMarkerDir(name) {
1240
1346
  function discoverInterceptingRoutes(slotDir, routeDir, appDir, matcher) {
1241
1347
  if (!fs.existsSync(slotDir)) return [];
1242
1348
  const results = [];
1243
- scanForInterceptingPages(slotDir, routeDir, appDir, results, matcher);
1349
+ scanForInterceptingPages(slotDir, slotDir, routeDir, appDir, results, matcher);
1244
1350
  return results;
1245
1351
  }
1246
1352
  /**
@@ -1277,7 +1383,7 @@ function discoverSiblingInterceptingRoutes(routes, appDir, matcher) {
1277
1383
  const restOfName = entry.name.slice(marker.prefix.length);
1278
1384
  const parentDir = dir;
1279
1385
  const results = [];
1280
- collectInterceptingPages(childDir, childDir, marker.convention, restOfName, parentDir, appDir, parentDir, results, matcher);
1386
+ collectInterceptingPages(childDir, childDir, marker.convention, restOfName, parentDir, appDir, parentDir, null, results, matcher);
1281
1387
  for (const ir of results) {
1282
1388
  ir.slotId = createAppRouteGraphSiblingInterceptSlotId(ir.sourceMatchPattern);
1283
1389
  const owner = findOwnerRouteForDir(parentDir, appDir, routes, routesByDir);
@@ -1334,7 +1440,7 @@ function findOwnerRouteForDir(dir, appDir, routes, routesByDir) {
1334
1440
  * Recursively scan a directory tree for page.tsx files that are inside
1335
1441
  * intercepting route directories.
1336
1442
  */
1337
- function scanForInterceptingPages(currentDir, routeDir, appDir, results, matcher) {
1443
+ function scanForInterceptingPages(currentDir, slotRootDir, routeDir, appDir, results, matcher, parentLoadingEntries = []) {
1338
1444
  if (!fs.existsSync(currentDir)) return;
1339
1445
  const entries = fs.readdirSync(currentDir, { withFileTypes: true });
1340
1446
  for (const entry of entries) {
@@ -1344,8 +1450,14 @@ function scanForInterceptingPages(currentDir, routeDir, appDir, results, matcher
1344
1450
  const interceptDir = path.join(currentDir, entry.name);
1345
1451
  if (interceptMatch) {
1346
1452
  const restOfName = entry.name.slice(interceptMatch.prefix.length);
1347
- collectInterceptingPages(interceptDir, interceptDir, interceptMatch.convention, restOfName, routeDir, appDir, currentDir, results, matcher);
1348
- } else scanForInterceptingPages(interceptDir, routeDir, appDir, results, matcher);
1453
+ collectInterceptingPages(interceptDir, interceptDir, interceptMatch.convention, restOfName, routeDir, appDir, currentDir, slotRootDir, results, matcher, [], parentLoadingEntries, path.relative(slotRootDir, currentDir).split(path.sep).filter(Boolean).length);
1454
+ } else {
1455
+ const loading = findFile(interceptDir, "loading", matcher);
1456
+ scanForInterceptingPages(interceptDir, slotRootDir, routeDir, appDir, results, matcher, loading ? [...parentLoadingEntries, {
1457
+ path: loading,
1458
+ treePosition: path.relative(slotRootDir, interceptDir).split(path.sep).filter(Boolean).length
1459
+ }] : parentLoadingEntries);
1460
+ }
1349
1461
  }
1350
1462
  }
1351
1463
  /**
@@ -1359,21 +1471,45 @@ function matchInterceptConvention(name) {
1359
1471
  * Collect page.tsx files inside an intercepting route directory tree
1360
1472
  * and compute their target URL patterns.
1361
1473
  */
1362
- function collectInterceptingPages(currentDir, interceptRoot, convention, interceptSegment, routeDir, appDir, interceptParentDir, results, matcher, parentLayoutPaths = []) {
1474
+ function collectInterceptingPages(currentDir, interceptRoot, convention, interceptSegment, routeDir, appDir, interceptParentDir, slotRootDir, results, matcher, parentLayoutPaths = [], parentLoadingEntries = [], treePositionOffset = 0) {
1363
1475
  const currentLayoutPath = findFile(currentDir, "layout", matcher);
1364
1476
  const layoutPaths = currentLayoutPath ? [...parentLayoutPaths, currentLayoutPath] : parentLayoutPaths;
1477
+ const currentLoadingPath = findFile(currentDir, "loading", matcher);
1478
+ const relativeCurrentDir = path.relative(interceptRoot, currentDir);
1479
+ const currentTreePosition = treePositionOffset + 1 + (relativeCurrentDir ? relativeCurrentDir.split(path.sep).filter(Boolean).length : 0);
1480
+ const loadingEntries = currentLoadingPath ? [...parentLoadingEntries, {
1481
+ path: currentLoadingPath,
1482
+ treePosition: currentTreePosition
1483
+ }] : parentLoadingEntries;
1365
1484
  const page = findFile(currentDir, "page", matcher);
1366
1485
  if (page) {
1367
1486
  const targetPattern = computeInterceptTarget(convention, interceptSegment, currentDir, interceptRoot, routeDir, appDir);
1368
1487
  if (targetPattern) {
1369
1488
  const sourceMatchPattern = computeInterceptSourceMatchPattern(interceptParentDir, appDir);
1489
+ const notFoundBoundary = findSlotNotFoundBoundary(slotRootDir ?? interceptRoot, page, matcher);
1490
+ const slotParentSegments = slotRootDir ? path.relative(slotRootDir, interceptParentDir).split(path.sep).filter(Boolean) : [];
1491
+ const branchSegments = [
1492
+ ...slotParentSegments,
1493
+ interceptSegment,
1494
+ ...path.relative(interceptRoot, path.dirname(page)).split(path.sep).filter(Boolean)
1495
+ ];
1370
1496
  results.push({
1371
- branchSegments: [interceptSegment, ...path.relative(interceptRoot, path.dirname(page)).split(path.sep).filter(Boolean)],
1497
+ branchSegments,
1372
1498
  convention,
1373
1499
  layoutPaths: [...layoutPaths],
1374
1500
  layoutSegments: layoutPaths.map((layoutPath) => {
1375
- return [interceptSegment, ...path.relative(interceptRoot, path.dirname(layoutPath)).split(path.sep).filter(Boolean)];
1501
+ const relativeDir = path.relative(interceptRoot, path.dirname(layoutPath));
1502
+ return [
1503
+ ...slotParentSegments,
1504
+ interceptSegment,
1505
+ ...relativeDir.split(path.sep).filter(Boolean)
1506
+ ];
1376
1507
  }),
1508
+ loadingPaths: loadingEntries.map((loading) => loading.path),
1509
+ loadingTreePositions: loadingEntries.map((loading) => loading.treePosition),
1510
+ notFoundBranchSegments: branchSegments,
1511
+ notFoundPath: notFoundBoundary.path,
1512
+ notFoundTreePosition: notFoundBoundary.treePosition === null ? null : slotRootDir ? notFoundBoundary.treePosition : notFoundBoundary.treePosition + 1,
1377
1513
  targetPattern: targetPattern.pattern,
1378
1514
  sourceMatchPattern,
1379
1515
  pagePath: page,
@@ -1387,7 +1523,7 @@ function collectInterceptingPages(currentDir, interceptRoot, convention, interce
1387
1523
  for (const entry of entries) {
1388
1524
  if (!entry.isDirectory()) continue;
1389
1525
  if (entry.name.startsWith("_")) continue;
1390
- collectInterceptingPages(path.join(currentDir, entry.name), interceptRoot, convention, interceptSegment, routeDir, appDir, interceptParentDir, results, matcher, layoutPaths);
1526
+ collectInterceptingPages(path.join(currentDir, entry.name), interceptRoot, convention, interceptSegment, routeDir, appDir, interceptParentDir, slotRootDir, results, matcher, layoutPaths, loadingEntries, treePositionOffset);
1391
1527
  }
1392
1528
  }
1393
1529
  /**
@@ -1615,4 +1751,4 @@ function computeAppRouteStaticSiblings(allRoutes, matchedRoute) {
1615
1751
  return Array.from(siblings);
1616
1752
  }
1617
1753
  //#endregion
1618
- export { buildAppRouteGraph, computeAppRouteStaticSiblings, computeRootParamNames, convertSegmentsToRouteParts, findOwnerRouteForDir, isInvisibleSegment };
1754
+ export { buildAppRouteGraph, computeAppRouteStaticSiblings, computeRootParamNames, convertSegmentsToRouteParts, findOwnerRouteForDir, isInvisibleSegment, patternsStructurallyEquivalent };
@@ -20,7 +20,9 @@ import { buildAppRouteGraph, computeAppRouteStaticSiblings, computeRootParamName
20
20
  let cachedGraph = null;
21
21
  let cachedAppDir = null;
22
22
  let cachedPageExtensionsKey = null;
23
+ let cacheGeneration = 0;
23
24
  function invalidateAppRouteCache() {
25
+ cacheGeneration++;
24
26
  cachedGraph = null;
25
27
  cachedAppDir = null;
26
28
  cachedPageExtensionsKey = null;
@@ -35,12 +37,16 @@ function invalidateAppRouteCache() {
35
37
  async function appRouteGraph(appDir, pageExtensions, matcher) {
36
38
  matcher ??= createValidFileMatcher(pageExtensions);
37
39
  const pageExtensionsKey = JSON.stringify(matcher.extensions);
38
- if (cachedGraph && cachedAppDir === appDir && cachedPageExtensionsKey === pageExtensionsKey) return cachedGraph;
39
- const graph = await buildAppRouteGraph(appDir, matcher);
40
- cachedGraph = graph;
41
- cachedAppDir = appDir;
42
- cachedPageExtensionsKey = pageExtensionsKey;
43
- return graph;
40
+ while (true) {
41
+ if (cachedGraph && cachedAppDir === appDir && cachedPageExtensionsKey === pageExtensionsKey) return cachedGraph;
42
+ const scanGeneration = cacheGeneration;
43
+ const graph = await buildAppRouteGraph(appDir, matcher);
44
+ if (scanGeneration !== cacheGeneration) continue;
45
+ cachedGraph = graph;
46
+ cachedAppDir = appDir;
47
+ cachedPageExtensionsKey = pageExtensionsKey;
48
+ return graph;
49
+ }
44
50
  }
45
51
  /**
46
52
  * Scan the app/ directory and return a list of routes.
@@ -24,32 +24,21 @@ declare function findFileWithExtensions(basePath: string, matcher: ValidFileMatc
24
24
  */
25
25
  declare function findFileWithExts(dir: string, name: string, matcher: ValidFileMatcher): string | null;
26
26
  /**
27
- * Vite's default `resolve.extensions` covers `.tsx/.ts/.jsx/.js/.json` (and
28
- * `.mjs/.mts`). When the user configures `pageExtensions` with values Vite
29
- * does not know about — e.g. `["platform.tsx", "tsx", "mdx"]` from the
30
- * Next.js `resolve-extensions` fixture — extensionless imports of those
31
- * files fail to resolve, and the build crashes with "Custom deploy script
32
- * failed: undefined (1)".
27
+ * Add the config extensions produced by `vinext init` to vinext's resolver.
33
28
  *
34
- * Build the merged extension list that Vite should use:
29
+ * `pageExtensions` is intentionally not part of module resolution. Next.js
30
+ * uses it to discover route files; custom module extensions are configured
31
+ * separately through `turbopack.resolveExtensions` or webpack
32
+ * `resolve.extensions`.
35
33
  *
36
- * 1. User-configured pageExtensions go first (each prefixed with `.`) so
37
- * the user's priority wins. e.g. `.platform.tsx` resolves before `.tsx`.
38
- * 2. Vite's defaults follow, with duplicates removed.
39
- * 3. `.cjs`/`.cts` go last (lowest priority). Neither Vite's defaults nor the
40
- * user's pageExtensions include them, but `vinext init` renames CJS config
41
- * files (e.g. `tailwind.config.js` → `tailwind.config.cjs`) when it adds
42
- * `"type": "module"`, and app code imports those extensionlessly
43
- * (`import cfg from "../tailwind.config"`). Without these, the bundle fails
44
- * with "[UNRESOLVED_IMPORT] Could not resolve '../tailwind.config'".
34
+ * The default order preserves vinext's existing module-resolution behavior
35
+ * and matches Next.js Turbopack for overlapping JavaScript and TypeScript
36
+ * extensions.
45
37
  *
46
- * The user's pageExtensions retain their relative order, which is what
47
- * Next.js / Turbopack do via the `resolveExtensions` config option.
48
- *
49
- * See: cloudflare/vinext#1502 for page-extension ordering, and
50
- * cloudflare/vinext#2435 for extensionless `.cjs` config imports.
38
+ * `.cjs`/`.cts` go last because `vinext init` renames CJS config files when it
39
+ * adds `"type": "module"`, and app code may import those files extensionlessly.
51
40
  */
52
- declare function buildViteResolveExtensions(pageExtensions?: readonly string[] | null, viteDefaults?: readonly string[]): string[];
41
+ declare function buildViteResolveExtensions(viteExtensions?: readonly string[]): string[];
53
42
  /**
54
43
  * Normalize an explicit Next.js resolver extension list for Vite.
55
44
  *
@@ -9,6 +9,15 @@ const DEFAULT_PAGE_EXTENSIONS = [
9
9
  "jsx",
10
10
  "js"
11
11
  ];
12
+ const DEFAULT_VINEXT_RESOLVE_EXTENSIONS = [
13
+ ".tsx",
14
+ ".ts",
15
+ ".jsx",
16
+ ".js",
17
+ ".mjs",
18
+ ".mts",
19
+ ".json"
20
+ ];
12
21
  function normalizePageExtensions(pageExtensions) {
13
22
  if (!Array.isArray(pageExtensions) || pageExtensions.length === 0) return [...DEFAULT_PAGE_EXTENSIONS];
14
23
  const filtered = pageExtensions.filter((ext) => typeof ext === "string").map((ext) => ext.trim().replace(/^\.+/, "")).filter((ext) => ext.length > 0);
@@ -79,46 +88,25 @@ function findFileWithExts(dir, name, matcher) {
79
88
  return null;
80
89
  }
81
90
  /**
82
- * Vite's default `resolve.extensions` covers `.tsx/.ts/.jsx/.js/.json` (and
83
- * `.mjs/.mts`). When the user configures `pageExtensions` with values Vite
84
- * does not know about — e.g. `["platform.tsx", "tsx", "mdx"]` from the
85
- * Next.js `resolve-extensions` fixture — extensionless imports of those
86
- * files fail to resolve, and the build crashes with "Custom deploy script
87
- * failed: undefined (1)".
91
+ * Add the config extensions produced by `vinext init` to vinext's resolver.
88
92
  *
89
- * Build the merged extension list that Vite should use:
93
+ * `pageExtensions` is intentionally not part of module resolution. Next.js
94
+ * uses it to discover route files; custom module extensions are configured
95
+ * separately through `turbopack.resolveExtensions` or webpack
96
+ * `resolve.extensions`.
90
97
  *
91
- * 1. User-configured pageExtensions go first (each prefixed with `.`) so
92
- * the user's priority wins. e.g. `.platform.tsx` resolves before `.tsx`.
93
- * 2. Vite's defaults follow, with duplicates removed.
94
- * 3. `.cjs`/`.cts` go last (lowest priority). Neither Vite's defaults nor the
95
- * user's pageExtensions include them, but `vinext init` renames CJS config
96
- * files (e.g. `tailwind.config.js` → `tailwind.config.cjs`) when it adds
97
- * `"type": "module"`, and app code imports those extensionlessly
98
- * (`import cfg from "../tailwind.config"`). Without these, the bundle fails
99
- * with "[UNRESOLVED_IMPORT] Could not resolve '../tailwind.config'".
98
+ * The default order preserves vinext's existing module-resolution behavior
99
+ * and matches Next.js Turbopack for overlapping JavaScript and TypeScript
100
+ * extensions.
100
101
  *
101
- * The user's pageExtensions retain their relative order, which is what
102
- * Next.js / Turbopack do via the `resolveExtensions` config option.
103
- *
104
- * See: cloudflare/vinext#1502 for page-extension ordering, and
105
- * cloudflare/vinext#2435 for extensionless `.cjs` config imports.
102
+ * `.cjs`/`.cts` go last because `vinext init` renames CJS config files when it
103
+ * adds `"type": "module"`, and app code may import those files extensionlessly.
106
104
  */
107
- function buildViteResolveExtensions(pageExtensions, viteDefaults = [
108
- ".mjs",
109
- ".js",
110
- ".mts",
111
- ".ts",
112
- ".jsx",
113
- ".tsx",
114
- ".json"
115
- ]) {
116
- const dotted = normalizePageExtensions(pageExtensions).map((ext) => `.${ext}`);
105
+ function buildViteResolveExtensions(viteExtensions = DEFAULT_VINEXT_RESOLVE_EXTENSIONS) {
117
106
  const seen = /* @__PURE__ */ new Set();
118
107
  const result = [];
119
108
  for (const ext of [
120
- ...dotted,
121
- ...viteDefaults,
109
+ ...viteExtensions,
122
110
  ".cjs",
123
111
  ".cts"
124
112
  ]) {
@@ -2,7 +2,7 @@ import path from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
2
2
  import { decodeRouteSegment, sortRoutes } from "./utils.js";
3
3
  import { createValidFileMatcher, scanWithExtensions } from "./file-matcher.js";
4
4
  import { patternToNextFormat, validateRoutePatterns } from "./route-validation.js";
5
- import { createRouteTrieCache, matchRouteWithTrie } from "./route-matching.js";
5
+ import { createRouteTrieCache, matchRouteWithTrieRawPathname } from "./route-matching.js";
6
6
  //#region src/routing/pages-router.ts
7
7
  /** Next.js special pages that should not produce routes. */
8
8
  const RESERVED_PAGE_NAMES = /* @__PURE__ */ new Set([
@@ -11,11 +11,13 @@ const RESERVED_PAGE_NAMES = /* @__PURE__ */ new Set([
11
11
  "_error"
12
12
  ]);
13
13
  const routeCache = /* @__PURE__ */ new Map();
14
+ let routeCacheGeneration = 0;
14
15
  /**
15
16
  * Invalidate cached routes for a given pages directory.
16
17
  * Called by the file watcher when pages are added/removed.
17
18
  */
18
19
  function invalidateRouteCache(pagesDir) {
20
+ routeCacheGeneration++;
19
21
  for (const key of routeCache.keys()) if (key.startsWith(`pages:${pagesDir}:`) || key.startsWith(`api:${pagesDir}:`)) routeCache.delete(key);
20
22
  }
21
23
  /**
@@ -32,20 +34,7 @@ function invalidateRouteCache(pagesDir) {
32
34
  */
33
35
  async function pagesRouter(pagesDir, pageExtensions, matcher) {
34
36
  matcher ??= createValidFileMatcher(pageExtensions);
35
- const cacheKey = `pages:${pagesDir}:${JSON.stringify(matcher.extensions)}`;
36
- const cached = routeCache.get(cacheKey);
37
- if (cached) return cached.promise;
38
- const promise = scanPageRoutes(pagesDir, matcher);
39
- routeCache.set(cacheKey, {
40
- routes: [],
41
- promise
42
- });
43
- const routes = await promise;
44
- routeCache.set(cacheKey, {
45
- routes,
46
- promise
47
- });
48
- return routes;
37
+ return getCachedRoutes(`pages:${pagesDir}:${JSON.stringify(matcher.extensions)}`, () => scanPageRoutes(pagesDir, matcher));
49
38
  }
50
39
  async function scanPageRoutes(pagesDir, matcher) {
51
40
  const routes = [];
@@ -114,7 +103,7 @@ const trieCache = createRouteTrieCache();
114
103
  * Returns the matched params or null if no match.
115
104
  */
116
105
  function matchRoute(url, routes) {
117
- return matchRouteWithTrie(url, routes, trieCache);
106
+ return matchRouteWithTrieRawPathname(url, routes, trieCache);
118
107
  }
119
108
  /**
120
109
  * Scan the pages/api/ directory and return API routes.
@@ -126,20 +115,25 @@ function matchRoute(url, routes) {
126
115
  */
127
116
  async function apiRouter(pagesDir, pageExtensions, matcher) {
128
117
  matcher ??= createValidFileMatcher(pageExtensions);
129
- const cacheKey = `api:${pagesDir}:${JSON.stringify(matcher.extensions)}`;
118
+ return getCachedRoutes(`api:${pagesDir}:${JSON.stringify(matcher.extensions)}`, () => scanApiRoutes(pagesDir, matcher));
119
+ }
120
+ async function getCachedRoutes(cacheKey, scan) {
130
121
  const cached = routeCache.get(cacheKey);
131
- if (cached) return cached.promise;
132
- const promise = scanApiRoutes(pagesDir, matcher);
133
- routeCache.set(cacheKey, {
134
- routes: [],
135
- promise
136
- });
137
- const routes = await promise;
138
- routeCache.set(cacheKey, {
139
- routes,
140
- promise
141
- });
142
- return routes;
122
+ if (cached) return cached;
123
+ const promise = (async () => {
124
+ while (true) {
125
+ const scanGeneration = routeCacheGeneration;
126
+ const routes = await scan();
127
+ if (scanGeneration === routeCacheGeneration) return routes;
128
+ }
129
+ })();
130
+ routeCache.set(cacheKey, promise);
131
+ try {
132
+ return await promise;
133
+ } catch (error) {
134
+ if (routeCache.get(cacheKey) === promise) routeCache.delete(cacheKey);
135
+ throw error;
136
+ }
143
137
  }
144
138
  async function scanApiRoutes(pagesDir, matcher) {
145
139
  const apiDir = path.join(pagesDir, "api");
@@ -23,5 +23,18 @@ declare function matchRouteWithTrie<R extends {
23
23
  route: R;
24
24
  params: Record<string, string | string[]>;
25
25
  } | null;
26
+ /**
27
+ * Match a filesystem route against the request's raw encoded pathname.
28
+ *
29
+ * Next.js compares static route identity before decoding, so `/%61bout` does
30
+ * not select the filesystem route `/about`. Dynamic captures are still decoded
31
+ * exactly once by `trieMatch`, matching `getRouteMatcher` upstream.
32
+ */
33
+ declare function matchRouteWithTrieRawPathname<R extends {
34
+ patternParts: string[];
35
+ }>(url: string, routes: R[], cache: RouteTrieCache<R>): {
36
+ route: R;
37
+ params: Record<string, string | string[]>;
38
+ } | null;
26
39
  //#endregion
27
- export { createRouteTrieCache, matchRouteWithTrie };
40
+ export { createRouteTrieCache, matchRouteWithTrie, matchRouteWithTrieRawPathname };
@@ -38,5 +38,17 @@ function matchRouteWithTrie(url, routes, cache) {
38
38
  const urlParts = normalizedUrl.split("/").filter(Boolean);
39
39
  return trieMatch(getOrBuildTrie(cache, routes), urlParts);
40
40
  }
41
+ /**
42
+ * Match a filesystem route against the request's raw encoded pathname.
43
+ *
44
+ * Next.js compares static route identity before decoding, so `/%61bout` does
45
+ * not select the filesystem route `/about`. Dynamic captures are still decoded
46
+ * exactly once by `trieMatch`, matching `getRouteMatcher` upstream.
47
+ */
48
+ function matchRouteWithTrieRawPathname(url, routes, cache) {
49
+ const pathname = url.split("?")[0];
50
+ const urlParts = (pathname === "/" ? "/" : pathname.replace(/\/$/, "")).split("/").filter(Boolean);
51
+ return trieMatch(getOrBuildTrie(cache, routes), urlParts);
52
+ }
41
53
  //#endregion
42
- export { createRouteTrieCache, matchRouteWithTrie };
54
+ export { createRouteTrieCache, matchRouteWithTrie, matchRouteWithTrieRawPathname };