vinext 0.1.1 → 0.1.2

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 (147) hide show
  1. package/README.md +2 -5
  2. package/dist/build/client-build-config.d.ts +7 -1
  3. package/dist/build/client-build-config.js +9 -1
  4. package/dist/check.js +4 -3
  5. package/dist/client/navigation-runtime.d.ts +3 -2
  6. package/dist/client/window-next.d.ts +6 -4
  7. package/dist/config/config-matchers.d.ts +11 -4
  8. package/dist/config/config-matchers.js +15 -2
  9. package/dist/config/next-config.d.ts +13 -0
  10. package/dist/config/next-config.js +2 -0
  11. package/dist/deploy.js +9 -2
  12. package/dist/entries/app-rsc-entry.js +7 -1
  13. package/dist/entries/pages-client-entry.js +1 -1
  14. package/dist/entries/pages-server-entry.js +7 -6
  15. package/dist/index.d.ts +0 -2
  16. package/dist/index.js +86 -78
  17. package/dist/plugins/dynamic-preload-metadata.d.ts +13 -0
  18. package/dist/plugins/dynamic-preload-metadata.js +415 -0
  19. package/dist/plugins/og-assets.js +2 -2
  20. package/dist/plugins/optimize-imports.d.ts +8 -4
  21. package/dist/plugins/optimize-imports.js +16 -12
  22. package/dist/plugins/sass.d.ts +53 -24
  23. package/dist/plugins/sass.js +249 -1
  24. package/dist/plugins/wasm-module-import.d.ts +15 -0
  25. package/dist/plugins/wasm-module-import.js +50 -0
  26. package/dist/routing/app-route-graph.d.ts +23 -1
  27. package/dist/routing/app-route-graph.js +47 -8
  28. package/dist/routing/file-matcher.js +1 -1
  29. package/dist/server/app-browser-entry.js +108 -213
  30. package/dist/server/app-browser-error.d.ts +4 -1
  31. package/dist/server/app-browser-error.js +7 -1
  32. package/dist/server/app-browser-history-controller.d.ts +104 -0
  33. package/dist/server/app-browser-history-controller.js +210 -0
  34. package/dist/server/app-browser-navigation-controller.d.ts +3 -2
  35. package/dist/server/app-browser-navigation-controller.js +10 -7
  36. package/dist/server/app-browser-rsc-redirect.d.ts +11 -2
  37. package/dist/server/app-browser-rsc-redirect.js +30 -8
  38. package/dist/server/app-browser-state.js +4 -7
  39. package/dist/server/app-browser-visible-commit.js +1 -1
  40. package/dist/server/app-fallback-renderer.d.ts +2 -1
  41. package/dist/server/app-fallback-renderer.js +3 -1
  42. package/dist/server/app-middleware.js +1 -0
  43. package/dist/server/app-optimistic-routing.js +22 -1
  44. package/dist/server/app-page-boundary-render.d.ts +2 -1
  45. package/dist/server/app-page-boundary-render.js +4 -2
  46. package/dist/server/app-page-cache.js +9 -7
  47. package/dist/server/app-page-dispatch.d.ts +8 -0
  48. package/dist/server/app-page-dispatch.js +18 -5
  49. package/dist/server/app-page-element-builder.d.ts +22 -2
  50. package/dist/server/app-page-element-builder.js +37 -8
  51. package/dist/server/app-page-execution.d.ts +1 -1
  52. package/dist/server/app-page-execution.js +32 -17
  53. package/dist/server/app-page-render.d.ts +1 -1
  54. package/dist/server/app-page-render.js +7 -14
  55. package/dist/server/app-page-request.d.ts +1 -0
  56. package/dist/server/app-page-request.js +3 -2
  57. package/dist/server/app-page-response.js +1 -1
  58. package/dist/server/app-page-route-wiring.d.ts +3 -1
  59. package/dist/server/app-page-route-wiring.js +8 -7
  60. package/dist/server/app-page-stream.d.ts +1 -6
  61. package/dist/server/app-page-stream.js +1 -4
  62. package/dist/server/app-route-handler-response.js +11 -10
  63. package/dist/server/app-route-handler-runtime.js +12 -1
  64. package/dist/server/app-rsc-handler.js +1 -1
  65. package/dist/server/app-rsc-response-finalizer.js +1 -1
  66. package/dist/server/app-server-action-execution.d.ts +11 -0
  67. package/dist/server/app-server-action-execution.js +5 -2
  68. package/dist/server/app-ssr-entry.js +2 -2
  69. package/dist/server/app-ssr-stream.js +9 -1
  70. package/dist/server/dev-lockfile.js +2 -1
  71. package/dist/server/dev-server.js +43 -12
  72. package/dist/server/headers.d.ts +8 -1
  73. package/dist/server/headers.js +8 -1
  74. package/dist/server/instrumentation-runtime.d.ts +6 -0
  75. package/dist/server/instrumentation-runtime.js +8 -0
  76. package/dist/server/isr-decision.d.ts +79 -0
  77. package/dist/server/isr-decision.js +70 -0
  78. package/dist/server/metadata-route-response.js +5 -3
  79. package/dist/server/middleware-runtime.d.ts +13 -0
  80. package/dist/server/middleware-runtime.js +11 -7
  81. package/dist/server/middleware.js +1 -0
  82. package/dist/server/navigation-planner.d.ts +62 -1
  83. package/dist/server/navigation-planner.js +188 -0
  84. package/dist/server/navigation-trace.d.ts +11 -1
  85. package/dist/server/navigation-trace.js +11 -1
  86. package/dist/server/normalize-path.d.ts +0 -8
  87. package/dist/server/normalize-path.js +3 -1
  88. package/dist/server/otel-tracer-extension.d.ts +45 -0
  89. package/dist/server/otel-tracer-extension.js +89 -0
  90. package/dist/server/pages-api-route.d.ts +14 -3
  91. package/dist/server/pages-api-route.js +6 -1
  92. package/dist/server/pages-asset-tags.d.ts +15 -4
  93. package/dist/server/pages-asset-tags.js +18 -12
  94. package/dist/server/pages-data-route.js +5 -1
  95. package/dist/server/pages-node-compat.d.ts +3 -11
  96. package/dist/server/pages-node-compat.js +174 -121
  97. package/dist/server/pages-page-data.d.ts +28 -0
  98. package/dist/server/pages-page-data.js +61 -17
  99. package/dist/server/pages-page-handler.d.ts +1 -0
  100. package/dist/server/pages-page-handler.js +22 -6
  101. package/dist/server/pages-page-response.d.ts +45 -1
  102. package/dist/server/pages-page-response.js +66 -5
  103. package/dist/server/pages-readiness.d.ts +1 -1
  104. package/dist/server/pages-request-pipeline.d.ts +15 -1
  105. package/dist/server/pages-request-pipeline.js +23 -2
  106. package/dist/server/prod-server.d.ts +39 -1
  107. package/dist/server/prod-server.js +98 -34
  108. package/dist/shims/cache-runtime.js +9 -2
  109. package/dist/shims/dynamic-preload-chunks.d.ts +8 -0
  110. package/dist/shims/dynamic-preload-chunks.js +77 -0
  111. package/dist/shims/dynamic.d.ts +4 -0
  112. package/dist/shims/dynamic.js +4 -2
  113. package/dist/shims/error-boundary.d.ts +4 -4
  114. package/dist/shims/error.js +37 -11
  115. package/dist/shims/fetch-cache.d.ts +9 -1
  116. package/dist/shims/fetch-cache.js +11 -1
  117. package/dist/shims/head.js +6 -1
  118. package/dist/shims/headers.d.ts +16 -2
  119. package/dist/shims/headers.js +37 -1
  120. package/dist/shims/image-config.js +7 -1
  121. package/dist/shims/internal/app-route-detection.d.ts +6 -3
  122. package/dist/shims/internal/app-route-detection.js +10 -6
  123. package/dist/shims/internal/app-router-context.d.ts +5 -0
  124. package/dist/shims/metadata.d.ts +6 -2
  125. package/dist/shims/metadata.js +32 -14
  126. package/dist/shims/navigation.d.ts +7 -16
  127. package/dist/shims/navigation.js +33 -16
  128. package/dist/shims/router.js +28 -1
  129. package/dist/shims/script-nonce-context.d.ts +1 -1
  130. package/dist/shims/script-nonce-context.js +11 -3
  131. package/dist/shims/server.d.ts +17 -1
  132. package/dist/shims/server.js +31 -6
  133. package/dist/shims/slot.js +1 -1
  134. package/dist/shims/unified-request-context.js +1 -0
  135. package/dist/typegen.js +1 -0
  136. package/dist/utils/client-build-manifest.js +15 -5
  137. package/dist/utils/client-runtime-metadata.d.ts +45 -0
  138. package/dist/utils/client-runtime-metadata.js +63 -0
  139. package/dist/utils/hash.d.ts +17 -1
  140. package/dist/utils/hash.js +36 -1
  141. package/dist/utils/lazy-chunks.d.ts +27 -1
  142. package/dist/utils/lazy-chunks.js +65 -1
  143. package/dist/utils/manifest-paths.d.ts +20 -2
  144. package/dist/utils/manifest-paths.js +38 -3
  145. package/dist/utils/path.d.ts +2 -1
  146. package/dist/utils/path.js +5 -1
  147. package/package.json +2 -2
