vinext 0.1.2 → 0.1.4

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 (243) hide show
  1. package/dist/build/client-build-config.d.ts +11 -2
  2. package/dist/build/client-build-config.js +17 -6
  3. package/dist/build/prerender.d.ts +9 -1
  4. package/dist/build/prerender.js +42 -12
  5. package/dist/build/run-prerender.d.ts +10 -2
  6. package/dist/build/run-prerender.js +15 -1
  7. package/dist/client/app-nav-failure-handler.d.ts +8 -0
  8. package/dist/client/app-nav-failure-handler.js +44 -0
  9. package/dist/client/pages-router-link-navigation.d.ts +33 -7
  10. package/dist/client/pages-router-link-navigation.js +32 -2
  11. package/dist/client/vinext-next-data.d.ts +18 -1
  12. package/dist/client/vinext-next-data.js +2 -0
  13. package/dist/client/window-next.d.ts +2 -1
  14. package/dist/client/window-next.js +12 -1
  15. package/dist/cloudflare/src/cache/cdn-adapter.runtime.js +6 -1
  16. package/dist/config/config-matchers.d.ts +11 -1
  17. package/dist/config/config-matchers.js +87 -16
  18. package/dist/config/next-config.d.ts +46 -4
  19. package/dist/config/next-config.js +147 -48
  20. package/dist/config/tsconfig-paths.js +14 -1
  21. package/dist/deploy.d.ts +30 -11
  22. package/dist/deploy.js +200 -112
  23. package/dist/entries/app-browser-entry.d.ts +9 -3
  24. package/dist/entries/app-browser-entry.js +21 -3
  25. package/dist/entries/app-rsc-entry.d.ts +2 -0
  26. package/dist/entries/app-rsc-entry.js +65 -5
  27. package/dist/entries/app-rsc-manifest.js +2 -0
  28. package/dist/entries/app-ssr-entry.js +1 -1
  29. package/dist/entries/pages-client-entry.js +66 -20
  30. package/dist/entries/pages-server-entry.js +47 -31
  31. package/dist/index.js +417 -102
  32. package/dist/plugins/dynamic-preload-metadata.js +2 -4
  33. package/dist/plugins/extensionless-dynamic-import.d.ts +6 -0
  34. package/dist/plugins/extensionless-dynamic-import.js +152 -0
  35. package/dist/plugins/fonts.js +5 -4
  36. package/dist/plugins/optimize-imports.d.ts +2 -1
  37. package/dist/plugins/optimize-imports.js +11 -9
  38. package/dist/plugins/postcss.js +7 -7
  39. package/dist/plugins/strip-server-exports.d.ts +9 -7
  40. package/dist/plugins/strip-server-exports.js +493 -46
  41. package/dist/plugins/typeof-window.d.ts +14 -0
  42. package/dist/plugins/typeof-window.js +150 -0
  43. package/dist/routing/app-route-graph.d.ts +2 -1
  44. package/dist/routing/app-route-graph.js +46 -16
  45. package/dist/routing/file-matcher.d.ts +10 -1
  46. package/dist/routing/file-matcher.js +22 -1
  47. package/dist/routing/pages-router.js +3 -3
  48. package/dist/routing/utils.d.ts +35 -6
  49. package/dist/routing/utils.js +59 -7
  50. package/dist/server/api-handler.d.ts +6 -1
  51. package/dist/server/api-handler.js +21 -15
  52. package/dist/server/app-browser-action-result.d.ts +19 -6
  53. package/dist/server/app-browser-action-result.js +20 -11
  54. package/dist/server/app-browser-entry.js +175 -91
  55. package/dist/server/app-browser-error.d.ts +10 -6
  56. package/dist/server/app-browser-error.js +43 -8
  57. package/dist/server/app-browser-hydration.d.ts +2 -0
  58. package/dist/server/app-browser-hydration.js +1 -0
  59. package/dist/server/app-browser-navigation-controller.d.ts +5 -3
  60. package/dist/server/app-browser-navigation-controller.js +23 -2
  61. package/dist/server/app-browser-server-action-navigation.d.ts +6 -0
  62. package/dist/server/app-browser-server-action-navigation.js +9 -0
  63. package/dist/server/app-browser-state.d.ts +1 -1
  64. package/dist/server/app-browser-state.js +19 -11
  65. package/dist/server/app-browser-stream.js +86 -43
  66. package/dist/server/app-browser-visible-commit.d.ts +1 -1
  67. package/dist/server/app-elements-wire.d.ts +6 -1
  68. package/dist/server/app-elements-wire.js +14 -4
  69. package/dist/server/app-elements.d.ts +2 -2
  70. package/dist/server/app-elements.js +2 -2
  71. package/dist/server/app-fallback-renderer.d.ts +1 -0
  72. package/dist/server/app-fallback-renderer.js +3 -1
  73. package/dist/server/app-optimistic-routing.js +2 -2
  74. package/dist/server/app-page-boundary-render.d.ts +1 -0
  75. package/dist/server/app-page-boundary-render.js +27 -14
  76. package/dist/server/app-page-cache-render.d.ts +53 -0
  77. package/dist/server/app-page-cache-render.js +91 -0
  78. package/dist/server/app-page-cache.d.ts +16 -2
  79. package/dist/server/app-page-cache.js +62 -1
  80. package/dist/server/app-page-dispatch.d.ts +26 -0
  81. package/dist/server/app-page-dispatch.js +149 -92
  82. package/dist/server/app-page-element-builder.d.ts +1 -0
  83. package/dist/server/app-page-element-builder.js +5 -2
  84. package/dist/server/app-page-execution.d.ts +6 -1
  85. package/dist/server/app-page-execution.js +21 -1
  86. package/dist/server/app-page-probe.d.ts +1 -0
  87. package/dist/server/app-page-probe.js +4 -0
  88. package/dist/server/app-page-render-observation.d.ts +3 -1
  89. package/dist/server/app-page-render-observation.js +17 -1
  90. package/dist/server/app-page-render.d.ts +12 -1
  91. package/dist/server/app-page-render.js +42 -4
  92. package/dist/server/app-page-request.d.ts +2 -0
  93. package/dist/server/app-page-request.js +2 -1
  94. package/dist/server/app-page-route-wiring.d.ts +3 -1
  95. package/dist/server/app-page-route-wiring.js +14 -5
  96. package/dist/server/app-page-stream.d.ts +15 -3
  97. package/dist/server/app-page-stream.js +11 -5
  98. package/dist/server/app-pages-bridge.d.ts +23 -1
  99. package/dist/server/app-pages-bridge.js +26 -17
  100. package/dist/server/app-ppr-fallback-shell-render.d.ts +17 -0
  101. package/dist/server/app-ppr-fallback-shell-render.js +26 -0
  102. package/dist/server/app-ppr-fallback-shell.d.ts +13 -1
  103. package/dist/server/app-ppr-fallback-shell.js +8 -1
  104. package/dist/server/app-route-handler-dispatch.js +9 -2
  105. package/dist/server/app-route-handler-policy.d.ts +1 -0
  106. package/dist/server/app-router-entry.js +5 -0
  107. package/dist/server/app-rsc-cache-busting.js +2 -0
  108. package/dist/server/app-rsc-handler.d.ts +28 -0
  109. package/dist/server/app-rsc-handler.js +195 -59
  110. package/dist/server/app-rsc-route-matching.d.ts +3 -0
  111. package/dist/server/app-rsc-route-matching.js +8 -2
  112. package/dist/server/app-segment-config.d.ts +9 -1
  113. package/dist/server/app-segment-config.js +12 -3
  114. package/dist/server/app-server-action-execution.d.ts +1 -0
  115. package/dist/server/app-server-action-execution.js +47 -15
  116. package/dist/server/app-ssr-entry.d.ts +2 -0
  117. package/dist/server/app-ssr-entry.js +84 -39
  118. package/dist/server/before-interactive-head.d.ts +17 -0
  119. package/dist/server/before-interactive-head.js +35 -0
  120. package/dist/server/cache-control.js +4 -0
  121. package/dist/server/csp.js +1 -4
  122. package/dist/server/dev-server.d.ts +2 -2
  123. package/dist/server/dev-server.js +321 -83
  124. package/dist/server/hybrid-route-priority.d.ts +22 -0
  125. package/dist/server/hybrid-route-priority.js +33 -0
  126. package/dist/server/image-optimization.d.ts +18 -9
  127. package/dist/server/image-optimization.js +37 -23
  128. package/dist/server/implicit-tags.d.ts +2 -1
  129. package/dist/server/implicit-tags.js +4 -1
  130. package/dist/server/middleware-matcher.js +12 -3
  131. package/dist/server/middleware-runtime.d.ts +3 -4
  132. package/dist/server/middleware-runtime.js +2 -0
  133. package/dist/server/navigation-planner.d.ts +135 -41
  134. package/dist/server/navigation-planner.js +138 -0
  135. package/dist/server/navigation-trace.d.ts +9 -1
  136. package/dist/server/navigation-trace.js +9 -1
  137. package/dist/server/operation-token.d.ts +40 -0
  138. package/dist/server/operation-token.js +85 -0
  139. package/dist/server/pages-api-route.d.ts +6 -0
  140. package/dist/server/pages-api-route.js +13 -2
  141. package/dist/server/pages-asset-tags.d.ts +2 -1
  142. package/dist/server/pages-asset-tags.js +6 -2
  143. package/dist/server/pages-data-route.d.ts +9 -2
  144. package/dist/server/pages-data-route.js +18 -6
  145. package/dist/server/pages-dev-module-url.d.ts +4 -0
  146. package/dist/server/pages-dev-module-url.js +15 -0
  147. package/dist/server/pages-document-initial-props.d.ts +4 -15
  148. package/dist/server/pages-document-initial-props.js +27 -56
  149. package/dist/server/pages-get-initial-props.d.ts +54 -4
  150. package/dist/server/pages-get-initial-props.js +43 -1
  151. package/dist/server/pages-i18n.js +2 -2
  152. package/dist/server/pages-node-compat.js +2 -2
  153. package/dist/server/pages-page-data.d.ts +11 -2
  154. package/dist/server/pages-page-data.js +207 -34
  155. package/dist/server/pages-page-handler.d.ts +4 -2
  156. package/dist/server/pages-page-handler.js +62 -23
  157. package/dist/server/pages-page-response.d.ts +4 -1
  158. package/dist/server/pages-page-response.js +11 -8
  159. package/dist/server/pages-readiness.js +1 -1
  160. package/dist/server/pages-request-pipeline.d.ts +8 -7
  161. package/dist/server/pages-request-pipeline.js +126 -47
  162. package/dist/server/pregenerated-concrete-paths.d.ts +1 -17
  163. package/dist/server/pregenerated-concrete-paths.js +2 -19
  164. package/dist/server/prerender-manifest.d.ts +33 -0
  165. package/dist/server/prerender-manifest.js +54 -0
  166. package/dist/server/prerender-route-params.d.ts +1 -2
  167. package/dist/server/prod-server.d.ts +3 -1
  168. package/dist/server/prod-server.js +50 -13
  169. package/dist/server/request-pipeline.d.ts +3 -15
  170. package/dist/server/request-pipeline.js +58 -47
  171. package/dist/server/rsc-stream-hints.d.ts +5 -1
  172. package/dist/server/rsc-stream-hints.js +6 -1
  173. package/dist/server/seed-cache.js +10 -18
  174. package/dist/server/static-file-cache.js +16 -4
  175. package/dist/shims/app-router-scroll-state.d.ts +3 -1
  176. package/dist/shims/app-router-scroll-state.js +14 -2
  177. package/dist/shims/app-router-scroll.d.ts +3 -0
  178. package/dist/shims/app-router-scroll.js +28 -18
  179. package/dist/shims/before-interactive-context.d.ts +14 -3
  180. package/dist/shims/cache-runtime.js +3 -2
  181. package/dist/shims/cache.d.ts +1 -0
  182. package/dist/shims/cache.js +1 -1
  183. package/dist/shims/cdn-cache.d.ts +5 -5
  184. package/dist/shims/document.d.ts +15 -20
  185. package/dist/shims/document.js +5 -8
  186. package/dist/shims/dynamic-preload-chunks.js +6 -4
  187. package/dist/shims/error-boundary.d.ts +2 -0
  188. package/dist/shims/error-boundary.js +7 -0
  189. package/dist/shims/error.js +3 -2
  190. package/dist/shims/error.react-server.d.ts +9 -0
  191. package/dist/shims/error.react-server.js +6 -0
  192. package/dist/shims/fetch-cache.d.ts +3 -1
  193. package/dist/shims/fetch-cache.js +45 -20
  194. package/dist/shims/hash-scroll.js +6 -1
  195. package/dist/shims/headers.js +29 -4
  196. package/dist/shims/image.js +9 -2
  197. package/dist/shims/internal/als-registry.js +28 -1
  198. package/dist/shims/internal/app-route-detection.js +8 -17
  199. package/dist/shims/internal/hybrid-client-route-owner.d.ts +31 -0
  200. package/dist/shims/internal/hybrid-client-route-owner.js +143 -0
  201. package/dist/shims/internal/navigation-untracked.d.ts +35 -0
  202. package/dist/shims/internal/navigation-untracked.js +55 -0
  203. package/dist/shims/internal/pages-data-fetch-dedup.d.ts +6 -7
  204. package/dist/shims/internal/pages-data-fetch-dedup.js +67 -14
  205. package/dist/shims/internal/pages-data-target.d.ts +7 -2
  206. package/dist/shims/internal/pages-data-target.js +17 -8
  207. package/dist/shims/internal/pages-router-accessor.d.ts +19 -0
  208. package/dist/shims/internal/pages-router-accessor.js +13 -0
  209. package/dist/shims/internal/router-context.d.ts +2 -1
  210. package/dist/shims/internal/router-context.js +3 -1
  211. package/dist/shims/link.js +47 -19
  212. package/dist/shims/metadata.js +4 -4
  213. package/dist/shims/navigation.d.ts +8 -2
  214. package/dist/shims/navigation.js +63 -31
  215. package/dist/shims/ppr-fallback-shell.d.ts +5 -1
  216. package/dist/shims/ppr-fallback-shell.js +28 -7
  217. package/dist/shims/router.d.ts +18 -3
  218. package/dist/shims/router.js +512 -142
  219. package/dist/shims/script.js +8 -4
  220. package/dist/shims/server.d.ts +16 -1
  221. package/dist/shims/server.js +44 -12
  222. package/dist/shims/unified-request-context.js +1 -0
  223. package/dist/utils/built-asset-url.d.ts +4 -0
  224. package/dist/utils/built-asset-url.js +11 -0
  225. package/dist/utils/commonjs-loader.d.ts +16 -0
  226. package/dist/utils/commonjs-loader.js +100 -0
  227. package/dist/utils/deployment-id.d.ts +8 -0
  228. package/dist/utils/deployment-id.js +22 -0
  229. package/dist/utils/has-trailing-comma.d.ts +24 -0
  230. package/dist/utils/has-trailing-comma.js +62 -0
  231. package/dist/utils/html-limited-bots.d.ts +18 -1
  232. package/dist/utils/html-limited-bots.js +23 -1
  233. package/dist/utils/parse-cookie.d.ts +13 -0
  234. package/dist/utils/parse-cookie.js +52 -0
  235. package/dist/utils/path.d.ts +7 -1
  236. package/dist/utils/path.js +9 -1
  237. package/dist/utils/text-stream.d.ts +1 -1
  238. package/dist/utils/text-stream.js +2 -2
  239. package/dist/utils/vite-version.d.ts +12 -1
  240. package/dist/utils/vite-version.js +9 -1
  241. package/package.json +2 -2
  242. package/dist/shims/internal/parse-cookie-header.d.ts +0 -14
  243. package/dist/shims/internal/parse-cookie-header.js +0 -30
