vinext 0.0.53 → 0.0.54
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.
- package/dist/build/inline-css.d.ts +7 -0
- package/dist/build/inline-css.js +50 -0
- package/dist/build/inline-css.js.map +1 -0
- package/dist/build/prerender.js +2 -1
- package/dist/build/prerender.js.map +1 -1
- package/dist/check.js +4 -0
- package/dist/check.js.map +1 -1
- package/dist/client/navigation-runtime.d.ts +2 -1
- package/dist/client/navigation-runtime.js.map +1 -1
- package/dist/client/window-next.d.ts +7 -0
- package/dist/client/window-next.js.map +1 -1
- package/dist/config/next-config.d.ts +83 -1
- package/dist/config/next-config.js +131 -2
- package/dist/config/next-config.js.map +1 -1
- package/dist/deploy.js +13 -0
- package/dist/deploy.js.map +1 -1
- package/dist/entries/app-browser-entry.d.ts +11 -1
- package/dist/entries/app-browser-entry.js +16 -6
- package/dist/entries/app-browser-entry.js.map +1 -1
- package/dist/entries/app-rsc-entry.d.ts +8 -1
- package/dist/entries/app-rsc-entry.js +18 -5
- package/dist/entries/app-rsc-entry.js.map +1 -1
- package/dist/entries/app-rsc-manifest.d.ts +21 -1
- package/dist/entries/app-rsc-manifest.js +6 -4
- package/dist/entries/app-rsc-manifest.js.map +1 -1
- package/dist/entries/pages-client-entry.d.ts +4 -1
- package/dist/entries/pages-client-entry.js +18 -2
- package/dist/entries/pages-client-entry.js.map +1 -1
- package/dist/entries/pages-server-entry.js +82 -4
- package/dist/entries/pages-server-entry.js.map +1 -1
- package/dist/entries/runtime-entry-module.d.ts +1 -10
- package/dist/entries/runtime-entry-module.js +2 -12
- package/dist/entries/runtime-entry-module.js.map +1 -1
- package/dist/index.js +63 -5
- package/dist/index.js.map +1 -1
- package/dist/plugins/remove-console.d.ts +16 -0
- package/dist/plugins/remove-console.js +176 -0
- package/dist/plugins/remove-console.js.map +1 -0
- package/dist/routing/app-route-graph.d.ts +24 -1
- package/dist/routing/app-route-graph.js +52 -4
- package/dist/routing/app-route-graph.js.map +1 -1
- package/dist/routing/app-router.d.ts +2 -2
- package/dist/routing/app-router.js +2 -2
- package/dist/routing/app-router.js.map +1 -1
- package/dist/routing/file-matcher.d.ts +21 -1
- package/dist/routing/file-matcher.js +39 -1
- package/dist/routing/file-matcher.js.map +1 -1
- package/dist/routing/pages-router.d.ts +1 -1
- package/dist/routing/pages-router.js +10 -3
- package/dist/routing/pages-router.js.map +1 -1
- package/dist/server/api-handler.js +1 -1
- package/dist/server/app-browser-entry.js +25 -16
- package/dist/server/app-browser-entry.js.map +1 -1
- package/dist/server/app-browser-navigation-controller.d.ts +2 -0
- package/dist/server/app-browser-navigation-controller.js +4 -0
- package/dist/server/app-browser-navigation-controller.js.map +1 -1
- package/dist/server/app-elements-wire.d.ts +13 -4
- package/dist/server/app-elements-wire.js +10 -1
- package/dist/server/app-elements-wire.js.map +1 -1
- package/dist/server/app-elements.d.ts +2 -2
- package/dist/server/app-elements.js +2 -2
- package/dist/server/app-elements.js.map +1 -1
- package/dist/server/app-fallback-renderer.d.ts +15 -5
- package/dist/server/app-fallback-renderer.js +10 -4
- package/dist/server/app-fallback-renderer.js.map +1 -1
- package/dist/server/app-inline-css-client.d.ts +7 -0
- package/dist/server/app-inline-css-client.js +37 -0
- package/dist/server/app-inline-css-client.js.map +1 -0
- package/dist/server/app-page-boundary.d.ts +21 -1
- package/dist/server/app-page-boundary.js +28 -3
- package/dist/server/app-page-boundary.js.map +1 -1
- package/dist/server/app-page-cache.d.ts +7 -3
- package/dist/server/app-page-cache.js +7 -7
- package/dist/server/app-page-cache.js.map +1 -1
- package/dist/server/app-page-dispatch.d.ts +10 -1
- package/dist/server/app-page-dispatch.js +126 -79
- package/dist/server/app-page-dispatch.js.map +1 -1
- package/dist/server/app-page-element-builder.js +12 -28
- package/dist/server/app-page-element-builder.js.map +1 -1
- package/dist/server/app-page-render-identity.d.ts +22 -0
- package/dist/server/app-page-render-identity.js +42 -0
- package/dist/server/app-page-render-identity.js.map +1 -0
- package/dist/server/app-page-render.d.ts +8 -1
- package/dist/server/app-page-render.js +4 -1
- package/dist/server/app-page-render.js.map +1 -1
- package/dist/server/app-page-request.d.ts +6 -3
- package/dist/server/app-page-request.js +5 -2
- package/dist/server/app-page-request.js.map +1 -1
- package/dist/server/app-page-response.js +2 -2
- package/dist/server/app-page-response.js.map +1 -1
- package/dist/server/app-page-route-wiring.d.ts +15 -0
- package/dist/server/app-page-route-wiring.js +7 -5
- package/dist/server/app-page-route-wiring.js.map +1 -1
- package/dist/server/app-page-stream.d.ts +11 -0
- package/dist/server/app-page-stream.js +1 -0
- package/dist/server/app-page-stream.js.map +1 -1
- package/dist/server/app-route-handler-response.js +37 -5
- package/dist/server/app-route-handler-response.js.map +1 -1
- package/dist/server/app-rsc-handler.d.ts +14 -3
- package/dist/server/app-rsc-handler.js +45 -5
- package/dist/server/app-rsc-handler.js.map +1 -1
- package/dist/server/app-rsc-request-normalization.d.ts +2 -1
- package/dist/server/app-rsc-request-normalization.js +3 -2
- package/dist/server/app-rsc-request-normalization.js.map +1 -1
- package/dist/server/app-server-action-execution.d.ts +21 -3
- package/dist/server/app-server-action-execution.js +42 -7
- package/dist/server/app-server-action-execution.js.map +1 -1
- package/dist/server/app-ssr-entry.d.ts +6 -0
- package/dist/server/app-ssr-entry.js +22 -7
- package/dist/server/app-ssr-entry.js.map +1 -1
- package/dist/server/app-ssr-error-meta.js +3 -3
- package/dist/server/app-ssr-error-meta.js.map +1 -1
- package/dist/server/app-ssr-stream.d.ts +2 -1
- package/dist/server/app-ssr-stream.js +176 -31
- package/dist/server/app-ssr-stream.js.map +1 -1
- package/dist/server/client-trace-metadata.d.ts +31 -0
- package/dist/server/client-trace-metadata.js +83 -0
- package/dist/server/client-trace-metadata.js.map +1 -0
- package/dist/server/cookie-utils.d.ts +13 -0
- package/dist/server/cookie-utils.js +20 -0
- package/dist/server/cookie-utils.js.map +1 -0
- package/dist/server/dev-server.d.ts +8 -1
- package/dist/server/dev-server.js +34 -5
- package/dist/server/dev-server.js.map +1 -1
- package/dist/server/html.d.ts +2 -1
- package/dist/server/html.js +6 -1
- package/dist/server/html.js.map +1 -1
- package/dist/server/isr-cache.d.ts +7 -5
- package/dist/server/isr-cache.js +17 -6
- package/dist/server/isr-cache.js.map +1 -1
- package/dist/server/middleware-runtime.js +1 -2
- package/dist/server/middleware-runtime.js.map +1 -1
- package/dist/server/pages-document-initial-props.d.ts +7 -0
- package/dist/server/pages-document-initial-props.js +14 -0
- package/dist/server/pages-document-initial-props.js.map +1 -0
- package/dist/server/pages-page-data.js +3 -0
- package/dist/server/pages-page-data.js.map +1 -1
- package/dist/server/pages-page-method.d.ts +48 -0
- package/dist/server/pages-page-method.js +19 -0
- package/dist/server/pages-page-method.js.map +1 -0
- package/dist/server/pages-page-response.d.ts +6 -0
- package/dist/server/pages-page-response.js +10 -3
- package/dist/server/pages-page-response.js.map +1 -1
- package/dist/server/pages-serializable-props.d.ts +25 -0
- package/dist/server/pages-serializable-props.js +69 -0
- package/dist/server/pages-serializable-props.js.map +1 -0
- package/dist/server/prod-server.js +3 -0
- package/dist/server/prod-server.js.map +1 -1
- package/dist/server/server-action-not-found.js +3 -2
- package/dist/server/server-action-not-found.js.map +1 -1
- package/dist/server/static-file-cache.js +2 -1
- package/dist/server/static-file-cache.js.map +1 -1
- package/dist/shims/app-router-scroll-state.d.ts +4 -2
- package/dist/shims/app-router-scroll-state.js +16 -3
- package/dist/shims/app-router-scroll-state.js.map +1 -1
- package/dist/shims/app-router-scroll.d.ts +16 -2
- package/dist/shims/app-router-scroll.js +18 -3
- package/dist/shims/app-router-scroll.js.map +1 -1
- package/dist/shims/cache.d.ts +6 -0
- package/dist/shims/cache.js +7 -0
- package/dist/shims/cache.js.map +1 -1
- package/dist/shims/error.js +3 -0
- package/dist/shims/error.js.map +1 -1
- package/dist/shims/headers.d.ts +7 -0
- package/dist/shims/headers.js +9 -1
- package/dist/shims/headers.js.map +1 -1
- package/dist/shims/internal/app-route-detection.d.ts +37 -0
- package/dist/shims/internal/app-route-detection.js +69 -0
- package/dist/shims/internal/app-route-detection.js.map +1 -0
- package/dist/shims/link.d.ts +18 -2
- package/dist/shims/link.js +70 -6
- package/dist/shims/link.js.map +1 -1
- package/dist/shims/metadata.d.ts +7 -6
- package/dist/shims/metadata.js +9 -5
- package/dist/shims/metadata.js.map +1 -1
- package/dist/shims/navigation.d.ts +1 -2
- package/dist/shims/navigation.js +63 -12
- package/dist/shims/navigation.js.map +1 -1
- package/dist/shims/router.d.ts +5 -0
- package/dist/shims/router.js +14 -4
- package/dist/shims/router.js.map +1 -1
- package/dist/shims/script.d.ts +11 -1
- package/dist/shims/script.js +75 -6
- package/dist/shims/script.js.map +1 -1
- package/dist/utils/path.d.ts +13 -0
- package/dist/utils/path.js +16 -0
- package/dist/utils/path.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-router.js","names":[],"sources":["../../src/routing/app-router.ts"],"sourcesContent":["/**\n * App Router file-system routing.\n *\n * Scans the app/ directory following Next.js App Router conventions:\n * - app/page.tsx -> /\n * - app/about/page.tsx -> /about\n * - app/blog/[slug]/page.tsx -> /blog/:slug\n * - app/[...catchAll]/page.tsx -> /:catchAll+\n * - app/route.ts -> / (API route)\n * - app/(group)/page.tsx -> / (route groups are transparent)\n * - Layouts: app/layout.tsx wraps all children\n * - Loading: app/loading.tsx -> Suspense fallback\n * - Error: app/error.tsx -> ErrorBoundary\n * - Not Found: app/not-found.tsx\n */\nimport { createValidFileMatcher, type ValidFileMatcher } from \"./file-matcher.js\";\nimport { createRouteTrieCache, matchRouteWithTrie } from \"./route-matching.js\";\nimport {\n buildAppRouteGraph,\n type AppRoute,\n type AppRouteGraphRoute,\n type RouteManifest,\n} from \"./app-route-graph.js\";\nexport type { AppRoute } from \"./app-route-graph.js\";\nexport {
|
|
1
|
+
{"version":3,"file":"app-router.js","names":[],"sources":["../../src/routing/app-router.ts"],"sourcesContent":["/**\n * App Router file-system routing.\n *\n * Scans the app/ directory following Next.js App Router conventions:\n * - app/page.tsx -> /\n * - app/about/page.tsx -> /about\n * - app/blog/[slug]/page.tsx -> /blog/:slug\n * - app/[...catchAll]/page.tsx -> /:catchAll+\n * - app/route.ts -> / (API route)\n * - app/(group)/page.tsx -> / (route groups are transparent)\n * - Layouts: app/layout.tsx wraps all children\n * - Loading: app/loading.tsx -> Suspense fallback\n * - Error: app/error.tsx -> ErrorBoundary\n * - Not Found: app/not-found.tsx\n */\nimport { createValidFileMatcher, type ValidFileMatcher } from \"./file-matcher.js\";\nimport { createRouteTrieCache, matchRouteWithTrie } from \"./route-matching.js\";\nimport {\n buildAppRouteGraph,\n type AppRoute,\n type AppRouteGraphRoute,\n type RouteManifest,\n} from \"./app-route-graph.js\";\nexport type { AppRoute } from \"./app-route-graph.js\";\nexport {\n computeAppRouteStaticSiblings,\n computeRootParamNames,\n convertSegmentsToRouteParts,\n} from \"./app-route-graph.js\";\n\ntype AppRouteGraph = {\n routes: AppRouteGraphRoute[];\n routeManifest: RouteManifest;\n};\n\n// Cache for app routes\nlet cachedGraph: AppRouteGraph | null = null;\nlet cachedAppDir: string | null = null;\nlet cachedPageExtensionsKey: string | null = null;\n\nexport function invalidateAppRouteCache(): void {\n cachedGraph = null;\n cachedAppDir = null;\n cachedPageExtensionsKey = null;\n}\n\n/**\n * Scan the app/ directory and return the route graph.\n * TODO(#726): Layer 4 should consume this read model directly once the\n * navigation planner owns route graph facts.\n *\n * @internal\n */\nexport async function appRouteGraph(\n appDir: string,\n pageExtensions?: readonly string[],\n matcher?: ValidFileMatcher,\n): Promise<AppRouteGraph> {\n matcher ??= createValidFileMatcher(pageExtensions);\n const pageExtensionsKey = JSON.stringify(matcher.extensions);\n if (cachedGraph && cachedAppDir === appDir && cachedPageExtensionsKey === pageExtensionsKey) {\n return cachedGraph;\n }\n\n const graph = await buildAppRouteGraph(appDir, matcher);\n cachedGraph = graph;\n cachedAppDir = appDir;\n cachedPageExtensionsKey = pageExtensionsKey;\n return graph;\n}\n\n/**\n * Scan the app/ directory and return a list of routes.\n */\nexport async function appRouter(\n appDir: string,\n pageExtensions?: readonly string[],\n matcher?: ValidFileMatcher,\n): Promise<AppRouteGraphRoute[]> {\n const graph = await appRouteGraph(appDir, pageExtensions, matcher);\n return graph.routes;\n}\n\n// Trie cache — keyed by route array identity (same array = same trie)\nconst appTrieCache = createRouteTrieCache<AppRoute>();\n\n/**\n * Match a URL against App Router routes.\n */\nexport function matchAppRoute(\n url: string,\n routes: AppRoute[],\n): { route: AppRoute; params: Record<string, string | string[]> } | null {\n return matchRouteWithTrie(url, routes, appTrieCache);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAoCA,IAAI,cAAoC;AACxC,IAAI,eAA8B;AAClC,IAAI,0BAAyC;AAE7C,SAAgB,0BAAgC;CAC9C,cAAc;CACd,eAAe;CACf,0BAA0B;;;;;;;;;AAU5B,eAAsB,cACpB,QACA,gBACA,SACwB;CACxB,YAAY,uBAAuB,eAAe;CAClD,MAAM,oBAAoB,KAAK,UAAU,QAAQ,WAAW;CAC5D,IAAI,eAAe,iBAAiB,UAAU,4BAA4B,mBACxE,OAAO;CAGT,MAAM,QAAQ,MAAM,mBAAmB,QAAQ,QAAQ;CACvD,cAAc;CACd,eAAe;CACf,0BAA0B;CAC1B,OAAO;;;;;AAMT,eAAsB,UACpB,QACA,gBACA,SAC+B;CAE/B,QAAO,MADa,cAAc,QAAQ,gBAAgB,QAAQ,EACrD;;AAIf,MAAM,eAAe,sBAAgC;;;;AAKrD,SAAgB,cACd,KACA,QACuE;CACvE,OAAO,mBAAmB,KAAK,QAAQ,aAAa"}
|
|
@@ -18,10 +18,30 @@ type ValidFileMatcher = {
|
|
|
18
18
|
declare function createValidFileMatcher(pageExtensions?: readonly string[] | null): ValidFileMatcher;
|
|
19
19
|
/** Check if a file exists with any configured page extension. */
|
|
20
20
|
declare function findFileWithExtensions(basePath: string, matcher: ValidFileMatcher): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Vite's default `resolve.extensions` covers `.tsx/.ts/.jsx/.js/.json` (and
|
|
23
|
+
* `.mjs/.mts`). When the user configures `pageExtensions` with values Vite
|
|
24
|
+
* does not know about — e.g. `["platform.tsx", "tsx", "mdx"]` from the
|
|
25
|
+
* Next.js `resolve-extensions` fixture — extensionless imports of those
|
|
26
|
+
* files fail to resolve, and the build crashes with "Custom deploy script
|
|
27
|
+
* failed: undefined (1)".
|
|
28
|
+
*
|
|
29
|
+
* Build the merged extension list that Vite should use:
|
|
30
|
+
*
|
|
31
|
+
* 1. User-configured pageExtensions go first (each prefixed with `.`) so
|
|
32
|
+
* the user's priority wins. e.g. `.platform.tsx` resolves before `.tsx`.
|
|
33
|
+
* 2. Vite's defaults follow, with duplicates removed.
|
|
34
|
+
*
|
|
35
|
+
* The user's pageExtensions retain their relative order, which is what
|
|
36
|
+
* Next.js / Turbopack do via the `resolveExtensions` config option.
|
|
37
|
+
*
|
|
38
|
+
* See: cloudflare/vinext#1502
|
|
39
|
+
*/
|
|
40
|
+
declare function buildViteResolveExtensions(pageExtensions?: readonly string[] | null, viteDefaults?: readonly string[]): string[];
|
|
21
41
|
/**
|
|
22
42
|
* Use function-form exclude for Node < 22.14 compatibility.
|
|
23
43
|
*/
|
|
24
44
|
declare function scanWithExtensions(stem: string, cwd: string, extensions: readonly string[], exclude?: (name: string) => boolean): AsyncGenerator<string>;
|
|
25
45
|
//#endregion
|
|
26
|
-
export { ValidFileMatcher, createValidFileMatcher, findFileWithExtensions, normalizePageExtensions, scanWithExtensions };
|
|
46
|
+
export { ValidFileMatcher, buildViteResolveExtensions, createValidFileMatcher, findFileWithExtensions, normalizePageExtensions, scanWithExtensions };
|
|
27
47
|
//# sourceMappingURL=file-matcher.d.ts.map
|
|
@@ -65,6 +65,44 @@ function findFileWithExtensions(basePath, matcher) {
|
|
|
65
65
|
return matcher.dottedExtensions.some((ext) => existsSync(basePath + ext));
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
|
+
* Vite's default `resolve.extensions` covers `.tsx/.ts/.jsx/.js/.json` (and
|
|
69
|
+
* `.mjs/.mts`). When the user configures `pageExtensions` with values Vite
|
|
70
|
+
* does not know about — e.g. `["platform.tsx", "tsx", "mdx"]` from the
|
|
71
|
+
* Next.js `resolve-extensions` fixture — extensionless imports of those
|
|
72
|
+
* files fail to resolve, and the build crashes with "Custom deploy script
|
|
73
|
+
* failed: undefined (1)".
|
|
74
|
+
*
|
|
75
|
+
* Build the merged extension list that Vite should use:
|
|
76
|
+
*
|
|
77
|
+
* 1. User-configured pageExtensions go first (each prefixed with `.`) so
|
|
78
|
+
* the user's priority wins. e.g. `.platform.tsx` resolves before `.tsx`.
|
|
79
|
+
* 2. Vite's defaults follow, with duplicates removed.
|
|
80
|
+
*
|
|
81
|
+
* The user's pageExtensions retain their relative order, which is what
|
|
82
|
+
* Next.js / Turbopack do via the `resolveExtensions` config option.
|
|
83
|
+
*
|
|
84
|
+
* See: cloudflare/vinext#1502
|
|
85
|
+
*/
|
|
86
|
+
function buildViteResolveExtensions(pageExtensions, viteDefaults = [
|
|
87
|
+
".mjs",
|
|
88
|
+
".js",
|
|
89
|
+
".mts",
|
|
90
|
+
".ts",
|
|
91
|
+
".jsx",
|
|
92
|
+
".tsx",
|
|
93
|
+
".json"
|
|
94
|
+
]) {
|
|
95
|
+
const dotted = normalizePageExtensions(pageExtensions).map((ext) => `.${ext}`);
|
|
96
|
+
const seen = /* @__PURE__ */ new Set();
|
|
97
|
+
const result = [];
|
|
98
|
+
for (const ext of [...dotted, ...viteDefaults]) {
|
|
99
|
+
if (seen.has(ext)) continue;
|
|
100
|
+
seen.add(ext);
|
|
101
|
+
result.push(ext);
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
68
106
|
* Use function-form exclude for Node < 22.14 compatibility.
|
|
69
107
|
*/
|
|
70
108
|
async function* scanWithExtensions(stem, cwd, extensions, exclude) {
|
|
@@ -75,6 +113,6 @@ async function* scanWithExtensions(stem, cwd, extensions, exclude) {
|
|
|
75
113
|
})) yield file;
|
|
76
114
|
}
|
|
77
115
|
//#endregion
|
|
78
|
-
export { createValidFileMatcher, findFileWithExtensions, normalizePageExtensions, scanWithExtensions };
|
|
116
|
+
export { buildViteResolveExtensions, createValidFileMatcher, findFileWithExtensions, normalizePageExtensions, scanWithExtensions };
|
|
79
117
|
|
|
80
118
|
//# sourceMappingURL=file-matcher.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-matcher.js","names":[],"sources":["../../src/routing/file-matcher.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { glob } from \"node:fs/promises\";\n\nconst DEFAULT_PAGE_EXTENSIONS = [\"tsx\", \"ts\", \"jsx\", \"js\"] as const;\n\nfunction escapeRegex(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nexport function normalizePageExtensions(pageExtensions?: readonly string[] | null): string[] {\n if (!Array.isArray(pageExtensions) || pageExtensions.length === 0) {\n return [...DEFAULT_PAGE_EXTENSIONS];\n }\n\n const filtered = pageExtensions\n .filter((ext): ext is string => typeof ext === \"string\")\n .map((ext) => ext.trim().replace(/^\\.+/, \"\"))\n .filter((ext) => ext.length > 0);\n return filtered.length > 0 ? [...filtered] : [...DEFAULT_PAGE_EXTENSIONS];\n}\n\nfunction buildExtensionGlob(stem: string, extensions: readonly string[]): string {\n if (extensions.length === 1) {\n return `${stem}.${extensions[0]}`;\n }\n return `${stem}.{${extensions.join(\",\")}}`;\n}\n\nexport type ValidFileMatcher = {\n extensions: string[];\n dottedExtensions: string[];\n extensionRegex: RegExp;\n isPageFile(filePath: string): boolean;\n isAppRouterPage(filePath: string): boolean;\n isAppRouterRoute(filePath: string): boolean;\n isAppLayoutFile(filePath: string): boolean;\n isAppDefaultFile(filePath: string): boolean;\n stripExtension(filePath: string): string;\n};\n\n/**\n * Ported in spirit from Next.js createValidFileMatcher:\n * packages/next/src/server/lib/find-page-file.ts\n */\nexport function createValidFileMatcher(\n pageExtensions?: readonly string[] | null,\n): ValidFileMatcher {\n const extensions = normalizePageExtensions(pageExtensions);\n const dottedExtensions = extensions.map((ext) => `.${ext}`);\n const extPattern = `(?:${extensions.map((ext) => escapeRegex(ext)).join(\"|\")})`;\n\n const extensionRegex = new RegExp(`\\\\.${extPattern}$`);\n const createLeafPattern = (fileNames: readonly string[]): RegExp => {\n const names = fileNames.length === 1 ? fileNames[0] : `(${fileNames.join(\"|\")})`;\n return new RegExp(`(^${names}|[\\\\\\\\/]${names})\\\\.${extPattern}$`);\n };\n\n const appRouterPageRegex = createLeafPattern([\"page\", \"route\"]);\n const appRouterRouteRegex = createLeafPattern([\"route\"]);\n const appLayoutRegex = createLeafPattern([\"layout\"]);\n const appDefaultRegex = createLeafPattern([\"default\"]);\n\n return {\n extensions,\n dottedExtensions,\n extensionRegex,\n isPageFile(filePath: string) {\n return extensionRegex.test(filePath);\n },\n isAppRouterPage(filePath: string) {\n return appRouterPageRegex.test(filePath);\n },\n isAppRouterRoute(filePath: string) {\n return appRouterRouteRegex.test(filePath);\n },\n isAppLayoutFile(filePath: string) {\n return appLayoutRegex.test(filePath);\n },\n isAppDefaultFile(filePath: string) {\n return appDefaultRegex.test(filePath);\n },\n stripExtension(filePath: string) {\n return filePath.replace(extensionRegex, \"\");\n },\n };\n}\n\n/** Check if a file exists with any configured page extension. */\nexport function findFileWithExtensions(basePath: string, matcher: ValidFileMatcher): boolean {\n return matcher.dottedExtensions.some((ext) => existsSync(basePath + ext));\n}\n\n/**\n * Use function-form exclude for Node < 22.14 compatibility.\n */\nexport async function* scanWithExtensions(\n stem: string,\n cwd: string,\n extensions: readonly string[],\n exclude?: (name: string) => boolean,\n): AsyncGenerator<string> {\n const pattern = buildExtensionGlob(stem, extensions);\n for await (const file of glob(pattern, {\n cwd,\n ...(exclude ? { exclude } : {}),\n })) {\n yield file;\n }\n}\n"],"mappings":";;;AAGA,MAAM,0BAA0B;CAAC;CAAO;CAAM;CAAO;CAAK;AAE1D,SAAS,YAAY,OAAuB;CAC1C,OAAO,MAAM,QAAQ,uBAAuB,OAAO;;AAGrD,SAAgB,wBAAwB,gBAAqD;CAC3F,IAAI,CAAC,MAAM,QAAQ,eAAe,IAAI,eAAe,WAAW,GAC9D,OAAO,CAAC,GAAG,wBAAwB;CAGrC,MAAM,WAAW,eACd,QAAQ,QAAuB,OAAO,QAAQ,SAAS,CACvD,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,QAAQ,GAAG,CAAC,CAC5C,QAAQ,QAAQ,IAAI,SAAS,EAAE;CAClC,OAAO,SAAS,SAAS,IAAI,CAAC,GAAG,SAAS,GAAG,CAAC,GAAG,wBAAwB;;AAG3E,SAAS,mBAAmB,MAAc,YAAuC;CAC/E,IAAI,WAAW,WAAW,GACxB,OAAO,GAAG,KAAK,GAAG,WAAW;CAE/B,OAAO,GAAG,KAAK,IAAI,WAAW,KAAK,IAAI,CAAC;;;;;;AAmB1C,SAAgB,uBACd,gBACkB;CAClB,MAAM,aAAa,wBAAwB,eAAe;CAC1D,MAAM,mBAAmB,WAAW,KAAK,QAAQ,IAAI,MAAM;CAC3D,MAAM,aAAa,MAAM,WAAW,KAAK,QAAQ,YAAY,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC;CAE7E,MAAM,iBAAiB,IAAI,OAAO,MAAM,WAAW,GAAG;CACtD,MAAM,qBAAqB,cAAyC;EAClE,MAAM,QAAQ,UAAU,WAAW,IAAI,UAAU,KAAK,IAAI,UAAU,KAAK,IAAI,CAAC;EAC9E,OAAO,IAAI,OAAO,KAAK,MAAM,UAAU,MAAM,MAAM,WAAW,GAAG;;CAGnE,MAAM,qBAAqB,kBAAkB,CAAC,QAAQ,QAAQ,CAAC;CAC/D,MAAM,sBAAsB,kBAAkB,CAAC,QAAQ,CAAC;CACxD,MAAM,iBAAiB,kBAAkB,CAAC,SAAS,CAAC;CACpD,MAAM,kBAAkB,kBAAkB,CAAC,UAAU,CAAC;CAEtD,OAAO;EACL;EACA;EACA;EACA,WAAW,UAAkB;GAC3B,OAAO,eAAe,KAAK,SAAS;;EAEtC,gBAAgB,UAAkB;GAChC,OAAO,mBAAmB,KAAK,SAAS;;EAE1C,iBAAiB,UAAkB;GACjC,OAAO,oBAAoB,KAAK,SAAS;;EAE3C,gBAAgB,UAAkB;GAChC,OAAO,eAAe,KAAK,SAAS;;EAEtC,iBAAiB,UAAkB;GACjC,OAAO,gBAAgB,KAAK,SAAS;;EAEvC,eAAe,UAAkB;GAC/B,OAAO,SAAS,QAAQ,gBAAgB,GAAG;;EAE9C;;;AAIH,SAAgB,uBAAuB,UAAkB,SAAoC;CAC3F,OAAO,QAAQ,iBAAiB,MAAM,QAAQ,WAAW,WAAW,IAAI,CAAC;;;;;
|
|
1
|
+
{"version":3,"file":"file-matcher.js","names":[],"sources":["../../src/routing/file-matcher.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { glob } from \"node:fs/promises\";\n\nconst DEFAULT_PAGE_EXTENSIONS = [\"tsx\", \"ts\", \"jsx\", \"js\"] as const;\n\nfunction escapeRegex(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nexport function normalizePageExtensions(pageExtensions?: readonly string[] | null): string[] {\n if (!Array.isArray(pageExtensions) || pageExtensions.length === 0) {\n return [...DEFAULT_PAGE_EXTENSIONS];\n }\n\n const filtered = pageExtensions\n .filter((ext): ext is string => typeof ext === \"string\")\n .map((ext) => ext.trim().replace(/^\\.+/, \"\"))\n .filter((ext) => ext.length > 0);\n return filtered.length > 0 ? [...filtered] : [...DEFAULT_PAGE_EXTENSIONS];\n}\n\nfunction buildExtensionGlob(stem: string, extensions: readonly string[]): string {\n if (extensions.length === 1) {\n return `${stem}.${extensions[0]}`;\n }\n return `${stem}.{${extensions.join(\",\")}}`;\n}\n\nexport type ValidFileMatcher = {\n extensions: string[];\n dottedExtensions: string[];\n extensionRegex: RegExp;\n isPageFile(filePath: string): boolean;\n isAppRouterPage(filePath: string): boolean;\n isAppRouterRoute(filePath: string): boolean;\n isAppLayoutFile(filePath: string): boolean;\n isAppDefaultFile(filePath: string): boolean;\n stripExtension(filePath: string): string;\n};\n\n/**\n * Ported in spirit from Next.js createValidFileMatcher:\n * packages/next/src/server/lib/find-page-file.ts\n */\nexport function createValidFileMatcher(\n pageExtensions?: readonly string[] | null,\n): ValidFileMatcher {\n const extensions = normalizePageExtensions(pageExtensions);\n const dottedExtensions = extensions.map((ext) => `.${ext}`);\n const extPattern = `(?:${extensions.map((ext) => escapeRegex(ext)).join(\"|\")})`;\n\n const extensionRegex = new RegExp(`\\\\.${extPattern}$`);\n const createLeafPattern = (fileNames: readonly string[]): RegExp => {\n const names = fileNames.length === 1 ? fileNames[0] : `(${fileNames.join(\"|\")})`;\n return new RegExp(`(^${names}|[\\\\\\\\/]${names})\\\\.${extPattern}$`);\n };\n\n const appRouterPageRegex = createLeafPattern([\"page\", \"route\"]);\n const appRouterRouteRegex = createLeafPattern([\"route\"]);\n const appLayoutRegex = createLeafPattern([\"layout\"]);\n const appDefaultRegex = createLeafPattern([\"default\"]);\n\n return {\n extensions,\n dottedExtensions,\n extensionRegex,\n isPageFile(filePath: string) {\n return extensionRegex.test(filePath);\n },\n isAppRouterPage(filePath: string) {\n return appRouterPageRegex.test(filePath);\n },\n isAppRouterRoute(filePath: string) {\n return appRouterRouteRegex.test(filePath);\n },\n isAppLayoutFile(filePath: string) {\n return appLayoutRegex.test(filePath);\n },\n isAppDefaultFile(filePath: string) {\n return appDefaultRegex.test(filePath);\n },\n stripExtension(filePath: string) {\n return filePath.replace(extensionRegex, \"\");\n },\n };\n}\n\n/** Check if a file exists with any configured page extension. */\nexport function findFileWithExtensions(basePath: string, matcher: ValidFileMatcher): boolean {\n return matcher.dottedExtensions.some((ext) => existsSync(basePath + ext));\n}\n\n/**\n * Vite's default `resolve.extensions` covers `.tsx/.ts/.jsx/.js/.json` (and\n * `.mjs/.mts`). When the user configures `pageExtensions` with values Vite\n * does not know about — e.g. `[\"platform.tsx\", \"tsx\", \"mdx\"]` from the\n * Next.js `resolve-extensions` fixture — extensionless imports of those\n * files fail to resolve, and the build crashes with \"Custom deploy script\n * failed: undefined (1)\".\n *\n * Build the merged extension list that Vite should use:\n *\n * 1. User-configured pageExtensions go first (each prefixed with `.`) so\n * the user's priority wins. e.g. `.platform.tsx` resolves before `.tsx`.\n * 2. Vite's defaults follow, with duplicates removed.\n *\n * The user's pageExtensions retain their relative order, which is what\n * Next.js / Turbopack do via the `resolveExtensions` config option.\n *\n * See: cloudflare/vinext#1502\n */\nexport function buildViteResolveExtensions(\n pageExtensions?: readonly string[] | null,\n viteDefaults: readonly string[] = [\".mjs\", \".js\", \".mts\", \".ts\", \".jsx\", \".tsx\", \".json\"],\n): string[] {\n const normalized = normalizePageExtensions(pageExtensions);\n const dotted = normalized.map((ext) => `.${ext}`);\n const seen = new Set<string>();\n const result: string[] = [];\n for (const ext of [...dotted, ...viteDefaults]) {\n if (seen.has(ext)) continue;\n seen.add(ext);\n result.push(ext);\n }\n return result;\n}\n\n/**\n * Use function-form exclude for Node < 22.14 compatibility.\n */\nexport async function* scanWithExtensions(\n stem: string,\n cwd: string,\n extensions: readonly string[],\n exclude?: (name: string) => boolean,\n): AsyncGenerator<string> {\n const pattern = buildExtensionGlob(stem, extensions);\n for await (const file of glob(pattern, {\n cwd,\n ...(exclude ? { exclude } : {}),\n })) {\n yield file;\n }\n}\n"],"mappings":";;;AAGA,MAAM,0BAA0B;CAAC;CAAO;CAAM;CAAO;CAAK;AAE1D,SAAS,YAAY,OAAuB;CAC1C,OAAO,MAAM,QAAQ,uBAAuB,OAAO;;AAGrD,SAAgB,wBAAwB,gBAAqD;CAC3F,IAAI,CAAC,MAAM,QAAQ,eAAe,IAAI,eAAe,WAAW,GAC9D,OAAO,CAAC,GAAG,wBAAwB;CAGrC,MAAM,WAAW,eACd,QAAQ,QAAuB,OAAO,QAAQ,SAAS,CACvD,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,QAAQ,GAAG,CAAC,CAC5C,QAAQ,QAAQ,IAAI,SAAS,EAAE;CAClC,OAAO,SAAS,SAAS,IAAI,CAAC,GAAG,SAAS,GAAG,CAAC,GAAG,wBAAwB;;AAG3E,SAAS,mBAAmB,MAAc,YAAuC;CAC/E,IAAI,WAAW,WAAW,GACxB,OAAO,GAAG,KAAK,GAAG,WAAW;CAE/B,OAAO,GAAG,KAAK,IAAI,WAAW,KAAK,IAAI,CAAC;;;;;;AAmB1C,SAAgB,uBACd,gBACkB;CAClB,MAAM,aAAa,wBAAwB,eAAe;CAC1D,MAAM,mBAAmB,WAAW,KAAK,QAAQ,IAAI,MAAM;CAC3D,MAAM,aAAa,MAAM,WAAW,KAAK,QAAQ,YAAY,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC;CAE7E,MAAM,iBAAiB,IAAI,OAAO,MAAM,WAAW,GAAG;CACtD,MAAM,qBAAqB,cAAyC;EAClE,MAAM,QAAQ,UAAU,WAAW,IAAI,UAAU,KAAK,IAAI,UAAU,KAAK,IAAI,CAAC;EAC9E,OAAO,IAAI,OAAO,KAAK,MAAM,UAAU,MAAM,MAAM,WAAW,GAAG;;CAGnE,MAAM,qBAAqB,kBAAkB,CAAC,QAAQ,QAAQ,CAAC;CAC/D,MAAM,sBAAsB,kBAAkB,CAAC,QAAQ,CAAC;CACxD,MAAM,iBAAiB,kBAAkB,CAAC,SAAS,CAAC;CACpD,MAAM,kBAAkB,kBAAkB,CAAC,UAAU,CAAC;CAEtD,OAAO;EACL;EACA;EACA;EACA,WAAW,UAAkB;GAC3B,OAAO,eAAe,KAAK,SAAS;;EAEtC,gBAAgB,UAAkB;GAChC,OAAO,mBAAmB,KAAK,SAAS;;EAE1C,iBAAiB,UAAkB;GACjC,OAAO,oBAAoB,KAAK,SAAS;;EAE3C,gBAAgB,UAAkB;GAChC,OAAO,eAAe,KAAK,SAAS;;EAEtC,iBAAiB,UAAkB;GACjC,OAAO,gBAAgB,KAAK,SAAS;;EAEvC,eAAe,UAAkB;GAC/B,OAAO,SAAS,QAAQ,gBAAgB,GAAG;;EAE9C;;;AAIH,SAAgB,uBAAuB,UAAkB,SAAoC;CAC3F,OAAO,QAAQ,iBAAiB,MAAM,QAAQ,WAAW,WAAW,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;AAsB3E,SAAgB,2BACd,gBACA,eAAkC;CAAC;CAAQ;CAAO;CAAQ;CAAO;CAAQ;CAAQ;CAAQ,EAC/E;CAEV,MAAM,SADa,wBAAwB,eAClB,CAAC,KAAK,QAAQ,IAAI,MAAM;CACjD,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAM,SAAmB,EAAE;CAC3B,KAAK,MAAM,OAAO,CAAC,GAAG,QAAQ,GAAG,aAAa,EAAE;EAC9C,IAAI,KAAK,IAAI,IAAI,EAAE;EACnB,KAAK,IAAI,IAAI;EACb,OAAO,KAAK,IAAI;;CAElB,OAAO;;;;;AAMT,gBAAuB,mBACrB,MACA,KACA,YACA,SACwB;CACxB,MAAM,UAAU,mBAAmB,MAAM,WAAW;CACpD,WAAW,MAAM,QAAQ,KAAK,SAAS;EACrC;EACA,GAAI,UAAU,EAAE,SAAS,GAAG,EAAE;EAC/B,CAAC,EACA,MAAM"}
|
|
@@ -23,7 +23,7 @@ declare function invalidateRouteCache(pagesDir: string): void;
|
|
|
23
23
|
* - pages/about.tsx -> /about
|
|
24
24
|
* - pages/posts/[id].tsx -> /posts/:id
|
|
25
25
|
* - pages/[...slug].tsx -> /:slug+
|
|
26
|
-
* - Ignores _app.tsx, _document.tsx, _error.tsx
|
|
26
|
+
* - Ignores _app.tsx, _document.tsx, _error.tsx (Next.js special files)
|
|
27
27
|
* - Ignores pages/api/ (handled separately later)
|
|
28
28
|
*/
|
|
29
29
|
declare function pagesRouter(pagesDir: string, pageExtensions?: readonly string[], matcher?: ValidFileMatcher): Promise<Route[]>;
|
|
@@ -4,6 +4,12 @@ import { patternToNextFormat, validateRoutePatterns } from "./route-validation.j
|
|
|
4
4
|
import { createRouteTrieCache, matchRouteWithTrie } from "./route-matching.js";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
//#region src/routing/pages-router.ts
|
|
7
|
+
/** Next.js special pages that should not produce routes. */
|
|
8
|
+
const RESERVED_PAGE_NAMES = new Set([
|
|
9
|
+
"_app",
|
|
10
|
+
"_document",
|
|
11
|
+
"_error"
|
|
12
|
+
]);
|
|
7
13
|
const routeCache = /* @__PURE__ */ new Map();
|
|
8
14
|
/**
|
|
9
15
|
* Invalidate cached routes for a given pages directory.
|
|
@@ -21,7 +27,7 @@ function invalidateRouteCache(pagesDir) {
|
|
|
21
27
|
* - pages/about.tsx -> /about
|
|
22
28
|
* - pages/posts/[id].tsx -> /posts/:id
|
|
23
29
|
* - pages/[...slug].tsx -> /:slug+
|
|
24
|
-
* - Ignores _app.tsx, _document.tsx, _error.tsx
|
|
30
|
+
* - Ignores _app.tsx, _document.tsx, _error.tsx (Next.js special files)
|
|
25
31
|
* - Ignores pages/api/ (handled separately later)
|
|
26
32
|
*/
|
|
27
33
|
async function pagesRouter(pagesDir, pageExtensions, matcher) {
|
|
@@ -43,7 +49,7 @@ async function pagesRouter(pagesDir, pageExtensions, matcher) {
|
|
|
43
49
|
}
|
|
44
50
|
async function scanPageRoutes(pagesDir, matcher) {
|
|
45
51
|
const routes = [];
|
|
46
|
-
for await (const file of scanWithExtensions("**/*", pagesDir, matcher.extensions, (name) => name === "api" ||
|
|
52
|
+
for await (const file of scanWithExtensions("**/*", pagesDir, matcher.extensions, (name) => name === "api" || RESERVED_PAGE_NAMES.has(name))) {
|
|
47
53
|
const route = fileToRoute(file, pagesDir, matcher);
|
|
48
54
|
if (route) routes.push(route);
|
|
49
55
|
}
|
|
@@ -93,6 +99,7 @@ function fileToRoute(file, pagesDir, matcher) {
|
|
|
93
99
|
urlSegments.push(decodeRouteSegment(segment));
|
|
94
100
|
}
|
|
95
101
|
const pattern = "/" + urlSegments.join("/");
|
|
102
|
+
if (segments.length === 1 && RESERVED_PAGE_NAMES.has(segments[0])) return null;
|
|
96
103
|
return {
|
|
97
104
|
pattern: pattern === "/" ? "/" : pattern,
|
|
98
105
|
patternParts: urlSegments.filter(Boolean),
|
|
@@ -139,7 +146,7 @@ async function scanApiRoutes(pagesDir, matcher) {
|
|
|
139
146
|
let files;
|
|
140
147
|
try {
|
|
141
148
|
files = [];
|
|
142
|
-
for await (const file of scanWithExtensions("**/*", apiDir, matcher.extensions
|
|
149
|
+
for await (const file of scanWithExtensions("**/*", apiDir, matcher.extensions)) files.push(file);
|
|
143
150
|
} catch {
|
|
144
151
|
files = [];
|
|
145
152
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pages-router.js","names":[],"sources":["../../src/routing/pages-router.ts"],"sourcesContent":["import path from \"node:path\";\nimport { compareRoutes, decodeRouteSegment } from \"./utils.js\";\nimport {\n createValidFileMatcher,\n scanWithExtensions,\n type ValidFileMatcher,\n} from \"./file-matcher.js\";\nimport { validateRoutePatterns } from \"./route-validation.js\";\nimport { createRouteTrieCache, matchRouteWithTrie } from \"./route-matching.js\";\n\nexport type Route = {\n /** URL pattern, e.g. \"/\" or \"/about\" or \"/posts/:id\" */\n pattern: string;\n /** Pre-split pattern segments (computed once at scan time, reused per request) */\n patternParts: string[];\n /** Absolute file path to the page component */\n filePath: string;\n /** Whether this is a dynamic route */\n isDynamic: boolean;\n /** Parameter names for dynamic segments */\n params: string[];\n};\n\n// Route cache — invalidated when pages directory changes\nconst routeCache = new Map<string, { routes: Route[]; promise: Promise<Route[]> }>();\n\n/**\n * Invalidate cached routes for a given pages directory.\n * Called by the file watcher when pages are added/removed.\n */\nexport function invalidateRouteCache(pagesDir: string): void {\n for (const key of routeCache.keys()) {\n if (key.startsWith(`pages:${pagesDir}:`) || key.startsWith(`api:${pagesDir}:`)) {\n routeCache.delete(key);\n }\n }\n}\n\n/**\n * Scan the pages/ directory and return a list of routes.\n * Results are cached — call invalidateRouteCache() when files change.\n *\n * Follows Next.js Pages Router conventions:\n * - pages/index.tsx -> /\n * - pages/about.tsx -> /about\n * - pages/posts/[id].tsx -> /posts/:id\n * - pages/[...slug].tsx -> /:slug+\n * - Ignores _app.tsx, _document.tsx, _error.tsx, files starting with _\n * - Ignores pages/api/ (handled separately later)\n */\nexport async function pagesRouter(\n pagesDir: string,\n pageExtensions?: readonly string[],\n matcher?: ValidFileMatcher,\n): Promise<Route[]> {\n matcher ??= createValidFileMatcher(pageExtensions);\n const cacheKey = `pages:${pagesDir}:${JSON.stringify(matcher.extensions)}`;\n const cached = routeCache.get(cacheKey);\n if (cached) return cached.promise;\n\n const promise = scanPageRoutes(pagesDir, matcher);\n routeCache.set(cacheKey, { routes: [], promise });\n const routes = await promise;\n routeCache.set(cacheKey, { routes, promise });\n return routes;\n}\n\nasync function scanPageRoutes(pagesDir: string, matcher: ValidFileMatcher): Promise<Route[]> {\n const routes: Route[] = [];\n\n // Use function form of exclude for Node < 22.14 compatibility (string arrays require >= 22.14)\n for await (const file of scanWithExtensions(\n \"**/*\",\n pagesDir,\n matcher.extensions,\n (name: string) => name === \"api\" || name.startsWith(\"_\"),\n )) {\n const route = fileToRoute(file, pagesDir, matcher);\n if (route) routes.push(route);\n }\n\n validateRoutePatterns(routes.map((route) => route.pattern));\n\n // Sort: static routes first, then dynamic, then catch-all\n routes.sort(compareRoutes);\n\n return routes;\n}\n\n/**\n * Convert a file path relative to pages/ into a Route.\n */\nfunction fileToRoute(file: string, pagesDir: string, matcher: ValidFileMatcher): Route | null {\n // Remove extension\n const withoutExt = matcher.stripExtension(file);\n if (withoutExt === file) return null;\n\n // Convert to URL segments\n const segments = withoutExt.split(path.sep);\n\n // Handle index files: pages/index.tsx -> /\n const lastSegment = segments[segments.length - 1];\n if (lastSegment === \"index\") {\n segments.pop();\n }\n\n const params: string[] = [];\n let isDynamic = false;\n\n // Convert Next.js dynamic segments to URL patterns.\n // Catch-all segments are only valid in terminal position.\n const urlSegments: string[] = [];\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n\n // Catch-all: [...slug] -> :slug+ (param names may contain any non-] chars)\n // Matches Next.js PARAMETER_PATTERN.\n const catchAllMatch = segment.match(/^\\[\\.\\.\\.([^\\]]+)\\]$/);\n if (catchAllMatch) {\n if (i !== segments.length - 1) return null;\n // Guard: names ending in + or * would collide with internal pattern modifiers.\n if (catchAllMatch[1].endsWith(\"+\") || catchAllMatch[1].endsWith(\"*\")) return null;\n isDynamic = true;\n params.push(catchAllMatch[1]);\n urlSegments.push(`:${catchAllMatch[1]}+`);\n continue;\n }\n\n // Optional catch-all: [[...slug]] -> :slug* (param names may contain any non-] chars)\n const optionalCatchAllMatch = segment.match(/^\\[\\[\\.\\.\\.([^\\]]+)\\]\\]$/);\n if (optionalCatchAllMatch) {\n if (i !== segments.length - 1) return null;\n if (optionalCatchAllMatch[1].endsWith(\"+\") || optionalCatchAllMatch[1].endsWith(\"*\"))\n return null;\n isDynamic = true;\n params.push(optionalCatchAllMatch[1]);\n urlSegments.push(`:${optionalCatchAllMatch[1]}*`);\n continue;\n }\n\n // Dynamic segment: [id] -> :id (param names may contain any non-] chars)\n const dynamicMatch = segment.match(/^\\[([^\\]]+)\\]$/);\n if (dynamicMatch) {\n if (dynamicMatch[1].endsWith(\"+\") || dynamicMatch[1].endsWith(\"*\")) return null;\n isDynamic = true;\n params.push(dynamicMatch[1]);\n urlSegments.push(`:${dynamicMatch[1]}`);\n continue;\n }\n\n urlSegments.push(decodeRouteSegment(segment));\n }\n\n const pattern = \"/\" + urlSegments.join(\"/\");\n\n return {\n pattern: pattern === \"/\" ? \"/\" : pattern,\n patternParts: urlSegments.filter(Boolean),\n filePath: path.join(pagesDir, file),\n isDynamic,\n params,\n };\n}\n\n// Trie cache — keyed by route array identity (same array = same trie)\nconst trieCache = createRouteTrieCache<Route>();\n\n/**\n * Match a URL path against a route pattern.\n * Returns the matched params or null if no match.\n */\nexport function matchRoute(\n url: string,\n routes: Route[],\n): { route: Route; params: Record<string, string | string[]> } | null {\n return matchRouteWithTrie(url, routes, trieCache);\n}\n\n/**\n * Scan the pages/api/ directory and return API routes.\n * Results are cached — call invalidateRouteCache() when files change.\n *\n * Follows Next.js conventions:\n * - pages/api/hello.ts -> /api/hello\n * - pages/api/users/[id].ts -> /api/users/:id\n */\nexport async function apiRouter(\n pagesDir: string,\n pageExtensions?: readonly string[],\n matcher?: ValidFileMatcher,\n): Promise<Route[]> {\n matcher ??= createValidFileMatcher(pageExtensions);\n const cacheKey = `api:${pagesDir}:${JSON.stringify(matcher.extensions)}`;\n const cached = routeCache.get(cacheKey);\n if (cached) return cached.promise;\n\n const promise = scanApiRoutes(pagesDir, matcher);\n routeCache.set(cacheKey, { routes: [], promise });\n const routes = await promise;\n routeCache.set(cacheKey, { routes, promise });\n return routes;\n}\n\nasync function scanApiRoutes(pagesDir: string, matcher: ValidFileMatcher): Promise<Route[]> {\n const apiDir = path.join(pagesDir, \"api\");\n let files: string[];\n try {\n files = [];\n for await (const file of scanWithExtensions(\n \"**/*\",\n apiDir,\n matcher.extensions,\n (name: string) => name.startsWith(\"_\"),\n )) {\n files.push(file);\n }\n } catch {\n files = [];\n }\n\n const routes: Route[] = [];\n\n for (const file of files) {\n // Reuse fileToRoute but pretend the file is under a virtual \"api/\" prefix\n const route = fileToRoute(path.join(\"api\", file), pagesDir, matcher);\n if (route) {\n routes.push(route);\n }\n }\n\n validateRoutePatterns(routes.map((route) => route.pattern));\n\n // Sort same as page routes\n routes.sort(compareRoutes);\n\n return routes;\n}\n\n/**\n * Convert internal route pattern (e.g., \"/posts/:id\", \"/docs/:slug+\")\n * to Next.js bracket format (e.g., \"/posts/[id]\", \"/docs/[...slug]\").\n * Used for __NEXT_DATA__.page which apps expect in Next.js format.\n */\nexport { patternToNextFormat } from \"./route-validation.js\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pages-router.js","names":[],"sources":["../../src/routing/pages-router.ts"],"sourcesContent":["import path from \"node:path\";\nimport { compareRoutes, decodeRouteSegment } from \"./utils.js\";\nimport {\n createValidFileMatcher,\n scanWithExtensions,\n type ValidFileMatcher,\n} from \"./file-matcher.js\";\nimport { validateRoutePatterns } from \"./route-validation.js\";\nimport { createRouteTrieCache, matchRouteWithTrie } from \"./route-matching.js\";\n\nexport type Route = {\n /** URL pattern, e.g. \"/\" or \"/about\" or \"/posts/:id\" */\n pattern: string;\n /** Pre-split pattern segments (computed once at scan time, reused per request) */\n patternParts: string[];\n /** Absolute file path to the page component */\n filePath: string;\n /** Whether this is a dynamic route */\n isDynamic: boolean;\n /** Parameter names for dynamic segments */\n params: string[];\n};\n\n/** Next.js special pages that should not produce routes. */\nconst RESERVED_PAGE_NAMES = new Set([\"_app\", \"_document\", \"_error\"]);\n\n// Route cache — invalidated when pages directory changes\nconst routeCache = new Map<string, { routes: Route[]; promise: Promise<Route[]> }>();\n\n/**\n * Invalidate cached routes for a given pages directory.\n * Called by the file watcher when pages are added/removed.\n */\nexport function invalidateRouteCache(pagesDir: string): void {\n for (const key of routeCache.keys()) {\n if (key.startsWith(`pages:${pagesDir}:`) || key.startsWith(`api:${pagesDir}:`)) {\n routeCache.delete(key);\n }\n }\n}\n\n/**\n * Scan the pages/ directory and return a list of routes.\n * Results are cached — call invalidateRouteCache() when files change.\n *\n * Follows Next.js Pages Router conventions:\n * - pages/index.tsx -> /\n * - pages/about.tsx -> /about\n * - pages/posts/[id].tsx -> /posts/:id\n * - pages/[...slug].tsx -> /:slug+\n * - Ignores _app.tsx, _document.tsx, _error.tsx (Next.js special files)\n * - Ignores pages/api/ (handled separately later)\n */\nexport async function pagesRouter(\n pagesDir: string,\n pageExtensions?: readonly string[],\n matcher?: ValidFileMatcher,\n): Promise<Route[]> {\n matcher ??= createValidFileMatcher(pageExtensions);\n const cacheKey = `pages:${pagesDir}:${JSON.stringify(matcher.extensions)}`;\n const cached = routeCache.get(cacheKey);\n if (cached) return cached.promise;\n\n const promise = scanPageRoutes(pagesDir, matcher);\n routeCache.set(cacheKey, { routes: [], promise });\n const routes = await promise;\n routeCache.set(cacheKey, { routes, promise });\n return routes;\n}\n\nasync function scanPageRoutes(pagesDir: string, matcher: ValidFileMatcher): Promise<Route[]> {\n const routes: Route[] = [];\n\n // Use function form of exclude for Node < 22.14 compatibility (string arrays require >= 22.14).\n // The `RESERVED_PAGE_NAMES` check here is a directory-traversal optimization only — glob's\n // exclude callback fires on directory names, not file names, so root-level files like\n // `_app.tsx` still get yielded and are filtered by the guard in `fileToRoute()` below.\n for await (const file of scanWithExtensions(\n \"**/*\",\n pagesDir,\n matcher.extensions,\n (name: string) => name === \"api\" || RESERVED_PAGE_NAMES.has(name),\n )) {\n const route = fileToRoute(file, pagesDir, matcher);\n if (route) routes.push(route);\n }\n\n validateRoutePatterns(routes.map((route) => route.pattern));\n\n // Sort: static routes first, then dynamic, then catch-all\n routes.sort(compareRoutes);\n\n return routes;\n}\n\n/**\n * Convert a file path relative to pages/ into a Route.\n */\nfunction fileToRoute(file: string, pagesDir: string, matcher: ValidFileMatcher): Route | null {\n // Remove extension\n const withoutExt = matcher.stripExtension(file);\n if (withoutExt === file) return null;\n\n // Convert to URL segments\n const segments = withoutExt.split(path.sep);\n\n // Handle index files: pages/index.tsx -> /\n const lastSegment = segments[segments.length - 1];\n if (lastSegment === \"index\") {\n segments.pop();\n }\n\n const params: string[] = [];\n let isDynamic = false;\n\n // Convert Next.js dynamic segments to URL patterns.\n // Catch-all segments are only valid in terminal position.\n const urlSegments: string[] = [];\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n\n // Catch-all: [...slug] -> :slug+ (param names may contain any non-] chars)\n // Matches Next.js PARAMETER_PATTERN.\n const catchAllMatch = segment.match(/^\\[\\.\\.\\.([^\\]]+)\\]$/);\n if (catchAllMatch) {\n if (i !== segments.length - 1) return null;\n // Guard: names ending in + or * would collide with internal pattern modifiers.\n if (catchAllMatch[1].endsWith(\"+\") || catchAllMatch[1].endsWith(\"*\")) return null;\n isDynamic = true;\n params.push(catchAllMatch[1]);\n urlSegments.push(`:${catchAllMatch[1]}+`);\n continue;\n }\n\n // Optional catch-all: [[...slug]] -> :slug* (param names may contain any non-] chars)\n const optionalCatchAllMatch = segment.match(/^\\[\\[\\.\\.\\.([^\\]]+)\\]\\]$/);\n if (optionalCatchAllMatch) {\n if (i !== segments.length - 1) return null;\n if (optionalCatchAllMatch[1].endsWith(\"+\") || optionalCatchAllMatch[1].endsWith(\"*\"))\n return null;\n isDynamic = true;\n params.push(optionalCatchAllMatch[1]);\n urlSegments.push(`:${optionalCatchAllMatch[1]}*`);\n continue;\n }\n\n // Dynamic segment: [id] -> :id (param names may contain any non-] chars)\n const dynamicMatch = segment.match(/^\\[([^\\]]+)\\]$/);\n if (dynamicMatch) {\n if (dynamicMatch[1].endsWith(\"+\") || dynamicMatch[1].endsWith(\"*\")) return null;\n isDynamic = true;\n params.push(dynamicMatch[1]);\n urlSegments.push(`:${dynamicMatch[1]}`);\n continue;\n }\n\n urlSegments.push(decodeRouteSegment(segment));\n }\n\n const pattern = \"/\" + urlSegments.join(\"/\");\n\n // Skip Next.js special pages (_app, _document, _error) at the root level only.\n // Subdirectory files like admin/_app.tsx are not reserved and should be served.\n // Read segments[0] after the index pop so this is correct for both `_app.tsx`\n // and `_app/index.tsx` shapes, independent of the glob-level exclude.\n if (segments.length === 1 && RESERVED_PAGE_NAMES.has(segments[0])) {\n return null;\n }\n\n return {\n pattern: pattern === \"/\" ? \"/\" : pattern,\n patternParts: urlSegments.filter(Boolean),\n filePath: path.join(pagesDir, file),\n isDynamic,\n params,\n };\n}\n\n// Trie cache — keyed by route array identity (same array = same trie)\nconst trieCache = createRouteTrieCache<Route>();\n\n/**\n * Match a URL path against a route pattern.\n * Returns the matched params or null if no match.\n */\nexport function matchRoute(\n url: string,\n routes: Route[],\n): { route: Route; params: Record<string, string | string[]> } | null {\n return matchRouteWithTrie(url, routes, trieCache);\n}\n\n/**\n * Scan the pages/api/ directory and return API routes.\n * Results are cached — call invalidateRouteCache() when files change.\n *\n * Follows Next.js conventions:\n * - pages/api/hello.ts -> /api/hello\n * - pages/api/users/[id].ts -> /api/users/:id\n */\nexport async function apiRouter(\n pagesDir: string,\n pageExtensions?: readonly string[],\n matcher?: ValidFileMatcher,\n): Promise<Route[]> {\n matcher ??= createValidFileMatcher(pageExtensions);\n const cacheKey = `api:${pagesDir}:${JSON.stringify(matcher.extensions)}`;\n const cached = routeCache.get(cacheKey);\n if (cached) return cached.promise;\n\n const promise = scanApiRoutes(pagesDir, matcher);\n routeCache.set(cacheKey, { routes: [], promise });\n const routes = await promise;\n routeCache.set(cacheKey, { routes, promise });\n return routes;\n}\n\nasync function scanApiRoutes(pagesDir: string, matcher: ValidFileMatcher): Promise<Route[]> {\n const apiDir = path.join(pagesDir, \"api\");\n let files: string[];\n try {\n files = [];\n for await (const file of scanWithExtensions(\"**/*\", apiDir, matcher.extensions)) {\n files.push(file);\n }\n } catch {\n files = [];\n }\n\n const routes: Route[] = [];\n\n for (const file of files) {\n // Reuse fileToRoute but pretend the file is under a virtual \"api/\" prefix\n const route = fileToRoute(path.join(\"api\", file), pagesDir, matcher);\n if (route) {\n routes.push(route);\n }\n }\n\n validateRoutePatterns(routes.map((route) => route.pattern));\n\n // Sort same as page routes\n routes.sort(compareRoutes);\n\n return routes;\n}\n\n/**\n * Convert internal route pattern (e.g., \"/posts/:id\", \"/docs/:slug+\")\n * to Next.js bracket format (e.g., \"/posts/[id]\", \"/docs/[...slug]\").\n * Used for __NEXT_DATA__.page which apps expect in Next.js format.\n */\nexport { patternToNextFormat } from \"./route-validation.js\";\n"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { matchRoute } from "../routing/pages-router.js";
|
|
2
2
|
import "./server-globals.js";
|
|
3
3
|
import { NextRequest } from "../shims/server.js";
|
|
4
|
-
import { mergeRouteParamsIntoQuery, parseQueryString } from "../utils/query.js";
|
|
5
4
|
import { importModule, reportRequestError } from "./instrumentation.js";
|
|
5
|
+
import { mergeRouteParamsIntoQuery, parseQueryString } from "../utils/query.js";
|
|
6
6
|
import { PagesBodyParseError, getMediaType, isJsonMediaType } from "./pages-media-type.js";
|
|
7
7
|
import { isEdgeApiRuntime } from "./edge-api-runtime.js";
|
|
8
8
|
import { DEFAULT_PAGES_API_BODY_SIZE_LIMIT, resolveBodyParserConfig } from "./pages-body-parser-config.js";
|
|
@@ -3,17 +3,19 @@ import { ACTION_REDIRECT_HEADER, VINEXT_MOUNTED_SLOTS_HEADER, VINEXT_PARAMS_HEAD
|
|
|
3
3
|
import { DANGEROUS_URL_BLOCK_MESSAGE, isDangerousScheme } from "../shims/url-safety.js";
|
|
4
4
|
import { AppElementsWire } from "./app-elements-wire.js";
|
|
5
5
|
import { APP_RSC_RENDER_MODE_REFRESH_PRESERVE_UI } from "./app-rsc-render-mode.js";
|
|
6
|
+
import { getMountedSlotIdsHeader, resolveVisitedResponseInterceptionContext } from "./app-elements.js";
|
|
6
7
|
import { installWindowNext } from "../client/window-next.js";
|
|
7
8
|
import { scrollToHashTargetOnNextFrame } from "../shims/hash-scroll.js";
|
|
8
9
|
import { getNavigationRuntime, registerNavigationRuntimeBootstrap, registerNavigationRuntimeFunctions } from "../client/navigation-runtime.js";
|
|
9
10
|
import { notifyAppRouterTransitionStart } from "../client/instrumentation-client-state.js";
|
|
10
|
-
import { getMountedSlotIdsHeader, resolveVisitedResponseInterceptionContext } from "./app-elements.js";
|
|
11
11
|
import { resolveManifestNavigationInterceptionContext } from "./app-browser-interception-context.js";
|
|
12
12
|
import { createHistoryStateWithNavigationMetadata, createHistoryStateWithPreviousNextUrl, readHistoryStatePreviousNextUrl, readHistoryStateTraversalIndex, resolveHistoryTraversalIntent } from "./app-history-state.js";
|
|
13
13
|
import { VINEXT_RSC_COMPATIBILITY_ID_HEADER, createRscRequestHeaders, createRscRequestUrl, getVinextRscCompatibilityId, resolveHardNavigationTargetFromRscResponse, resolveRscCompatibilityNavigationDecision } from "./app-rsc-cache-busting.js";
|
|
14
14
|
import { AppRouterContext } from "../shims/internal/app-router-context.js";
|
|
15
|
+
import { consumeAppRouterScrollIntent } from "../shims/app-router-scroll-state.js";
|
|
15
16
|
import { __basePath, appRouterInstance, commitClientNavigationState, consumePrefetchResponse, createCachedRscResponseSnapshot, createClientNavigationRenderSnapshot, getClientNavigationRenderContext, getPrefetchCache, invalidatePrefetchCache, navigateClientSide, pushHistoryStateWithoutNotify, replaceClientParamsWithoutNotify, replaceHistoryStateWithoutNotify, restoreRscResponse, setClientParams, setMountedSlotsHeader, setNavigationContext, setPendingPathname } from "../shims/navigation.js";
|
|
16
17
|
import { DevRecoveryBoundary, RedirectBoundary } from "../shims/error-boundary.js";
|
|
18
|
+
import { AppRouterScrollCommitProvider } from "../shims/app-router-scroll.js";
|
|
17
19
|
import { ElementsContext, Slot } from "../shims/slot.js";
|
|
18
20
|
import "../client/instrumentation-client.js";
|
|
19
21
|
import { createDiscardedServerActionRefreshScheduler, createServerActionInitiationSnapshot, isServerActionResult, parseServerActionRevalidationHeader, resolveServerActionRedirectLocation, shouldClearClientNavigationCachesForServerActionResult } from "./app-browser-action-result.js";
|
|
@@ -28,6 +30,7 @@ import { devOnCaughtError, devOnUncaughtError, installDevErrorOverlay } from "./
|
|
|
28
30
|
import { throwOnServerActionNotFound } from "./server-action-not-found.js";
|
|
29
31
|
import { resolveRscRedirectLifecycleHop } from "./app-browser-rsc-redirect.js";
|
|
30
32
|
import { createOptimisticRouteTemplate, getOptimisticPrefetchSourceKey, getOptimisticRouteTemplateKey, resolveOptimisticNavigationPayload } from "./app-optimistic-routing.js";
|
|
33
|
+
import { removeStylesheetLinksCoveredByInlineCss } from "./app-inline-css-client.js";
|
|
31
34
|
import { createElement, startTransition, use, useLayoutEffect, useRef, useState } from "react";
|
|
32
35
|
import { createFromFetch, createFromReadableStream, createTemporaryReferenceSet, encodeReply, setServerCallback } from "@vitejs/plugin-rsc/browser";
|
|
33
36
|
import { hydrateRoot } from "react-dom/client";
|
|
@@ -267,7 +270,7 @@ function createNavigationCommitEffect(options) {
|
|
|
267
270
|
commitClientNavigationState(navId);
|
|
268
271
|
};
|
|
269
272
|
}
|
|
270
|
-
async function renderNavigationPayload(payload, navigationSnapshot, targetHref, navId, historyUpdateMode, params, previousNextUrl, pendingRouterState, payloadOrigin, actionType = "navigate", operationLane = "navigation", traversalIntent = null) {
|
|
273
|
+
async function renderNavigationPayload(payload, navigationSnapshot, targetHref, navId, historyUpdateMode, params, previousNextUrl, pendingRouterState, payloadOrigin, actionType = "navigate", operationLane = "navigation", traversalIntent = null, scrollIntent = null) {
|
|
271
274
|
try {
|
|
272
275
|
return await browserNavigationController.renderNavigationPayload({
|
|
273
276
|
actionType,
|
|
@@ -283,6 +286,7 @@ async function renderNavigationPayload(payload, navigationSnapshot, targetHref,
|
|
|
283
286
|
params,
|
|
284
287
|
pendingRouterState,
|
|
285
288
|
previousNextUrl,
|
|
289
|
+
scrollIntent,
|
|
286
290
|
targetHistoryIndex: traversalIntent === null ? void 0 : traversalIntent.targetHistoryIndex,
|
|
287
291
|
targetHref,
|
|
288
292
|
navId
|
|
@@ -434,6 +438,7 @@ function BrowserRoot({ initialElements, initialNavigationSnapshot }) {
|
|
|
434
438
|
}, [setTreeStateValue]);
|
|
435
439
|
useLayoutEffect(() => {
|
|
436
440
|
setMountedSlotsHeader(getMountedSlotIdsHeader(stateRef.current.elements));
|
|
441
|
+
removeStylesheetLinksCoveredByInlineCss();
|
|
437
442
|
getNavigationRuntime()?.functions.pingVisibleLinks?.();
|
|
438
443
|
}, [treeState.elements]);
|
|
439
444
|
useLayoutEffect(() => {
|
|
@@ -449,9 +454,10 @@ function BrowserRoot({ initialElements, initialNavigationSnapshot }) {
|
|
|
449
454
|
resetKey: treeState.renderId,
|
|
450
455
|
onCatch: handleDevRecoveryBoundaryCatch
|
|
451
456
|
}, innerTree) : innerTree;
|
|
457
|
+
const scrollScopedTree = createElement(AppRouterScrollCommitProvider, { commitId: treeState.renderId }, committedTree);
|
|
452
458
|
const ClientNavigationRenderContext = getClientNavigationRenderContext();
|
|
453
|
-
if (!ClientNavigationRenderContext) return
|
|
454
|
-
return createElement(ClientNavigationRenderContext.Provider, { value: treeState.navigationSnapshot },
|
|
459
|
+
if (!ClientNavigationRenderContext) return scrollScopedTree;
|
|
460
|
+
return createElement(ClientNavigationRenderContext.Provider, { value: treeState.navigationSnapshot }, scrollScopedTree);
|
|
455
461
|
}
|
|
456
462
|
function restoreHydrationNavigationContext(pathname, searchParams, params) {
|
|
457
463
|
setNavigationContext({
|
|
@@ -663,7 +669,7 @@ function bootstrapHydration(rscStream) {
|
|
|
663
669
|
registerNavigationRuntimeFunctions({
|
|
664
670
|
clearNavigationCaches: clearClientNavigationCaches,
|
|
665
671
|
commitHashNavigation: commitHashOnlyNavigation,
|
|
666
|
-
navigate: async function navigateRsc(href, redirectDepth = 0, navigationKind = "navigate", historyUpdateMode, previousNextUrlOverride, programmaticTransition = false, traversalIntent) {
|
|
672
|
+
navigate: async function navigateRsc(href, redirectDepth = 0, navigationKind = "navigate", historyUpdateMode, previousNextUrlOverride, programmaticTransition = false, traversalIntent, scrollIntent) {
|
|
667
673
|
let pendingRouterState = null;
|
|
668
674
|
const navId = browserNavigationController.beginNavigation();
|
|
669
675
|
discardedServerActionRefreshScheduler.markNavigationStart();
|
|
@@ -676,6 +682,10 @@ function bootstrapHydration(rscStream) {
|
|
|
676
682
|
currentHistoryIndex: currentHistoryTraversalIndex,
|
|
677
683
|
historyState: window.history.state
|
|
678
684
|
}) : null;
|
|
685
|
+
const performHardNavigationForScrollIntent = (targetHref) => {
|
|
686
|
+
consumeAppRouterScrollIntent(scrollIntent ?? null);
|
|
687
|
+
return browserNavigationController.performHardNavigation(targetHref);
|
|
688
|
+
};
|
|
679
689
|
try {
|
|
680
690
|
const shouldUsePendingRouterState = programmaticTransition;
|
|
681
691
|
if (shouldUsePendingRouterState && hasBrowserRouterState()) pendingRouterState = beginPendingBrowserRouterState();
|
|
@@ -709,7 +719,7 @@ function bootstrapHydration(rscStream) {
|
|
|
709
719
|
responseUrl: cachedRoute.response.url
|
|
710
720
|
});
|
|
711
721
|
if (compatibilityDecision.kind === "hard-navigate") {
|
|
712
|
-
|
|
722
|
+
performHardNavigationForScrollIntent(compatibilityDecision.hardNavigationTarget);
|
|
713
723
|
return;
|
|
714
724
|
}
|
|
715
725
|
if (!browserNavigationController.isCurrentNavigation(navId)) return;
|
|
@@ -717,7 +727,7 @@ function bootstrapHydration(rscStream) {
|
|
|
717
727
|
const cachedNavigationSnapshot = createClientNavigationRenderSnapshot(currentHref, cachedParams);
|
|
718
728
|
const cachedPayload = decodeAppElementsPromise(createFromFetch(Promise.resolve(restoreRscResponse(cachedRoute.response))));
|
|
719
729
|
if (!browserNavigationController.isCurrentNavigation(navId)) return;
|
|
720
|
-
await renderNavigationPayload(cachedPayload, cachedNavigationSnapshot, currentHref, navId, currentHistoryMode, cachedParams, requestPreviousNextUrl, detachedNavigationCommits ? null : pendingRouterState, VISITED_CACHE_APP_NAVIGATION_PAYLOAD_ORIGIN, toActionType(navigationKind), toOperationLane(navigationKind), activeTraversalIntent);
|
|
730
|
+
await renderNavigationPayload(cachedPayload, cachedNavigationSnapshot, currentHref, navId, currentHistoryMode, cachedParams, requestPreviousNextUrl, detachedNavigationCommits ? null : pendingRouterState, VISITED_CACHE_APP_NAVIGATION_PAYLOAD_ORIGIN, toActionType(navigationKind), toOperationLane(navigationKind), activeTraversalIntent, scrollIntent);
|
|
721
731
|
return;
|
|
722
732
|
}
|
|
723
733
|
let navResponse;
|
|
@@ -749,7 +759,7 @@ function bootstrapHydration(rscStream) {
|
|
|
749
759
|
if (optimisticPayload !== null) {
|
|
750
760
|
detachedNavigationCommits = true;
|
|
751
761
|
const optimisticNavigationSnapshot = createClientNavigationRenderSnapshot(currentHref, optimisticPayload.params);
|
|
752
|
-
renderNavigationPayload(Promise.resolve(optimisticPayload.elements), optimisticNavigationSnapshot, currentHref, navId, currentHistoryMode, optimisticPayload.params, requestPreviousNextUrl, null, FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN, toActionType(navigationKind), toOperationLane(navigationKind), activeTraversalIntent).catch((error) => {
|
|
762
|
+
renderNavigationPayload(Promise.resolve(optimisticPayload.elements), optimisticNavigationSnapshot, currentHref, navId, currentHistoryMode, optimisticPayload.params, requestPreviousNextUrl, null, FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN, toActionType(navigationKind), toOperationLane(navigationKind), activeTraversalIntent, scrollIntent).catch((error) => {
|
|
753
763
|
if (browserNavigationController.isCurrentNavigation(navId)) console.error("[vinext] Optimistic RSC navigation error:", error);
|
|
754
764
|
});
|
|
755
765
|
}
|
|
@@ -762,8 +772,7 @@ function bootstrapHydration(rscStream) {
|
|
|
762
772
|
if (!browserNavigationController.isCurrentNavigation(navId)) return;
|
|
763
773
|
const isRscResponse = (navResponse.headers.get("content-type") ?? "").startsWith("text/x-component");
|
|
764
774
|
if (!navResponse.ok || !isRscResponse || !navResponse.body) {
|
|
765
|
-
|
|
766
|
-
browserNavigationController.performHardNavigation(resolveHardNavigationTargetFromRscResponse(responseUrl, currentHref, window.location.origin));
|
|
775
|
+
performHardNavigationForScrollIntent(resolveHardNavigationTargetFromRscResponse(navResponseUrl ?? navResponse.url, currentHref, window.location.origin));
|
|
767
776
|
return;
|
|
768
777
|
}
|
|
769
778
|
const compatibilityDecision = resolveRscCompatibilityNavigationDecision({
|
|
@@ -774,7 +783,7 @@ function bootstrapHydration(rscStream) {
|
|
|
774
783
|
responseUrl: navResponseUrl ?? navResponse.url
|
|
775
784
|
});
|
|
776
785
|
if (compatibilityDecision.kind === "hard-navigate") {
|
|
777
|
-
|
|
786
|
+
performHardNavigationForScrollIntent(compatibilityDecision.hardNavigationTarget);
|
|
778
787
|
return;
|
|
779
788
|
}
|
|
780
789
|
const redirectDecision = resolveRscRedirectLifecycleHop({
|
|
@@ -787,7 +796,7 @@ function bootstrapHydration(rscStream) {
|
|
|
787
796
|
});
|
|
788
797
|
if (redirectDecision.kind === "terminal-hard-navigation") {
|
|
789
798
|
if (redirectDecision.reason === "maxRedirectsExceeded") console.error("[vinext] Too many RSC redirects — aborting navigation to prevent infinite loop.");
|
|
790
|
-
|
|
799
|
+
performHardNavigationForScrollIntent(redirectDecision.href);
|
|
791
800
|
return;
|
|
792
801
|
}
|
|
793
802
|
if (redirectDecision.kind === "follow") {
|
|
@@ -802,12 +811,12 @@ function bootstrapHydration(rscStream) {
|
|
|
802
811
|
navResponse.body?.cancel().catch(() => {});
|
|
803
812
|
const resolvedTarget = new URL(flightRedirectTarget, window.location.origin);
|
|
804
813
|
if (resolvedTarget.origin !== window.location.origin) {
|
|
805
|
-
|
|
814
|
+
performHardNavigationForScrollIntent(resolvedTarget.href);
|
|
806
815
|
return;
|
|
807
816
|
}
|
|
808
817
|
if (redirectCount >= 10) {
|
|
809
818
|
console.error("[vinext] Too many RSC redirects — aborting navigation to prevent infinite loop.");
|
|
810
|
-
|
|
819
|
+
performHardNavigationForScrollIntent(resolvedTarget.href);
|
|
811
820
|
return;
|
|
812
821
|
}
|
|
813
822
|
currentHref = `${resolvedTarget.pathname}${resolvedTarget.search}${resolvedTarget.hash}`;
|
|
@@ -827,7 +836,7 @@ function bootstrapHydration(rscStream) {
|
|
|
827
836
|
if (!browserNavigationController.isCurrentNavigation(navId)) return;
|
|
828
837
|
const rscPayload = decodeAppElementsPromise(createFromFetch(Promise.resolve(reactResponse)));
|
|
829
838
|
if (!browserNavigationController.isCurrentNavigation(navId)) return;
|
|
830
|
-
if (await renderNavigationPayload(rscPayload, navigationSnapshot, currentHref, navId, currentHistoryMode, navParams, requestPreviousNextUrl, detachedNavigationCommits ? null : pendingRouterState, FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN, toActionType(navigationKind), toOperationLane(navigationKind), activeTraversalIntent) !== "committed") return;
|
|
839
|
+
if (await renderNavigationPayload(rscPayload, navigationSnapshot, currentHref, navId, currentHistoryMode, navParams, requestPreviousNextUrl, detachedNavigationCommits ? null : pendingRouterState, FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN, toActionType(navigationKind), toOperationLane(navigationKind), activeTraversalIntent, scrollIntent) !== "committed") return;
|
|
831
840
|
if (!browserNavigationController.isCurrentNavigation(navId)) return;
|
|
832
841
|
const resolvedElements = await rscPayload;
|
|
833
842
|
const metadata = AppElementsWire.readMetadata(resolvedElements);
|
|
@@ -843,7 +852,7 @@ function bootstrapHydration(rscStream) {
|
|
|
843
852
|
} catch (error) {
|
|
844
853
|
if (!browserNavigationController.isCurrentNavigation(navId)) return;
|
|
845
854
|
if (!isPageUnloading) console.error("[vinext] RSC navigation error:", error);
|
|
846
|
-
|
|
855
|
+
performHardNavigationForScrollIntent(currentHref);
|
|
847
856
|
} finally {
|
|
848
857
|
browserNavigationController.finalizeNavigation(navId, pendingRouterState);
|
|
849
858
|
discardedServerActionRefreshScheduler.markNavigationSettled();
|