@@ -85,6 +85,7 @@ async function runMiddleware(runner, middlewarePath, request, i18nConfig, basePa
85
85
  return runGeneratedMiddleware({
86
86
  basePath,
87
87
  filePath: middlewarePath,
88
+ hadBasePath: true,
88
89
  i18nConfig,
89
90
  includeErrorDetails: process.env.NODE_ENV !== "production",
90
91
  isDataRequest,
@@ -114,6 +114,63 @@ type FlightResultV0 = {
114
114
  href: string;
115
115
  targetSnapshot: RouteSnapshotV0;
116
116
  };
117
+ type RscFetchResultSource = "cached" | "live";
118
+ type RscFetchResultFactsV0 = {
119
+ source: RscFetchResultSource;
120
+ currentHref: string;
121
+ origin: string;
122
+ effectiveHistoryUpdateMode: "push" | "replace";
123
+ redirectDepth: number;
124
+ requestPreviousNextUrl: string | null;
125
+ clientCompatibilityId: string | null;
126
+ responseOk: boolean;
127
+ isRscContentType: boolean;
128
+ hasBody: boolean;
129
+ compatibilityIdHeader: string | null;
130
+ responseUrl: string | null;
131
+ streamedRedirectTarget: string | null;
132
+ };
133
+ type RscRedirectFollowV0 = {
134
+ href: string;
135
+ historyUpdateMode: "push" | "replace";
136
+ previousNextUrl: string | null;
137
+ redirectDepth: number;
138
+ };
139
+ type RscFetchResultHardNavReason = "invalidRscPayload" | "rscCompatibilityMismatch" | "externalRedirectTarget" | "redirectDepthExhausted" | "streamedRedirectLoop";
140
+ type RscFetchResultDecisionV0 = {
141
+ kind: "proceedToCommit";
142
+ discardBody: false;
143
+ trace: NavigationTrace;
144
+ } | {
145
+ kind: "followRedirect";
146
+ discardBody: boolean;
147
+ redirect: RscRedirectFollowV0;
148
+ trace: NavigationTrace;
149
+ } | {
150
+ kind: "hardNavigate";
151
+ discardBody: boolean;
152
+ url: string;
153
+ reason: RscFetchResultHardNavReason;
154
+ trace: NavigationTrace;
155
+ };
156
+ type EarlyNavigationIntentFactsV0 = {
157
+ basePath: string;
158
+ currentHref: string;
159
+ mode: "push" | "replace";
160
+ scroll: boolean;
161
+ targetHref: string;
162
+ };
163
+ type EarlyNavigationIntentDecisionV0 = {
164
+ kind: "sameDocumentScroll";
165
+ hash: string;
166
+ mode: "push" | "replace";
167
+ scroll: boolean;
168
+ trace: NavigationTrace;
169
+ } | {
170
+ kind: "flightNavigation";
171
+ bypassNavigationCache: boolean;
172
+ trace: NavigationTrace;
173
+ };
117
174
  type NavigationPlannerInput = {
118
175
  routeManifest: RouteManifest | null;
119
176
  state: NavigationPlannerStateV0;
@@ -122,6 +179,8 @@ type NavigationPlannerInput = {
122
179
  type AcceptedCacheEntryReuseDecision = Extract<CacheEntryReuseDecision, {
123
180
  canReuse: true;
124
181
  }>;
182
+ declare function classifyRscFetchResult(facts: RscFetchResultFactsV0): RscFetchResultDecisionV0;
183
+ declare function classifyEarlyNavigationIntent(facts: EarlyNavigationIntentFactsV0): EarlyNavigationIntentDecisionV0;
125
184
  declare function classifyRootBoundaryTransition(currentRootBoundaryId: string | null, nextRootBoundaryId: string | null): RootBoundaryTransition;
126
185
  declare function resolveSameLayoutAncestorPersistence(currentSnapshot: RouteSnapshotV0, targetSnapshot: RouteSnapshotV0): readonly string[];
127
186
  declare function resolveMountedParallelSlotPersistence(currentSnapshot: RouteSnapshotV0, targetSnapshot: RouteSnapshotV0): readonly string[];
@@ -143,6 +202,8 @@ declare function resolveDefaultOrUnmatchedSlotPersistenceForLayouts(options: {
143
202
  }): readonly string[];
144
203
  declare function planNavigation(input: NavigationPlannerInput): NavigationDecisionV0;
145
204
  declare const navigationPlanner: {
205
+ classifyEarlyNavigationIntent: typeof classifyEarlyNavigationIntent;
206
+ classifyRscFetchResult: typeof classifyRscFetchResult;
146
207
  classifyRootBoundaryTransition: typeof classifyRootBoundaryTransition;
147
208
  plan: typeof planNavigation;
148
209
  resolveCurrentRootBoundaryElementPersistence: typeof resolveCurrentRootBoundaryElementPersistence;
@@ -150,4 +211,4 @@ declare const navigationPlanner: {
150
211
  resolveSameLayoutAncestorPersistence: typeof resolveSameLayoutAncestorPersistence;
151
212
  };
152
213
  //#endregion
153
- export { FlightResultV0, InterceptionSnapshotV0, MountedParallelSlotSnapshotV0, NavigationDecisionV0, NavigationEvent, NavigationPlannerInput, NavigationPlannerStateV0, OperationLane, OperationToken, ParallelSlotBindingSnapshotV0, RefreshScope, RootBoundaryTransition, RouteSnapshotV0, TraverseDirection, navigationPlanner, resolveDefaultOrUnmatchedSlotPersistenceForLayouts };
214
+ export { EarlyNavigationIntentDecisionV0, EarlyNavigationIntentFactsV0, FlightResultV0, InterceptionSnapshotV0, MountedParallelSlotSnapshotV0, NavigationDecisionV0, NavigationEvent, NavigationPlannerInput, NavigationPlannerStateV0, OperationLane, OperationToken, ParallelSlotBindingSnapshotV0, RefreshScope, RootBoundaryTransition, RouteSnapshotV0, RscFetchResultDecisionV0, RscFetchResultFactsV0, TraverseDirection, navigationPlanner, resolveDefaultOrUnmatchedSlotPersistenceForLayouts };
@@ -1,7 +1,10 @@
1
1
  import { splitPathnameForRouteMatch } from "../routing/utils.js";
2
+ import { stripBasePath } from "../utils/base-path.js";
2
3
  import { matchRoutePattern, matchRoutePatternPrefix, matchRoutePatternWithOptionalDynamicSegments } from "../routing/route-pattern.js";
3
4
  import { compareAppElementsSlotIds } from "./app-elements-wire.js";
4
5
  import "./app-elements.js";
6
+ import { resolveHardNavigationTargetFromRscResponse, resolveRscCompatibilityNavigationDecision } from "./app-rsc-cache-busting.js";
7
+ import { resolveRscRedirectLifecycleHop, resolveStreamedRscRedirectLifecycleHop } from "./app-browser-rsc-redirect.js";
5
8
  import { NavigationTraceReasonCodes, createNavigationLifecycleTraceFields, createNavigationTrace } from "./navigation-trace.js";
6
9
  //#region src/server/navigation-planner.ts
7
10
  const ROUTE_INTERCEPTION_CONTEXT_SEPARATOR = "\0";
@@ -27,6 +30,189 @@ function getRequestedWorkTargetHref(work) {
27
30
  default: throw new Error("[vinext] Unknown requested navigation work: " + String(work));
28
31
  }
29
32
  }
33
+ function createRscFetchResultTraceFields(facts, fields = {}) {
34
+ return {
35
+ fetchResultSource: facts.source,
36
+ ...fields
37
+ };
38
+ }
39
+ function createRscFetchResultHardNavigationDecision(options) {
40
+ return {
41
+ discardBody: options.discardBody,
42
+ kind: "hardNavigate",
43
+ reason: options.reason,
44
+ trace: createNavigationTrace(options.reasonCode, createRscFetchResultTraceFields(options.facts, {
45
+ ...options.redirectSignal !== void 0 ? { redirectSignal: options.redirectSignal } : {},
46
+ redirectDepth: options.facts.redirectDepth,
47
+ targetHref: options.url
48
+ })),
49
+ url: options.url
50
+ };
51
+ }
52
+ function createRscFetchResultFollowRedirectDecision(options) {
53
+ return {
54
+ discardBody: options.discardBody,
55
+ kind: "followRedirect",
56
+ redirect: options.redirect,
57
+ trace: createNavigationTrace(NavigationTraceReasonCodes.redirectFollow, createRscFetchResultTraceFields(options.facts, {
58
+ redirectDepth: options.redirect.redirectDepth,
59
+ redirectSignal: options.redirectSignal,
60
+ targetHref: options.redirect.href
61
+ }))
62
+ };
63
+ }
64
+ function mapRscRedirectTerminalReason(reason) {
65
+ switch (reason) {
66
+ case "externalRedirect": return {
67
+ hardNavigationReason: "externalRedirectTarget",
68
+ traceReasonCode: NavigationTraceReasonCodes.redirectTerminalExternal
69
+ };
70
+ case "maxRedirectsExceeded": return {
71
+ hardNavigationReason: "redirectDepthExhausted",
72
+ traceReasonCode: NavigationTraceReasonCodes.redirectTerminalDepth
73
+ };
74
+ default: throw new Error("[vinext] Unknown RSC redirect terminal reason: " + String(reason));
75
+ }
76
+ }
77
+ function classifyRscFetchResult(facts) {
78
+ if (!facts.responseOk || !facts.isRscContentType || !facts.hasBody) {
79
+ const url = resolveHardNavigationTargetFromRscResponse(facts.responseUrl, facts.currentHref, facts.origin);
80
+ return createRscFetchResultHardNavigationDecision({
81
+ discardBody: false,
82
+ facts,
83
+ reason: "invalidRscPayload",
84
+ reasonCode: NavigationTraceReasonCodes.invalidRscPayload,
85
+ url
86
+ });
87
+ }
88
+ const compatibilityDecision = resolveRscCompatibilityNavigationDecision({
89
+ clientCompatibilityId: facts.clientCompatibilityId,
90
+ currentHref: facts.currentHref,
91
+ origin: facts.origin,
92
+ responseCompatibilityId: facts.compatibilityIdHeader,
93
+ responseUrl: facts.responseUrl
94
+ });
95
+ if (compatibilityDecision.kind === "hard-navigate") return createRscFetchResultHardNavigationDecision({
96
+ discardBody: false,
97
+ facts,
98
+ reason: "rscCompatibilityMismatch",
99
+ reasonCode: NavigationTraceReasonCodes.rscCompatibilityMismatch,
100
+ url: compatibilityDecision.hardNavigationTarget
101
+ });
102
+ if (facts.responseUrl !== null) {
103
+ const redirectDecision = resolveRscRedirectLifecycleHop({
104
+ currentHref: facts.currentHref,
105
+ historyUpdateMode: facts.effectiveHistoryUpdateMode,
106
+ origin: facts.origin,
107
+ redirectDepth: facts.redirectDepth,
108
+ requestPreviousNextUrl: facts.requestPreviousNextUrl,
109
+ responseUrl: facts.responseUrl
110
+ });
111
+ if (redirectDecision.kind === "terminal-hard-navigation") {
112
+ const terminalReason = mapRscRedirectTerminalReason(redirectDecision.reason);
113
+ return createRscFetchResultHardNavigationDecision({
114
+ discardBody: false,
115
+ facts,
116
+ reason: terminalReason.hardNavigationReason,
117
+ reasonCode: terminalReason.traceReasonCode,
118
+ redirectSignal: "response-url",
119
+ url: redirectDecision.href
120
+ });
121
+ }
122
+ if (redirectDecision.kind === "follow") return createRscFetchResultFollowRedirectDecision({
123
+ discardBody: false,
124
+ facts,
125
+ redirect: {
126
+ href: redirectDecision.href,
127
+ historyUpdateMode: facts.effectiveHistoryUpdateMode,
128
+ previousNextUrl: redirectDecision.previousNextUrl,
129
+ redirectDepth: redirectDecision.redirectDepth
130
+ },
131
+ redirectSignal: "response-url"
132
+ });
133
+ }
134
+ if (facts.streamedRedirectTarget !== null) {
135
+ const redirectDecision = resolveStreamedRscRedirectLifecycleHop({
136
+ currentHref: facts.currentHref,
137
+ historyUpdateMode: facts.effectiveHistoryUpdateMode,
138
+ origin: facts.origin,
139
+ redirectDepth: facts.redirectDepth,
140
+ requestPreviousNextUrl: facts.requestPreviousNextUrl,
141
+ streamedRedirectTarget: facts.streamedRedirectTarget
142
+ });
143
+ if (redirectDecision.kind === "terminal-hard-navigation") {
144
+ const terminalReason = mapRscRedirectTerminalReason(redirectDecision.reason);
145
+ return createRscFetchResultHardNavigationDecision({
146
+ discardBody: true,
147
+ facts,
148
+ reason: terminalReason.hardNavigationReason,
149
+ reasonCode: terminalReason.traceReasonCode,
150
+ redirectSignal: "streamed-header",
151
+ url: redirectDecision.href
152
+ });
153
+ }
154
+ if (redirectDecision.kind === "follow") return createRscFetchResultFollowRedirectDecision({
155
+ discardBody: true,
156
+ facts,
157
+ redirect: {
158
+ href: redirectDecision.href,
159
+ historyUpdateMode: facts.effectiveHistoryUpdateMode,
160
+ previousNextUrl: redirectDecision.previousNextUrl,
161
+ redirectDepth: redirectDecision.redirectDepth
162
+ },
163
+ redirectSignal: "streamed-header"
164
+ });
165
+ return createRscFetchResultHardNavigationDecision({
166
+ discardBody: true,
167
+ facts,
168
+ reason: "streamedRedirectLoop",
169
+ reasonCode: NavigationTraceReasonCodes.streamedRedirectLoop,
170
+ redirectSignal: "streamed-header",
171
+ url: redirectDecision.href
172
+ });
173
+ }
174
+ return {
175
+ discardBody: false,
176
+ kind: "proceedToCommit",
177
+ trace: createNavigationTrace(NavigationTraceReasonCodes.proceedToCommit, createRscFetchResultTraceFields(facts))
178
+ };
179
+ }
180
+ function createEarlyNavigationIntentTrace(reasonCode, facts) {
181
+ return createNavigationTrace(reasonCode, { targetHref: facts.targetHref });
182
+ }
183
+ function classifyEarlyNavigationIntent(facts) {
184
+ let current;
185
+ let next;
186
+ try {
187
+ current = new URL(facts.currentHref);
188
+ next = new URL(facts.targetHref, facts.currentHref);
189
+ } catch {
190
+ return {
191
+ bypassNavigationCache: false,
192
+ kind: "flightNavigation",
193
+ trace: createEarlyNavigationIntentTrace(NavigationTraceReasonCodes.crossDocumentFlight, facts)
194
+ };
195
+ }
196
+ const samePathname = current.origin === next.origin && stripBasePath(current.pathname, facts.basePath) === stripBasePath(next.pathname, facts.basePath);
197
+ const sameSearch = current.searchParams.toString() === next.searchParams.toString();
198
+ if (samePathname && sameSearch && next.hash !== "") return {
199
+ hash: next.hash,
200
+ kind: "sameDocumentScroll",
201
+ mode: facts.mode,
202
+ scroll: facts.scroll,
203
+ trace: createEarlyNavigationIntentTrace(NavigationTraceReasonCodes.sameDocumentScroll, facts)
204
+ };
205
+ if (samePathname && !sameSearch) return {
206
+ bypassNavigationCache: true,
207
+ kind: "flightNavigation",
208
+ trace: createEarlyNavigationIntentTrace(NavigationTraceReasonCodes.samePageSearch, facts)
209
+ };
210
+ return {
211
+ bypassNavigationCache: false,
212
+ kind: "flightNavigation",
213
+ trace: createEarlyNavigationIntentTrace(NavigationTraceReasonCodes.crossDocumentFlight, facts)
214
+ };
215
+ }
30
216
  function createSnapshotRouteTopology(snapshot) {
31
217
  return {
32
218
  layoutIds: snapshot.layoutIds,
@@ -495,6 +681,8 @@ function planNavigation(input) {
495
681
  }
496
682
  }
497
683
  const navigationPlanner = {
684
+ classifyEarlyNavigationIntent,
685
+ classifyRscFetchResult,
498
686
  classifyRootBoundaryTransition,
499
687
  plan: planNavigation,
500
688
  resolveCurrentRootBoundaryElementPersistence,
@@ -4,6 +4,8 @@ type NavigationTraceSchemaVersion = 0;
4
4
  declare const NavigationTraceReasonCodes: {
5
5
  cacheProofRejected: "NC_CACHE_REJECT";
6
6
  commitCurrent: "NC_COMMIT";
7
+ crossDocumentFlight: "NC_CROSS_DOC_FLIGHT";
8
+ invalidRscPayload: "NC_RSC_INVALID";
7
9
  interceptedCommitCurrent: "NC_INTERCEPT_COMMIT";
8
10
  interceptedRejectedIncompatibleRoot: "NC_INTERCEPT_REJECT_ROOT";
9
11
  interceptedRejectedMissingProof: "NC_INTERCEPT_REJECT_MISSING_PROOF";
@@ -12,10 +14,18 @@ declare const NavigationTraceReasonCodes: {
12
14
  interceptedRejectedUndeclaredTopology: "NC_INTERCEPT_REJECT_GRAPH";
13
15
  interceptedRejectedUnknownSource: "NC_INTERCEPT_REJECT_SOURCE";
14
16
  prefetchOnly: "NC_PREFETCH_ONLY";
17
+ proceedToCommit: "NC_RSC_PROCEED";
18
+ redirectFollow: "NC_RSC_REDIRECT_FOLLOW";
19
+ redirectTerminalDepth: "NC_RSC_REDIRECT_DEPTH";
20
+ redirectTerminalExternal: "NC_RSC_REDIRECT_EXTERNAL";
15
21
  requestWork: "NC_REQUEST";
16
22
  rootBoundaryChanged: "NC_ROOT";
17
23
  rootBoundaryUnknown: "NC_ROOT_UNKNOWN";
24
+ rscCompatibilityMismatch: "NC_RSC_COMPAT_MISMATCH";
25
+ sameDocumentScroll: "NC_SAME_DOC_SCROLL";
26
+ samePageSearch: "NC_SAME_PAGE_SEARCH";
18
27
  staleOperation: "NC_STALE";
28
+ streamedRedirectLoop: "NC_RSC_STREAMED_REDIRECT_LOOP";
19
29
  };
20
30
  declare const NavigationTraceTransactionCodes: {
21
31
  hardNavigate: "NT_HARD_NAVIGATE";
@@ -25,7 +35,7 @@ declare const NavigationTraceTransactionCodes: {
25
35
  type NavigationTraceReasonCode = (typeof NavigationTraceReasonCodes)[keyof typeof NavigationTraceReasonCodes];
26
36
  type NavigationTraceTransactionCode = (typeof NavigationTraceTransactionCodes)[keyof typeof NavigationTraceTransactionCodes];
27
37
  type NavigationTraceCode = NavigationTraceReasonCode | NavigationTraceTransactionCode;
28
- type NavigationTraceFieldName = "activeNavigationId" | "cacheProofCode" | "cacheProofMode" | "cacheProofReuseClass" | "cacheProofScope" | "currentRootLayoutTreePath" | "currentVisibleCommitVersion" | "nextRootLayoutTreePath" | "eventKind" | "operationLane" | "pendingOperationId" | "startedVisibleCommitVersion" | "startedNavigationId" | "targetHref" | "traverseDirection";
38
+ type NavigationTraceFieldName = "activeNavigationId" | "cacheProofCode" | "cacheProofMode" | "cacheProofReuseClass" | "cacheProofScope" | "currentRootLayoutTreePath" | "currentVisibleCommitVersion" | "nextRootLayoutTreePath" | "eventKind" | "fetchResultSource" | "operationLane" | "pendingOperationId" | "redirectDepth" | "redirectSignal" | "startedVisibleCommitVersion" | "startedNavigationId" | "targetHref" | "traverseDirection";
29
39
  type NavigationTraceFieldValue = string | number | boolean | null;
30
40
  type NavigationTraceFields = Readonly<Partial<Record<NavigationTraceFieldName, NavigationTraceFieldValue>>>;
31
41
  type NavigationTraceEntry = Readonly<{
@@ -3,6 +3,8 @@ const NAVIGATION_TRACE_SCHEMA_VERSION = 0;
3
3
  const NavigationTraceReasonCodes = {
4
4
  cacheProofRejected: "NC_CACHE_REJECT",
5
5
  commitCurrent: "NC_COMMIT",
6
+ crossDocumentFlight: "NC_CROSS_DOC_FLIGHT",
7
+ invalidRscPayload: "NC_RSC_INVALID",
6
8
  interceptedCommitCurrent: "NC_INTERCEPT_COMMIT",
7
9
  interceptedRejectedIncompatibleRoot: "NC_INTERCEPT_REJECT_ROOT",
8
10
  interceptedRejectedMissingProof: "NC_INTERCEPT_REJECT_MISSING_PROOF",
@@ -11,10 +13,18 @@ const NavigationTraceReasonCodes = {
11
13
  interceptedRejectedUndeclaredTopology: "NC_INTERCEPT_REJECT_GRAPH",
12
14
  interceptedRejectedUnknownSource: "NC_INTERCEPT_REJECT_SOURCE",
13
15
  prefetchOnly: "NC_PREFETCH_ONLY",
16
+ proceedToCommit: "NC_RSC_PROCEED",
17
+ redirectFollow: "NC_RSC_REDIRECT_FOLLOW",
18
+ redirectTerminalDepth: "NC_RSC_REDIRECT_DEPTH",
19
+ redirectTerminalExternal: "NC_RSC_REDIRECT_EXTERNAL",
14
20
  requestWork: "NC_REQUEST",
15
21
  rootBoundaryChanged: "NC_ROOT",
16
22
  rootBoundaryUnknown: "NC_ROOT_UNKNOWN",
17
- staleOperation: "NC_STALE"
23
+ rscCompatibilityMismatch: "NC_RSC_COMPAT_MISMATCH",
24
+ sameDocumentScroll: "NC_SAME_DOC_SCROLL",
25
+ samePageSearch: "NC_SAME_PAGE_SEARCH",
26
+ staleOperation: "NC_STALE",
27
+ streamedRedirectLoop: "NC_RSC_STREAMED_REDIRECT_LOOP"
18
28
  };
19
29
  const NavigationTraceTransactionCodes = {
20
30
  hardNavigate: "NT_HARD_NAVIGATE",
@@ -1,12 +1,4 @@
1
1
  //#region src/server/normalize-path.d.ts
2
- /**
3
- * Re-encode path delimiter characters that were decoded by decodeURIComponent.
4
- * After decoding a URL segment, characters like / # ? \ need to be re-encoded
5
- * so they don't change the path structure.
6
- *
7
- * Ported from Next.js: packages/next/src/shared/lib/router/utils/escape-path-delimiters.ts
8
- * https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/escape-path-delimiters.ts
9
- */
10
2
  declare function escapePathDelimiters(segment: string, escapeEncoded?: boolean): string;
11
3
  /**
12
4
  * Decode a URL pathname segment-by-segment, preserving encoded path delimiters.
@@ -7,8 +7,10 @@
7
7
  * Ported from Next.js: packages/next/src/shared/lib/router/utils/escape-path-delimiters.ts
8
8
  * https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/escape-path-delimiters.ts
9
9
  */
10
+ const PATH_DELIMITERS_RE = /([/#?])/gi;
11
+ const PATH_DELIMITERS_ENCODED_RE = /([/#?]|%(2f|23|3f|5c))/gi;
10
12
  function escapePathDelimiters(segment, escapeEncoded) {
11
- return segment.replace(new RegExp(`([/#?]${escapeEncoded ? "|%(2f|23|3f|5c)" : ""})`, "gi"), (char) => encodeURIComponent(char));
13
+ return segment.replace(escapeEncoded ? PATH_DELIMITERS_ENCODED_RE : PATH_DELIMITERS_RE, (char) => encodeURIComponent(char));
12
14
  }
13
15
  /**
14
16
  * Decode a URL pathname segment-by-segment, preserving encoded path delimiters.
@@ -0,0 +1,45 @@
1
+ //#region src/server/otel-tracer-extension.d.ts
2
+ /**
3
+ * OpenTelemetry tracer provider extension for Cache Components.
4
+ *
5
+ * When `cacheComponents: true` is enabled in next.config, component renders
6
+ * go through multiple phases (warmup → resume). During these phases, the
7
+ * `workUnitAsyncStorage` carries a prerender or cache store. Without this
8
+ * extension, calls to `tracer.startSpan()` / `tracer.startActiveSpan()` from
9
+ * inside user RSC code would inherit that prerender context, causing:
10
+ *
11
+ * 1. Spans to reuse the same trace ID across requests (the frozen prerender
12
+ * context bleeds into the runtime resume render).
13
+ * 2. Spans not being created at all during fallback resume (the work unit
14
+ * context gates span creation in some OTel SDK implementations).
15
+ *
16
+ * The fix mirrors Next.js's `instrumentation-node-extensions.ts`:
17
+ * - Wrap `tracer.startSpan` to exit `workUnitAsyncStorage` before creating
18
+ * the span, ensuring a clean context for span ID generation.
19
+ * - Wrap `tracer.startActiveSpan` similarly and re-enter the work unit store
20
+ * for the callback, so that the callback runs with the correct request
21
+ * context restored.
22
+ *
23
+ * This extension is intentionally a no-op when:
24
+ * - `@opentelemetry/api` is not installed (graceful degradation).
25
+ * - No OTel tracer provider has been registered (provider is the noop provider).
26
+ * - `workUnitAsyncStorage` has no active store (non-render contexts).
27
+ *
28
+ * References:
29
+ * - packages/next/src/server/lib/router-utils/instrumentation-node-extensions.ts
30
+ * - https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/router-utils/instrumentation-node-extensions.ts
31
+ */
32
+ /**
33
+ * Extend the registered OTel tracer provider so that `startSpan` and
34
+ * `startActiveSpan` exit the `workUnitAsyncStorage` context before creating
35
+ * spans. This prevents the prerender/cache work unit store from leaking into
36
+ * span ID generation during Cache Component fallback resumes.
37
+ *
38
+ * Safe to call multiple times — subsequent calls are no-ops once the provider
39
+ * has been wrapped.
40
+ *
41
+ * Must only be called in Node.js environments (not Edge runtime).
42
+ */
43
+ declare function extendTracerProviderForCacheComponents(): void;
44
+ //#endregion
45
+ export { extendTracerProviderForCacheComponents };
@@ -0,0 +1,89 @@
1
+ import { workUnitAsyncStorage } from "../shims/internal/work-unit-async-storage.js";
2
+ //#region src/server/otel-tracer-extension.ts
3
+ /**
4
+ * OpenTelemetry tracer provider extension for Cache Components.
5
+ *
6
+ * When `cacheComponents: true` is enabled in next.config, component renders
7
+ * go through multiple phases (warmup → resume). During these phases, the
8
+ * `workUnitAsyncStorage` carries a prerender or cache store. Without this
9
+ * extension, calls to `tracer.startSpan()` / `tracer.startActiveSpan()` from
10
+ * inside user RSC code would inherit that prerender context, causing:
11
+ *
12
+ * 1. Spans to reuse the same trace ID across requests (the frozen prerender
13
+ * context bleeds into the runtime resume render).
14
+ * 2. Spans not being created at all during fallback resume (the work unit
15
+ * context gates span creation in some OTel SDK implementations).
16
+ *
17
+ * The fix mirrors Next.js's `instrumentation-node-extensions.ts`:
18
+ * - Wrap `tracer.startSpan` to exit `workUnitAsyncStorage` before creating
19
+ * the span, ensuring a clean context for span ID generation.
20
+ * - Wrap `tracer.startActiveSpan` similarly and re-enter the work unit store
21
+ * for the callback, so that the callback runs with the correct request
22
+ * context restored.
23
+ *
24
+ * This extension is intentionally a no-op when:
25
+ * - `@opentelemetry/api` is not installed (graceful degradation).
26
+ * - No OTel tracer provider has been registered (provider is the noop provider).
27
+ * - `workUnitAsyncStorage` has no active store (non-render contexts).
28
+ *
29
+ * References:
30
+ * - packages/next/src/server/lib/router-utils/instrumentation-node-extensions.ts
31
+ * - https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/router-utils/instrumentation-node-extensions.ts
32
+ */
33
+ const extendedProviders = /* @__PURE__ */ new WeakSet();
34
+ const USE_CACHE_FUNCTION_SYMBOL = Symbol.for("vinext.useCacheFunction");
35
+ function isUseCacheFn(fn) {
36
+ return typeof fn === "function" && fn[USE_CACHE_FUNCTION_SYMBOL] === true;
37
+ }
38
+ /**
39
+ * Extend the registered OTel tracer provider so that `startSpan` and
40
+ * `startActiveSpan` exit the `workUnitAsyncStorage` context before creating
41
+ * spans. This prevents the prerender/cache work unit store from leaking into
42
+ * span ID generation during Cache Component fallback resumes.
43
+ *
44
+ * Safe to call multiple times — subsequent calls are no-ops once the provider
45
+ * has been wrapped.
46
+ *
47
+ * Must only be called in Node.js environments (not Edge runtime).
48
+ */
49
+ function extendTracerProviderForCacheComponents() {
50
+ let api;
51
+ try {
52
+ const req = globalThis.require;
53
+ if (typeof req === "function") api = req("@opentelemetry/api");
54
+ } catch {
55
+ return;
56
+ }
57
+ if (!api) return;
58
+ const provider = api.trace.getTracerProvider();
59
+ if (!provider || typeof provider.getTracer !== "function") return;
60
+ if (extendedProviders.has(provider)) return;
61
+ extendedProviders.add(provider);
62
+ const originalGetTracer = provider.getTracer.bind(provider);
63
+ const wrappedTracers = /* @__PURE__ */ new WeakSet();
64
+ provider.getTracer = (...args) => {
65
+ const tracer = originalGetTracer(...args);
66
+ if (!tracer || wrappedTracers.has(tracer)) return tracer;
67
+ const originalStartSpan = tracer.startSpan;
68
+ if (typeof originalStartSpan === "function") tracer.startSpan = (...startSpanArgs) => workUnitAsyncStorage.exit(() => originalStartSpan.apply(tracer, startSpanArgs));
69
+ const originalStartActiveSpan = tracer.startActiveSpan;
70
+ if (typeof originalStartActiveSpan === "function") tracer.startActiveSpan = (...startActiveSpanArgs) => {
71
+ const workUnitStore = workUnitAsyncStorage.getStore();
72
+ if (!workUnitStore) return originalStartActiveSpan.apply(tracer, startActiveSpanArgs);
73
+ let fnIdx = 0;
74
+ if (startActiveSpanArgs.length === 2 && typeof startActiveSpanArgs[1] === "function") fnIdx = 1;
75
+ else if (startActiveSpanArgs.length === 3 && typeof startActiveSpanArgs[2] === "function") fnIdx = 2;
76
+ else if (startActiveSpanArgs.length > 3 && typeof startActiveSpanArgs[3] === "function") fnIdx = 3;
77
+ if (fnIdx > 0) {
78
+ const originalFn = startActiveSpanArgs[fnIdx];
79
+ if (isUseCacheFn(originalFn)) console.error("A Cache Function (`use cache`) was passed to startActiveSpan which means it will receive a Span argument with a possibly random ID on every invocation leading to cache misses. Provide a wrapping function around the Cache Function that does not forward the Span argument to avoid this issue.");
80
+ startActiveSpanArgs[fnIdx] = (...cbArgs) => workUnitAsyncStorage.run(workUnitStore, originalFn, ...cbArgs);
81
+ }
82
+ return workUnitAsyncStorage.exit(() => originalStartActiveSpan.apply(tracer, startActiveSpanArgs));
83
+ };
84
+ wrappedTracers.add(tracer);
85
+ return tracer;
86
+ };
87
+ }
88
+ //#endregion
89
+ export { extendTracerProviderForCacheComponents };
@@ -12,8 +12,7 @@ type PagesApiRouteConfig = {
12
12
  * `bodyParser: false` is critical for webhook handlers (Stripe, GitHub,
13
13
  * Slack, etc.) that need to read the raw bytes to verify an HMAC
14
14
  * signature. With it set, `req.body` is left undefined and the raw stream
15
- * is exposed on `req.body` as a Web `ReadableStream<Uint8Array>` so user
16
- * code can consume it.
15
+ * remains available through the Node-readable request object.
17
16
  *
18
17
  * @see https://nextjs.org/docs/pages/building-your-application/routing/api-routes#custom-config
19
18
  */
@@ -22,9 +21,21 @@ type PagesApiRouteConfig = {
22
21
  sizeLimit?: string | number;
23
22
  };
24
23
  responseLimit?: boolean | string | number;
24
+ /**
25
+ * `externalResolver: true` declares that the response is sent by an
26
+ * external resolver (e.g. express/connect proxy middleware) that may
27
+ * complete after the handler's promise settles. Next.js uses it to
28
+ * suppress the "API resolved without sending a response" dev warning;
29
+ * vinext additionally uses it to suppress the auto-`end()` safety net,
30
+ * which would otherwise resolve an empty response before the external
31
+ * resolver writes.
32
+ *
33
+ * @see https://nextjs.org/docs/pages/building-your-application/routing/api-routes#custom-config
34
+ */
35
+ externalResolver?: boolean;
25
36
  };
26
37
  };
27
- type PagesNodeApiRouteHandler = (req: PagesReqResRequest, res: PagesReqResResponse) => void | Promise<void>;
38
+ type PagesNodeApiRouteHandler = (req: PagesReqResRequest, res: PagesReqResResponse) => unknown;
28
39
  type PagesEdgeApiRouteHandler = (request: Request) => Response | Promise<Response>;
29
40
  type PagesApiRouteModule = {
30
41
  /**
@@ -43,8 +43,13 @@ async function _handlePagesApiRoute(options) {
43
43
  request: options.request,
44
44
  url: options.url
45
45
  });
46
+ let resWasPiped = false;
47
+ res.once("pipe", () => {
48
+ resWasPiped = true;
49
+ });
50
+ const externalResolver = route.module.config?.api?.externalResolver || false;
46
51
  await route.module.default(req, res);
47
- res.end();
52
+ if (!externalResolver && !resWasPiped && !res.headersSent) res.end();
48
53
  return await responsePromise;
49
54
  } catch (error) {
50
55
  if (error instanceof PagesBodyParseError) return new Response(error.message, {
@@ -23,11 +23,14 @@ declare function resolveSsrManifest(manifest: Record<string, string[]> | null |
23
23
  */
24
24
  declare function getManifestFilesForModule(manifest: Record<string, string[]> | null | undefined, moduleId: string | null | undefined): string[] | null;
25
25
  /**
26
- * Find the first `.js` file in the manifest for `moduleId` and return its
27
- * URL-path form (with a leading `/`). Used to resolve the hydration URL for
28
- * the matched page or the `_app` module.
26
+ * Find the first `.js` file in the manifest for `moduleId` and return the URL it
27
+ * is actually SERVED from. Used to resolve the client-navigation / hydration URL
28
+ * for the matched page or the `_app` module (it is `import()`ed on the client),
29
+ * so it must point at the served location: `assetPrefix` replaces `basePath` for
30
+ * asset URLs. SSR-manifest values are base-anchored; re-anchor under any
31
+ * configured `assetPrefix` (default `""` keeps the legacy `"/" + file`).
29
32
  */
30
- declare function resolveClientModuleUrl(manifest: Record<string, string[]> | null | undefined, moduleId: string | null | undefined): string | undefined;
33
+ declare function resolveClientModuleUrl(manifest: Record<string, string[]> | null | undefined, moduleId: string | null | undefined, basePath?: string, assetPrefix?: string): string | undefined;
31
34
  type CollectAssetTagsOptions = {
32
35
  /**
33
36
  * SSR manifest mapping module file paths to their associated asset list.
@@ -46,6 +49,14 @@ type CollectAssetTagsOptions = {
46
49
  * default.
47
50
  */
48
51
  disableOptimizedLoading: boolean;
52
+ /**
53
+ * Configured `basePath` / `assetPrefix`. SSR-manifest values are base-anchored
54
+ * (needed for the lazy-chunk membership test), but the EMITTED href must point
55
+ * where the asset is actually served — `assetPrefix` replaces `basePath` for
56
+ * asset URLs. Default `""` (both unset) keeps the legacy `"/" + value` href.
57
+ */
58
+ basePath?: string;
59
+ assetPrefix?: string;
49
60
  };
50
61
  /**
51
62
  * Build the HTML `<link>` and `<script>` tag string for the SSR response.