@@ -1,7 +1,7 @@
1
1
  import { removeTrailingSlash, stripBasePath } from "../../utils/base-path.js";
2
- import { createRouteTrieCache, matchRouteWithTrie } from "../../routing/route-matching.js";
2
+ import { getLocalePathPrefix } from "../../utils/domain-locale.js";
3
+ import { resolveHybridClientRouteOwner } from "./hybrid-client-route-owner.js";
3
4
  //#region src/shims/internal/app-route-detection.ts
4
- const appRouteTrieCache = createRouteTrieCache();
5
5
  const _COMPONENTS_KEY = Symbol.for("vinext.pagesRouter.components");
6
6
  /**
7
7
  * Get-or-create the Pages Router `components` map. Returns the same object
@@ -32,20 +32,11 @@ function resolveSameOriginPathname(href, basePath) {
32
32
  return null;
33
33
  }
34
34
  if (url.origin !== window.location.origin) return null;
35
- return stripBasePath(url.pathname, basePath);
36
- }
37
- /**
38
- * Returns true when the prefetch href matches any route in the App Router
39
- * prefetch manifest (static or dynamic). Returns false when the manifest is
40
- * absent (Pages-Router-only build), the URL is external, or no route matches.
41
- */
42
- function matchesAppRoute(href, basePath) {
43
- if (typeof window === "undefined") return false;
44
- const routes = window.__VINEXT_LINK_PREFETCH_ROUTES__;
45
- if (!routes || routes.length === 0) return false;
46
- const pathname = resolveSameOriginPathname(href, basePath);
47
- if (pathname === null) return false;
48
- return matchRouteWithTrie(pathname, routes, appRouteTrieCache) !== null;
35
+ const pathname = stripBasePath(url.pathname, basePath);
36
+ const locale = getLocalePathPrefix(pathname, window.__VINEXT_LOCALES__);
37
+ if (!locale) return pathname;
38
+ const localePrefixLength = locale.length + 1;
39
+ return pathname.length === localePrefixLength ? "/" : pathname.slice(localePrefixLength);
49
40
  }
50
41
  /**
51
42
  * Record `components[pathname] = { __appRouter: true }` on the shared
@@ -61,7 +52,7 @@ function matchesAppRoute(href, basePath) {
61
52
  */
62
53
  function markAppRouteDetectedOnPrefetch(href, basePath) {
63
54
  if (typeof window === "undefined") return;
64
- if (!matchesAppRoute(href, basePath)) return;
55
+ if (resolveHybridClientRouteOwner(href, basePath) !== "app") return;
65
56
  const rawPathname = resolveSameOriginPathname(href, basePath);
66
57
  if (rawPathname === null) return;
67
58
  const pathname = removeTrailingSlash(rawPathname);
@@ -0,0 +1,31 @@
1
+ import { NextRewrite } from "../../config/next-config.js";
2
+ import { VinextLinkPrefetchRoute, VinextPagesLinkPrefetchRoute } from "../../client/vinext-next-data.js";
3
+
4
+ //#region src/shims/internal/hybrid-client-route-owner.d.ts
5
+ type HybridClientOwner = "app" | "document" | "pages";
6
+ declare global {
7
+ interface Window {
8
+ __VINEXT_LINK_PREFETCH_ROUTES__?: VinextLinkPrefetchRoute[];
9
+ __VINEXT_PAGES_LINK_PREFETCH_ROUTES__?: VinextPagesLinkPrefetchRoute[];
10
+ __VINEXT_CLIENT_REWRITES__?: {
11
+ afterFiles: NextRewrite[];
12
+ beforeFiles: NextRewrite[];
13
+ fallback: NextRewrite[];
14
+ };
15
+ }
16
+ }
17
+ /**
18
+ * Decide which router should own a soft-navigated URL. Returns:
19
+ * - "app" → the App Router runtime handles the navigation (RSC fetch).
20
+ * - "pages" → Pages owns the URL; the caller must hard-navigate instead.
21
+ * - null → no router matched (preserves the existing 404 path).
22
+ *
23
+ * `basePath` must match what the page uses (typically `process.env.__NEXT_ROUTER_BASEPATH`).
24
+ *
25
+ * The lookup uses the App and Pages manifests on `window` so the same
26
+ * matcher trie produces the same result the server will see when the
27
+ * request lands.
28
+ */
29
+ declare function resolveHybridClientRouteOwner(href: string, basePath: string): HybridClientOwner | null;
30
+ //#endregion
31
+ export { HybridClientOwner, resolveHybridClientRouteOwner };
@@ -0,0 +1,143 @@
1
+ import { compareHybridRoutePatterns } from "../../routing/utils.js";
2
+ import { stripBasePath } from "../../utils/base-path.js";
3
+ import { createRouteTrieCache, matchRouteWithTrie } from "../../routing/route-matching.js";
4
+ import { isExternalUrl, matchRewrite, parseCookies } from "../../config/config-matchers.js";
5
+ import { getLocalePathPrefix } from "../../utils/domain-locale.js";
6
+ import { mergeRewriteQuery } from "../../utils/query.js";
7
+ //#region src/shims/internal/hybrid-client-route-owner.ts
8
+ /**
9
+ * Client-side resolver that decides whether a URL should be soft-navigated
10
+ * (App Router / RSC) or hard-navigated (Pages Router / document). Delegates
11
+ * the owner decision to `compareHybridRoutePatterns` in `routing/utils.ts`
12
+ * so the server and the client reach the same answer for the same
13
+ * (pages pattern, app pattern) pair.
14
+ *
15
+ * Lives in `shims/internal/` because both `link.tsx` and the App Router
16
+ * browser entry import it without pulling in the server route graph.
17
+ *
18
+ * The App + Pages route manifests are emitted once per page load by the
19
+ * Vite plugin onto the matching `__VINEXT_*_PREFETCH_ROUTES__` window
20
+ * globals (see `entries/app-browser-entry.ts` and
21
+ * `entries/pages-client-entry.ts`). Hybrid builds expose both globals; a
22
+ * single-router build only sets its own.
23
+ */
24
+ function resolveClientRewrite(href, basePath, rewrites, continueAfterMatch = false) {
25
+ const initialUrl = new URL(href, window.location.href);
26
+ const basePathState = {
27
+ basePath,
28
+ hadBasePath: basePath ? initialUrl.pathname === basePath || initialUrl.pathname.startsWith(`${basePath}/`) : true
29
+ };
30
+ let currentHref = href;
31
+ let matched = false;
32
+ for (const rewrite of rewrites) {
33
+ const pathname = resolveSameOriginPathname(currentHref, basePath);
34
+ if (pathname === null) return null;
35
+ const url = new URL(currentHref, window.location.href);
36
+ const headers = new Headers({ "user-agent": globalThis.navigator?.userAgent ?? "" });
37
+ const context = {
38
+ cookies: parseCookies(globalThis.document?.cookie ?? ""),
39
+ headers,
40
+ host: url.hostname,
41
+ query: url.searchParams
42
+ };
43
+ const rewritten = matchRewrite(pathname, [rewrite], context, basePathState);
44
+ if (rewritten === null) continue;
45
+ if (isExternalUrl(rewritten)) return { kind: "document" };
46
+ currentHref = mergeRewriteQuery(currentHref, rewritten);
47
+ matched = true;
48
+ if (!continueAfterMatch) break;
49
+ }
50
+ return matched ? {
51
+ href: currentHref,
52
+ kind: "rewrite"
53
+ } : null;
54
+ }
55
+ const appRouteTrieCache = createRouteTrieCache();
56
+ const pagesRouteTrieCache = createRouteTrieCache();
57
+ /**
58
+ * Build a `/`-joined pattern from a manifest's `patternParts`. Mirrors the
59
+ * server-side route-graph shape (`{ pattern: string }`) so the
60
+ * `compareHybridRoutePatterns` segment-rank comparator can score both Pages
61
+ * and App patterns. The
62
+ * `patternParts` array never includes an empty string for the static `/`
63
+ * route (the App catch-all handles the bare path), so the simple join is
64
+ * safe for everything the route trie actually matches.
65
+ */
66
+ function patternFromParts(parts) {
67
+ return "/" + parts.join("/");
68
+ }
69
+ function resolveSameOriginPathname(href, basePath) {
70
+ if (typeof window === "undefined") return null;
71
+ let url;
72
+ try {
73
+ url = new URL(href, window.location.href);
74
+ } catch {
75
+ return null;
76
+ }
77
+ if (url.origin !== window.location.origin) return null;
78
+ const pathname = stripBasePath(url.pathname, basePath);
79
+ const locale = getLocalePathPrefix(pathname, window.__VINEXT_LOCALES__);
80
+ if (!locale) return pathname;
81
+ const localePrefixLength = locale.length + 1;
82
+ return pathname.length === localePrefixLength ? "/" : pathname.slice(localePrefixLength);
83
+ }
84
+ function matchAppRoute(href, basePath, routes) {
85
+ const pathname = resolveSameOriginPathname(href, basePath);
86
+ if (pathname === null) return null;
87
+ return matchRouteWithTrie(pathname, routes, appRouteTrieCache)?.route ?? null;
88
+ }
89
+ function matchPagesRoute(href, basePath, routes) {
90
+ const pathname = resolveSameOriginPathname(href, basePath);
91
+ if (pathname === null) return null;
92
+ return matchRouteWithTrie(pathname, routes, pagesRouteTrieCache)?.route ?? null;
93
+ }
94
+ /**
95
+ * Decide which router should own a soft-navigated URL. Returns:
96
+ * - "app" → the App Router runtime handles the navigation (RSC fetch).
97
+ * - "pages" → Pages owns the URL; the caller must hard-navigate instead.
98
+ * - null → no router matched (preserves the existing 404 path).
99
+ *
100
+ * `basePath` must match what the page uses (typically `process.env.__NEXT_ROUTER_BASEPATH`).
101
+ *
102
+ * The lookup uses the App and Pages manifests on `window` so the same
103
+ * matcher trie produces the same result the server will see when the
104
+ * request lands.
105
+ */
106
+ function resolveHybridClientRouteOwner(href, basePath) {
107
+ if (typeof window === "undefined") return null;
108
+ const appRoutes = window.__VINEXT_LINK_PREFETCH_ROUTES__;
109
+ const pagesRoutes = window.__VINEXT_PAGES_LINK_PREFETCH_ROUTES__;
110
+ const rewrites = window.__VINEXT_CLIENT_REWRITES__;
111
+ if (rewrites) {
112
+ const beforeFilesRewrite = resolveClientRewrite(href, basePath, rewrites.beforeFiles, true);
113
+ if (beforeFilesRewrite?.kind === "document") return "document";
114
+ if (beforeFilesRewrite?.kind === "rewrite") href = beforeFilesRewrite.href;
115
+ }
116
+ let appMatch = appRoutes ? matchAppRoute(href, basePath, appRoutes) : null;
117
+ let pagesMatch = pagesRoutes ? matchPagesRoute(href, basePath, pagesRoutes) : null;
118
+ if (rewrites && (appMatch === null || appMatch.isDynamic) && (pagesMatch === null || pagesMatch.isDynamic)) for (const rewrite of rewrites.afterFiles) {
119
+ const afterFilesRewrite = resolveClientRewrite(href, basePath, [rewrite]);
120
+ if (afterFilesRewrite?.kind === "document") return "document";
121
+ if (afterFilesRewrite?.kind !== "rewrite") continue;
122
+ href = afterFilesRewrite.href;
123
+ appMatch = appRoutes ? matchAppRoute(href, basePath, appRoutes) : null;
124
+ pagesMatch = pagesRoutes ? matchPagesRoute(href, basePath, pagesRoutes) : null;
125
+ if (appMatch || pagesMatch) break;
126
+ }
127
+ if (rewrites && appMatch === null && pagesMatch === null) for (const rewrite of rewrites.fallback) {
128
+ const fallbackRewrite = resolveClientRewrite(href, basePath, [rewrite]);
129
+ if (fallbackRewrite?.kind === "document") return "document";
130
+ if (fallbackRewrite?.kind !== "rewrite") continue;
131
+ href = fallbackRewrite.href;
132
+ appMatch = appRoutes ? matchAppRoute(href, basePath, appRoutes) : null;
133
+ pagesMatch = pagesRoutes ? matchPagesRoute(href, basePath, pagesRoutes) : null;
134
+ if (appMatch || pagesMatch) break;
135
+ }
136
+ if (appMatch === null && pagesMatch === null) return null;
137
+ if (pagesMatch === null) return appMatch.documentOnly ? "document" : "app";
138
+ if (appMatch === null) return pagesMatch.documentOnly ? "document" : "pages";
139
+ const owner = compareHybridRoutePatterns(patternFromParts(pagesMatch.patternParts), pagesMatch.isDynamic, patternFromParts(appMatch.patternParts), appMatch.isDynamic);
140
+ return (owner === "app" ? appMatch : pagesMatch).documentOnly ? "document" : owner;
141
+ }
142
+ //#endregion
143
+ export { resolveHybridClientRouteOwner };
@@ -0,0 +1,35 @@
1
+ //#region src/shims/internal/navigation-untracked.d.ts
2
+ /**
3
+ * Internal navigation-untracked pathname hook.
4
+ *
5
+ * Used by `unstable_catchError` error boundaries to avoid subscribing to
6
+ * pathname changes. This is NOT part of the public `next/navigation` API.
7
+ *
8
+ * Ported from Next.js:
9
+ * https://github.com/vercel/next.js/blob/v16.2.6/packages/next/src/client/components/navigation-untracked.ts
10
+ */
11
+ /**
12
+ * Returns the current pathname without registering it as a tracked render
13
+ * dependency. Unlike `usePathname()`, this does not use `useSyncExternalStore`
14
+ * and therefore does not cause the component to re-render on navigation.
15
+ *
16
+ * Server: returns the pathname from context, or `"/"` when no navigation context
17
+ * is available (the client will hydrate with the real value). The `"/"` fallback
18
+ * deliberately matches vinext's `usePathname()` behavior rather than Next.js's
19
+ * null context default. Returns `null` only when the render is a missing-params
20
+ * shell — vinext does not yet implement fallback-route-param detection, so this
21
+ * path is not currently reachable.
22
+ *
23
+ * Client: prefers the render snapshot **only during an active navigation**
24
+ * transition (`navigationSnapshotActiveCount > 0`) so the hook returns the
25
+ * pending URL, not the stale committed one. After commit, falls back to the
26
+ * cached pathname so user `pushState`/`replaceState` calls are immediately
27
+ * reflected.
28
+ *
29
+ * Used by `unstable_catchError` error boundaries to avoid unnecessary re-renders.
30
+ *
31
+ * @internal
32
+ */
33
+ declare function useUntrackedPathname(): string | null;
34
+ //#endregion
35
+ export { useUntrackedPathname };
@@ -0,0 +1,55 @@
1
+ import { getPagesNavigationContext } from "./pages-router-accessor.js";
2
+ import { getClientNavigationState, getNavigationContext, useClientNavigationRenderSnapshot } from "../navigation.js";
3
+ //#region src/shims/internal/navigation-untracked.ts
4
+ /**
5
+ * Internal navigation-untracked pathname hook.
6
+ *
7
+ * Used by `unstable_catchError` error boundaries to avoid subscribing to
8
+ * pathname changes. This is NOT part of the public `next/navigation` API.
9
+ *
10
+ * Ported from Next.js:
11
+ * https://github.com/vercel/next.js/blob/v16.2.6/packages/next/src/client/components/navigation-untracked.ts
12
+ */
13
+ const isServer = typeof window === "undefined";
14
+ function getPathnameSnapshot() {
15
+ const pagesCtx = getPagesNavigationContext();
16
+ if (pagesCtx) return pagesCtx.pathname;
17
+ return getClientNavigationState()?.cachedPathname ?? "/";
18
+ }
19
+ /**
20
+ * Returns the current pathname without registering it as a tracked render
21
+ * dependency. Unlike `usePathname()`, this does not use `useSyncExternalStore`
22
+ * and therefore does not cause the component to re-render on navigation.
23
+ *
24
+ * Server: returns the pathname from context, or `"/"` when no navigation context
25
+ * is available (the client will hydrate with the real value). The `"/"` fallback
26
+ * deliberately matches vinext's `usePathname()` behavior rather than Next.js's
27
+ * null context default. Returns `null` only when the render is a missing-params
28
+ * shell — vinext does not yet implement fallback-route-param detection, so this
29
+ * path is not currently reachable.
30
+ *
31
+ * Client: prefers the render snapshot **only during an active navigation**
32
+ * transition (`navigationSnapshotActiveCount > 0`) so the hook returns the
33
+ * pending URL, not the stale committed one. After commit, falls back to the
34
+ * cached pathname so user `pushState`/`replaceState` calls are immediately
35
+ * reflected.
36
+ *
37
+ * Used by `unstable_catchError` error boundaries to avoid unnecessary re-renders.
38
+ *
39
+ * @internal
40
+ */
41
+ function useUntrackedPathname() {
42
+ if (isServer) {
43
+ const ctx = getNavigationContext();
44
+ if (ctx) return ctx.pathname;
45
+ const pagesCtx = getPagesNavigationContext();
46
+ return pagesCtx ? pagesCtx.pathname : "/";
47
+ }
48
+ const renderSnapshot = useClientNavigationRenderSnapshot();
49
+ if (renderSnapshot && (getClientNavigationState()?.navigationSnapshotActiveCount ?? 0) > 0) return renderSnapshot.pathname;
50
+ const pagesCtx = getPagesNavigationContext();
51
+ if (pagesCtx) return pagesCtx.pathname;
52
+ return getPathnameSnapshot();
53
+ }
54
+ //#endregion
55
+ export { useUntrackedPathname };
@@ -24,19 +24,18 @@
24
24
  * directly by anyone, which keeps subsequent clones legal even after one
25
25
  * caller has consumed its copy.
26
26
  *
27
- * - No `AbortSignal` is honored at the shared layer. Each `Router.push` cycle
28
- * has its own AbortController that supersedes prior navigations via
29
- * `_navigationId`; aborting the shared fetch on behalf of one caller would
30
- * destroy the dedup gain for every other concurrent caller. Cancellation is
31
- * handled by the caller's `assertStillCurrent()` checkpoints after `await`,
32
- * not by abort propagation.
27
+ * - Each caller owns one waiter. Cancelling a waiter leaves the shared request
28
+ * alive while another waiter remains; cancelling the final waiter aborts the
29
+ * underlying fetch and evicts the entry immediately so a replacement caller
30
+ * can retry without joining a doomed request.
33
31
  *
34
32
  * - The map is module-scoped (one per realm). The Pages Router runs in the
35
33
  * browser only, so a single `Map` is sufficient.
36
34
  */
37
35
  /**
38
36
  * Dedupe a `fetch()` against the `_next/data` endpoint. Multiple concurrent
39
- * callers for the same `dataHref` share one underlying network request.
37
+ * callers for the same resolved URL and deployment ID share one underlying
38
+ * network request.
40
39
  *
41
40
  * Each call returns a freshly-cloned `Response` so consumers can read the
42
41
  * body independently. Once the in-flight Promise settles (resolve or reject)
@@ -1,3 +1,4 @@
1
+ import "../../utils/deployment-id.js";
1
2
  //#region src/shims/internal/pages-data-fetch-dedup.ts
2
3
  /**
3
4
  * In-flight request dedup for the Pages Router `/_next/data/<id>/<page>.json`
@@ -24,21 +25,57 @@
24
25
  * directly by anyone, which keeps subsequent clones legal even after one
25
26
  * caller has consumed its copy.
26
27
  *
27
- * - No `AbortSignal` is honored at the shared layer. Each `Router.push` cycle
28
- * has its own AbortController that supersedes prior navigations via
29
- * `_navigationId`; aborting the shared fetch on behalf of one caller would
30
- * destroy the dedup gain for every other concurrent caller. Cancellation is
31
- * handled by the caller's `assertStillCurrent()` checkpoints after `await`,
32
- * not by abort propagation.
28
+ * - Each caller owns one waiter. Cancelling a waiter leaves the shared request
29
+ * alive while another waiter remains; cancelling the final waiter aborts the
30
+ * underlying fetch and evicts the entry immediately so a replacement caller
31
+ * can retry without joining a doomed request.
33
32
  *
34
33
  * - The map is module-scoped (one per realm). The Pages Router runs in the
35
34
  * browser only, so a single `Map` is sufficient.
36
35
  */
37
- /** Inflight fetch promises keyed by the resolved data URL. */
36
+ /** Inflight fetch entries keyed by the resolved data request identity. */
38
37
  const inflight = /* @__PURE__ */ new Map();
38
+ function getInflightKey(dataHref, init) {
39
+ let resolvedHref = dataHref;
40
+ if (typeof window !== "undefined") try {
41
+ resolvedHref = new URL(dataHref, window.location.href).href;
42
+ } catch {}
43
+ const deploymentId = new Headers(init?.headers).get("x-deployment-id") ?? "";
44
+ return `${resolvedHref}\n${deploymentId}`;
45
+ }
46
+ function cloneSharedResponse(key, entry, signal) {
47
+ entry.waiters += 1;
48
+ return new Promise((resolve, reject) => {
49
+ let released = false;
50
+ const release = (cancelled) => {
51
+ if (released) return;
52
+ released = true;
53
+ entry.waiters -= 1;
54
+ if (cancelled && entry.waiters === 0 && !entry.settled) {
55
+ if (inflight.get(key) === entry) inflight.delete(key);
56
+ entry.controller.abort();
57
+ }
58
+ };
59
+ const abort = () => {
60
+ release(true);
61
+ reject(new DOMException("Aborted", "AbortError"));
62
+ };
63
+ signal?.addEventListener("abort", abort, { once: true });
64
+ entry.promise.then((response) => {
65
+ signal?.removeEventListener("abort", abort);
66
+ release(false);
67
+ resolve(response.clone());
68
+ }, (error) => {
69
+ signal?.removeEventListener("abort", abort);
70
+ release(false);
71
+ reject(error);
72
+ });
73
+ });
74
+ }
39
75
  /**
40
76
  * Dedupe a `fetch()` against the `_next/data` endpoint. Multiple concurrent
41
- * callers for the same `dataHref` share one underlying network request.
77
+ * callers for the same resolved URL and deployment ID share one underlying
78
+ * network request.
42
79
  *
43
80
  * Each call returns a freshly-cloned `Response` so consumers can read the
44
81
  * body independently. Once the in-flight Promise settles (resolve or reject)
@@ -48,20 +85,36 @@ const inflight = /* @__PURE__ */ new Map();
48
85
  * dropped on failure so the next navigation can retry.
49
86
  */
50
87
  function dedupedPagesDataFetch(dataHref, init) {
51
- let entry = inflight.get(dataHref);
88
+ const key = getInflightKey(dataHref, init);
89
+ const signal = init?.signal ?? void 0;
90
+ if (signal?.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
91
+ let entry = inflight.get(key);
52
92
  if (!entry) {
53
- entry = fetch(dataHref, init).finally(() => {
54
- if (inflight.get(dataHref) === entry) inflight.delete(dataHref);
55
- });
56
- inflight.set(dataHref, entry);
93
+ const controller = new AbortController();
94
+ let currentEntry;
95
+ currentEntry = {
96
+ controller,
97
+ promise: fetch(dataHref, {
98
+ ...init,
99
+ signal: controller.signal
100
+ }).finally(() => {
101
+ currentEntry.settled = true;
102
+ if (inflight.get(key) === currentEntry) inflight.delete(key);
103
+ }),
104
+ settled: false,
105
+ waiters: 0
106
+ };
107
+ inflight.set(key, currentEntry);
108
+ entry = currentEntry;
57
109
  }
58
- return entry.then((res) => res.clone());
110
+ return cloneSharedResponse(key, entry, signal);
59
111
  }
60
112
  /**
61
113
  * Drop every cached in-flight entry. Intended for tests; production code
62
114
  * does not need to call this because entries self-evict on settle.
63
115
  */
64
116
  function clearPagesDataInflight() {
117
+ for (const entry of inflight.values()) entry.controller.abort();
65
118
  inflight.clear();
66
119
  }
67
120
  //#endregion
@@ -43,10 +43,15 @@ type PagesDataTarget = {
43
43
  */
44
44
  declare function resolvePagesDataNavigationTarget(browserUrl: string, basePath: string): PagesDataTarget | null;
45
45
  /**
46
- * Inject a `<link rel="prefetch">` for the data JSON and kick off the
47
- * code-split loader so the chunk is warm by the time the user clicks.
46
+ * Prefetch the data JSON and kick off the code-split loader so the chunk is
47
+ * warm by the time the user clicks.
48
48
  *
49
49
  * Used by both `Router.prefetch()` and `<Link>` hover/viewport prefetch. The
50
+ * JSON request uses `fetch()` rather than `<link rel="prefetch">` so it can
51
+ * carry Next.js's `x-deployment-id` skew-protection header. The in-flight
52
+ * request is shared with a racing navigation; after it settles, the browser's
53
+ * normal HTTP cache remains responsible for reuse.
54
+ *
50
55
  * loader's returned Promise is intentionally discarded — `import()` caches the
51
56
  * result, so a subsequent navigation re-invocation hits the cache without
52
57
  * paying for a second round trip. Errors are swallowed: prefetch is
@@ -1,6 +1,8 @@
1
1
  import { stripBasePath } from "../../utils/base-path.js";
2
2
  import { getLocalePathPrefix } from "../../utils/domain-locale.js";
3
3
  import { buildPagesDataHref, matchPagesPattern } from "./pages-data-url.js";
4
+ import { NEXT_DEPLOYMENT_ID_HEADER, getDeploymentId } from "../../utils/deployment-id.js";
5
+ import { dedupedPagesDataFetch } from "./pages-data-fetch-dedup.js";
4
6
  //#region src/shims/internal/pages-data-target.ts
5
7
  /**
6
8
  * Shared decision helper for the Pages Router `/_next/data/<id>/<page>.json`
@@ -66,10 +68,15 @@ function resolvePagesDataNavigationTarget(browserUrl, basePath) {
66
68
  };
67
69
  }
68
70
  /**
69
- * Inject a `<link rel="prefetch">` for the data JSON and kick off the
70
- * code-split loader so the chunk is warm by the time the user clicks.
71
+ * Prefetch the data JSON and kick off the code-split loader so the chunk is
72
+ * warm by the time the user clicks.
71
73
  *
72
74
  * Used by both `Router.prefetch()` and `<Link>` hover/viewport prefetch. The
75
+ * JSON request uses `fetch()` rather than `<link rel="prefetch">` so it can
76
+ * carry Next.js's `x-deployment-id` skew-protection header. The in-flight
77
+ * request is shared with a racing navigation; after it settles, the browser's
78
+ * normal HTTP cache remains responsible for reuse.
79
+ *
73
80
  * loader's returned Promise is intentionally discarded — `import()` caches the
74
81
  * result, so a subsequent navigation re-invocation hits the cache without
75
82
  * paying for a second round trip. Errors are swallowed: prefetch is
@@ -77,12 +84,14 @@ function resolvePagesDataNavigationTarget(browserUrl, basePath) {
77
84
  */
78
85
  function prefetchPagesData(target) {
79
86
  if (typeof document === "undefined") return;
80
- const link = document.createElement("link");
81
- link.rel = "prefetch";
82
- link.as = "fetch";
83
- link.crossOrigin = "anonymous";
84
- link.href = target.dataHref;
85
- document.head.appendChild(link);
87
+ const headers = {
88
+ Accept: "application/json",
89
+ purpose: "prefetch",
90
+ "x-nextjs-data": "1"
91
+ };
92
+ const deploymentId = getDeploymentId();
93
+ if (deploymentId) headers[NEXT_DEPLOYMENT_ID_HEADER] = deploymentId;
94
+ dedupedPagesDataFetch(target.dataHref, { headers }).catch(() => {});
86
95
  target.loader().catch(() => {});
87
96
  }
88
97
  //#endregion
@@ -0,0 +1,19 @@
1
+ //#region src/shims/internal/pages-router-accessor.d.ts
2
+ /**
3
+ * Shared Pages Router navigation accessor.
4
+ *
5
+ * Both `next/navigation` (usePathname/useParams/useSearchParams) and the
6
+ * internal `useUntrackedPathname` read from the same global Symbol. Keeping
7
+ * the lookup in one place avoids the Symbol string and the error-handling
8
+ * shape from drifting across modules.
9
+ *
10
+ * @internal
11
+ */
12
+ type PagesNavigationContext = {
13
+ pathname: string | null;
14
+ searchParams: URLSearchParams;
15
+ params: Record<string, string | string[]> | null;
16
+ };
17
+ declare function getPagesNavigationContext(): PagesNavigationContext | null;
18
+ //#endregion
19
+ export { PagesNavigationContext, getPagesNavigationContext };
@@ -0,0 +1,13 @@
1
+ //#region src/shims/internal/pages-router-accessor.ts
2
+ const PAGES_NAVIGATION_ACCESSOR_KEY = Symbol.for("vinext.navigation.pagesNavigationContextAccessor");
3
+ function getPagesNavigationContext() {
4
+ const accessor = globalThis[PAGES_NAVIGATION_ACCESSOR_KEY];
5
+ if (!accessor) return null;
6
+ try {
7
+ return accessor();
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+ //#endregion
13
+ export { getPagesNavigationContext };
@@ -1,6 +1,7 @@
1
1
  import { NextRouter } from "../router.js";
2
+ import { Context } from "react";
2
3
 
3
4
  //#region src/shims/internal/router-context.d.ts
4
- declare const RouterContext: import("react").Context<NextRouter | null>;
5
+ declare const RouterContext: Context<NextRouter | null>;
5
6
  //#endregion
6
7
  export { RouterContext };
@@ -6,6 +6,8 @@ import { createContext } from "react";
6
6
  * Used by: some testing utilities and older libraries.
7
7
  * Provides the Pages Router context.
8
8
  */
9
- const RouterContext = createContext(null);
9
+ const ROUTER_CONTEXT_KEY = Symbol.for("vinext.routerContext");
10
+ const globalState = globalThis;
11
+ const RouterContext = globalState[ROUTER_CONTEXT_KEY] ?? (globalState[ROUTER_CONTEXT_KEY] = createContext(null));
10
12
  //#endregion
11
13
  export { RouterContext };