vinext 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/build/assets-ignore.d.ts +32 -0
  2. package/dist/build/assets-ignore.js +48 -0
  3. package/dist/build/client-build-config.d.ts +27 -1
  4. package/dist/build/client-build-config.js +58 -1
  5. package/dist/cli.js +2 -0
  6. package/dist/client/navigation-runtime.d.ts +8 -0
  7. package/dist/client/navigation-runtime.js +1 -1
  8. package/dist/client/vinext-next-data.d.ts +2 -1
  9. package/dist/config/config-matchers.d.ts +20 -1
  10. package/dist/config/config-matchers.js +35 -1
  11. package/dist/config/next-config.d.ts +16 -3
  12. package/dist/config/next-config.js +30 -2
  13. package/dist/deploy.js +40 -304
  14. package/dist/entries/app-rsc-entry.d.ts +8 -2
  15. package/dist/entries/app-rsc-entry.js +54 -4
  16. package/dist/entries/app-rsc-manifest.js +20 -2
  17. package/dist/entries/pages-server-entry.js +9 -1
  18. package/dist/index.js +162 -217
  19. package/dist/plugins/postcss.js +18 -14
  20. package/dist/plugins/require-context.d.ts +6 -0
  21. package/dist/plugins/require-context.js +184 -0
  22. package/dist/routing/app-route-graph.d.ts +12 -1
  23. package/dist/routing/app-route-graph.js +137 -5
  24. package/dist/routing/route-pattern.d.ts +2 -1
  25. package/dist/routing/route-pattern.js +16 -1
  26. package/dist/server/api-handler.js +4 -0
  27. package/dist/server/app-browser-entry.js +84 -39
  28. package/dist/server/app-browser-interception-context.d.ts +2 -1
  29. package/dist/server/app-browser-interception-context.js +15 -2
  30. package/dist/server/app-browser-navigation-controller.d.ts +11 -1
  31. package/dist/server/app-browser-navigation-controller.js +77 -1
  32. package/dist/server/app-browser-popstate.d.ts +12 -3
  33. package/dist/server/app-browser-popstate.js +19 -4
  34. package/dist/server/app-browser-state.d.ts +3 -0
  35. package/dist/server/app-browser-state.js +6 -3
  36. package/dist/server/app-browser-visible-commit.js +9 -7
  37. package/dist/server/app-history-state.d.ts +45 -1
  38. package/dist/server/app-history-state.js +109 -1
  39. package/dist/server/app-page-boundary-render.js +41 -19
  40. package/dist/server/app-page-dispatch.d.ts +6 -0
  41. package/dist/server/app-page-dispatch.js +3 -1
  42. package/dist/server/app-page-element-builder.d.ts +1 -0
  43. package/dist/server/app-page-element-builder.js +22 -10
  44. package/dist/server/app-page-render.d.ts +6 -0
  45. package/dist/server/app-page-render.js +5 -3
  46. package/dist/server/app-page-request.d.ts +8 -6
  47. package/dist/server/app-page-request.js +12 -9
  48. package/dist/server/app-page-response.d.ts +2 -2
  49. package/dist/server/app-page-response.js +1 -1
  50. package/dist/server/app-page-route-wiring.js +2 -1
  51. package/dist/server/app-page-stream.d.ts +37 -2
  52. package/dist/server/app-page-stream.js +36 -3
  53. package/dist/server/app-pages-bridge.d.ts +16 -0
  54. package/dist/server/app-pages-bridge.js +23 -3
  55. package/dist/server/app-route-handler-cache.d.ts +1 -0
  56. package/dist/server/app-route-handler-cache.js +1 -0
  57. package/dist/server/app-route-handler-dispatch.d.ts +1 -0
  58. package/dist/server/app-route-handler-dispatch.js +2 -0
  59. package/dist/server/app-route-handler-execution.d.ts +1 -0
  60. package/dist/server/app-route-handler-execution.js +1 -0
  61. package/dist/server/app-route-handler-runtime.d.ts +1 -0
  62. package/dist/server/app-route-handler-runtime.js +3 -2
  63. package/dist/server/app-rsc-handler.d.ts +1 -0
  64. package/dist/server/app-rsc-handler.js +4 -3
  65. package/dist/server/app-rsc-route-matching.d.ts +20 -1
  66. package/dist/server/app-rsc-route-matching.js +29 -4
  67. package/dist/server/app-server-action-execution.d.ts +11 -1
  68. package/dist/server/app-server-action-execution.js +68 -10
  69. package/dist/server/app-ssr-entry.d.ts +6 -0
  70. package/dist/server/app-ssr-entry.js +17 -1
  71. package/dist/server/dev-server.d.ts +1 -1
  72. package/dist/server/dev-server.js +54 -31
  73. package/dist/server/isr-cache.d.ts +37 -1
  74. package/dist/server/isr-cache.js +85 -1
  75. package/dist/server/navigation-planner.js +5 -3
  76. package/dist/server/navigation-trace.d.ts +1 -1
  77. package/dist/server/pages-node-compat.d.ts +2 -0
  78. package/dist/server/pages-node-compat.js +4 -0
  79. package/dist/server/pages-page-data.d.ts +10 -7
  80. package/dist/server/pages-page-data.js +4 -2
  81. package/dist/server/pages-page-handler.d.ts +9 -2
  82. package/dist/server/pages-page-handler.js +29 -16
  83. package/dist/server/pages-page-response.d.ts +11 -2
  84. package/dist/server/pages-page-response.js +8 -1
  85. package/dist/server/pages-readiness.d.ts +36 -0
  86. package/dist/server/pages-readiness.js +21 -0
  87. package/dist/server/pages-request-pipeline.d.ts +99 -0
  88. package/dist/server/pages-request-pipeline.js +209 -0
  89. package/dist/server/pages-revalidate.d.ts +15 -0
  90. package/dist/server/pages-revalidate.js +19 -0
  91. package/dist/server/prod-server.d.ts +6 -2
  92. package/dist/server/prod-server.js +101 -217
  93. package/dist/server/socket-error-backstop.d.ts +19 -1
  94. package/dist/server/socket-error-backstop.js +77 -4
  95. package/dist/shims/app-router-scroll.js +22 -4
  96. package/dist/shims/cache-runtime.js +31 -1
  97. package/dist/shims/error-boundary.d.ts +21 -11
  98. package/dist/shims/error-boundary.js +8 -1
  99. package/dist/shims/fetch-cache.d.ts +14 -1
  100. package/dist/shims/fetch-cache.js +18 -1
  101. package/dist/shims/hash-scroll.d.ts +1 -0
  102. package/dist/shims/hash-scroll.js +3 -1
  103. package/dist/shims/internal/link-status-registry.d.ts +43 -0
  104. package/dist/shims/internal/link-status-registry.js +42 -0
  105. package/dist/shims/internal/route-pattern-for-warning.d.ts +27 -0
  106. package/dist/shims/internal/route-pattern-for-warning.js +40 -0
  107. package/dist/shims/internal/utils.d.ts +1 -0
  108. package/dist/shims/link.js +20 -6
  109. package/dist/shims/navigation.d.ts +2 -2
  110. package/dist/shims/navigation.js +63 -7
  111. package/dist/shims/router-state.d.ts +1 -0
  112. package/dist/shims/router-state.js +2 -0
  113. package/dist/shims/router.d.ts +6 -3
  114. package/dist/shims/router.js +128 -21
  115. package/dist/utils/client-build-manifest.d.ts +8 -1
  116. package/dist/utils/client-build-manifest.js +30 -5
  117. package/dist/utils/client-entry-manifest.d.ts +11 -0
  118. package/dist/utils/client-entry-manifest.js +29 -0
  119. package/package.json +5 -1
@@ -90,8 +90,8 @@ async function validateAppPageDynamicParams(options) {
90
90
  * `setNavigationContext` + element build + Response wrap) and the server-action
91
91
  * POST path (entries/app-rsc-entry.ts), which runs its own response pipeline.
92
92
  */
93
- function resolveAppPageInterceptMatch(options) {
94
- const interceptState = resolveAppPageInterceptState(options);
93
+ async function resolveAppPageInterceptMatch(options) {
94
+ const interceptState = await resolveAppPageInterceptState(options);
95
95
  if (interceptState.kind !== "source-route") return null;
96
96
  return {
97
97
  interceptOpts: options.toInterceptOpts(interceptState.intercept),
@@ -100,11 +100,12 @@ function resolveAppPageInterceptMatch(options) {
100
100
  sourceRoute: interceptState.sourceRoute
101
101
  };
102
102
  }
103
- function resolveAppPageInterceptState(options) {
103
+ async function resolveAppPageInterceptState(options) {
104
104
  if (!options.isRscRequest) return { kind: "none" };
105
105
  const intercept = options.findIntercept(options.cleanPathname);
106
106
  if (!intercept) return { kind: "none" };
107
- const sourceRoute = options.getSourceRoute(intercept.sourceRouteIndex);
107
+ if (intercept.__pageLoader && intercept.page == null) intercept.page = await intercept.__pageLoader();
108
+ const sourceRoute = await options.getSourceRoute(intercept.sourceRouteIndex);
108
109
  if (!sourceRoute) return { kind: "none" };
109
110
  if (sourceRoute === options.currentRoute) return {
110
111
  kind: "current-route",
@@ -116,8 +117,8 @@ function resolveAppPageInterceptState(options) {
116
117
  sourceRoute
117
118
  };
118
119
  }
119
- function resolveAppPageInterceptionRerenderTarget(options) {
120
- const interceptState = resolveAppPageInterceptState({
120
+ async function resolveAppPageInterceptionRerenderTarget(options) {
121
+ const interceptState = await resolveAppPageInterceptState({
121
122
  cleanPathname: options.cleanPathname,
122
123
  currentRoute: options.currentRoute,
123
124
  findIntercept: options.findIntercept,
@@ -143,7 +144,7 @@ function resolveAppPageActionRerenderTarget(options) {
143
144
  return resolveAppPageInterceptionRerenderTarget(options);
144
145
  }
145
146
  async function resolveAppPageIntercept(options) {
146
- const interceptState = resolveAppPageInterceptState({
147
+ const interceptState = await resolveAppPageInterceptState({
147
148
  cleanPathname: options.cleanPathname,
148
149
  currentRoute: options.currentRoute,
149
150
  findIntercept: options.findIntercept,
@@ -153,15 +154,17 @@ async function resolveAppPageIntercept(options) {
153
154
  toInterceptOpts: options.toInterceptOpts
154
155
  });
155
156
  if (interceptState.kind === "source-route") {
157
+ const renderRoute = interceptState.sourceRoute;
158
+ const renderParams = pickRouteParams(interceptState.intercept.matchedParams, options.getRouteParamNames(interceptState.sourceRoute));
156
159
  options.setNavigationContext({
157
160
  params: interceptState.intercept.matchedParams,
158
161
  pathname: options.cleanPathname,
159
162
  searchParams: options.searchParams
160
163
  });
161
- const interceptElement = await options.buildPageElement(interceptState.sourceRoute, pickRouteParams(interceptState.intercept.matchedParams, options.getRouteParamNames(interceptState.sourceRoute)), options.toInterceptOpts(interceptState.intercept), options.searchParams, options.layoutParamAccess);
164
+ const interceptElement = await options.buildPageElement(renderRoute, renderParams, options.toInterceptOpts(interceptState.intercept), options.searchParams, options.layoutParamAccess);
162
165
  return {
163
166
  interceptOpts: void 0,
164
- response: await options.renderInterceptResponse(interceptState.sourceRoute, interceptElement)
167
+ response: await options.renderInterceptResponse(renderRoute, interceptElement)
165
168
  };
166
169
  }
167
170
  return {
@@ -45,8 +45,8 @@ type BuildAppPageRscResponseOptions = {
45
45
  timing?: AppPageResponseTiming;
46
46
  };
47
47
  type BuildAppPageHtmlResponseOptions = {
48
- draftCookie?: string | null;
49
- fontLinkHeader?: string;
48
+ draftCookie?: string | null; /** Combined preload `Link` header value (React hints + font preloads), already capped. */
49
+ linkHeader?: string;
50
50
  isEdgeRuntime?: boolean;
51
51
  middlewareContext: AppPageMiddlewareContext;
52
52
  policy: AppPageResponsePolicy;
@@ -108,7 +108,7 @@ function buildAppPageHtmlResponse(body, options) {
108
108
  if (options.policy.cacheControl) headers.set("Cache-Control", options.policy.cacheControl);
109
109
  if (options.policy.cacheState) setCacheStateHeaders(headers, options.policy.cacheState);
110
110
  if (options.draftCookie) headers.append("Set-Cookie", options.draftCookie);
111
- if (options.fontLinkHeader) headers.set("Link", options.fontLinkHeader);
111
+ if (options.linkHeader) headers.set("Link", options.linkHeader);
112
112
  mergeMiddlewareResponseHeaders(headers, options.middlewareContext.headers);
113
113
  applyTimingHeader(headers, options.timing);
114
114
  return new Response(body, {
@@ -329,7 +329,7 @@ function buildAppPageElements(options) {
329
329
  }
330
330
  let routeChildren = /* @__PURE__ */ jsx(LayoutSegmentProvider, {
331
331
  segmentMap: { children: [] },
332
- children: /* @__PURE__ */ jsx(AppRouterScrollTarget, { children: /* @__PURE__ */ jsx(Slot, { id: pageId }) })
332
+ children: /* @__PURE__ */ jsx(Slot, { id: pageId })
333
333
  });
334
334
  if (isPrefetchLoadingShell) if (routeLoadingComponent === null) routeChildren = null;
335
335
  else routeChildren = /* @__PURE__ */ jsx(routeLoadingComponent, {});
@@ -339,6 +339,7 @@ function buildAppPageElements(options) {
339
339
  fallback: /* @__PURE__ */ jsx(routeLoadingComponent, {}),
340
340
  children: routeChildren
341
341
  }, routeResetKey);
342
+ routeChildren = /* @__PURE__ */ jsx(AppRouterScrollTarget, { children: routeChildren });
342
343
  }
343
344
  const lastLayoutErrorModule = errorEntries.length > 0 ? errorEntries[errorEntries.length - 1].errorModule : null;
344
345
  const notFoundComponent = getDefaultExport(options.route.notFound) ?? getDefaultExport(options.rootNotFoundModule);
@@ -18,8 +18,30 @@ type AppSsrRenderResult = {
18
18
  htmlStream: ReadableStream<Uint8Array>;
19
19
  metadataReady: Promise<void>;
20
20
  capturedRscData: Promise<ArrayBuffer> | null;
21
+ /**
22
+ * Preload `Link` header value emitted by React during SSR (via `onHeaders`),
23
+ * already capped to `reactMaxHeadersLength`. Empty/undefined when React
24
+ * emitted no preload headers (or emission was disabled with `0`).
25
+ */
26
+ linkHeader?: string;
21
27
  };
22
28
  declare function isAppSsrRenderResult(value: unknown): value is AppSsrRenderResult;
29
+ /**
30
+ * Combine the React-emitted preload `Link` header with vinext's font preload
31
+ * `Link` header, capping the result to `reactMaxHeadersLength`.
32
+ *
33
+ * React already caps its own portion, but vinext emits font preloads through a
34
+ * separate channel. Mirroring Next.js — where every preload flows through a
35
+ * single capped `onHeaders` callback — we cap the *combined* header here,
36
+ * keeping only whole entries that fit and dropping the rest once the limit is
37
+ * exceeded. `0` disables emission entirely (matches React); `undefined` falls
38
+ * back to the React default of 6000.
39
+ *
40
+ * React's hints (scripts/modules/styles) come first so that under a tight cap
41
+ * the render-critical entries survive and trailing font preloads are dropped
42
+ * first.
43
+ */
44
+ declare function buildAppPageLinkHeader(reactLinkHeader: string | undefined, fontLinkHeader: string | undefined, maxHeadersLength: number | undefined): string;
23
45
  type AppPageSsrHandler = {
24
46
  handleSsr: (rscStream: ReadableStream<Uint8Array>, navigationContext: NavigationContext | null, fontData: AppPageFontData, options?: {
25
47
  formState?: ReactFormState | null;
@@ -30,6 +52,12 @@ type AppPageSsrHandler = {
30
52
  * in the SSR head. Sourced from `experimental.clientTraceMetadata`.
31
53
  */
32
54
  clientTraceMetadata?: readonly string[];
55
+ /**
56
+ * Maximum total length (in characters) of the preload `Link` header
57
+ * emitted during SSR. `0` disables emission. From `reactMaxHeadersLength`
58
+ * in `next.config`.
59
+ */
60
+ reactMaxHeadersLength?: number;
33
61
  rootParams?: RootParams;
34
62
  sideStream?: ReadableStream<Uint8Array>;
35
63
  capturedRscDataRef?: {
@@ -52,6 +80,12 @@ type RenderAppPageHtmlStreamOptions = {
52
80
  * the SSR head. Undefined or empty disables emission.
53
81
  */
54
82
  clientTraceMetadata?: readonly string[];
83
+ /**
84
+ * Maximum total length (in characters) of the preload `Link` header emitted
85
+ * during SSR. `0` disables emission. From `reactMaxHeadersLength` in
86
+ * `next.config`.
87
+ */
88
+ reactMaxHeadersLength?: number;
55
89
  rootParams?: RootParams;
56
90
  ssrHandler: AppPageSsrHandler;
57
91
  /** Pre-split side stream for fused embed+capture (#981). When set,
@@ -74,7 +108,8 @@ type AppPageHtmlStreamRecoveryResult = {
74
108
  htmlStream: ReadableStream<Uint8Array> | null;
75
109
  response: Response | null;
76
110
  metadataReady: Promise<void>;
77
- capturedRscData: Promise<ArrayBuffer> | null;
111
+ capturedRscData: Promise<ArrayBuffer> | null; /** React-emitted preload `Link` header (already capped). */
112
+ linkHeader?: string;
78
113
  };
79
114
  type RenderAppPageHtmlStreamWithRecoveryOptions<TSpecialError> = {
80
115
  onShellRendered?: () => void;
@@ -113,4 +148,4 @@ declare function renderAppPageHtmlStreamWithRecovery<TSpecialError>(options: Ren
113
148
  declare function createAppPageRscErrorTracker(baseOnError: (error: unknown, requestInfo: unknown, errorContext: unknown) => unknown): AppPageRscErrorTracker;
114
149
  declare function shouldRerenderAppPageWithGlobalError(options: ShouldRerenderAppPageWithGlobalErrorOptions): boolean;
115
150
  //#endregion
116
- export { AppPageFontData, AppPageSsrHandler, AppSsrRenderResult, createAppPageFontData, createAppPageRscErrorTracker, deferUntilStreamConsumed, isAppSsrRenderResult, renderAppPageHtmlResponse, renderAppPageHtmlStream, renderAppPageHtmlStreamWithRecovery, shouldRerenderAppPageWithGlobalError };
151
+ export { AppPageFontData, AppPageSsrHandler, AppSsrRenderResult, buildAppPageLinkHeader, createAppPageFontData, createAppPageRscErrorTracker, deferUntilStreamConsumed, isAppSsrRenderResult, renderAppPageHtmlResponse, renderAppPageHtmlStream, renderAppPageHtmlStreamWithRecovery, shouldRerenderAppPageWithGlobalError };
@@ -14,6 +14,37 @@ function normalizeAppSsrRenderResult(raw, fallbackCapturedRscData = null) {
14
14
  capturedRscData: fallbackCapturedRscData
15
15
  };
16
16
  }
17
+ /**
18
+ * Combine the React-emitted preload `Link` header with vinext's font preload
19
+ * `Link` header, capping the result to `reactMaxHeadersLength`.
20
+ *
21
+ * React already caps its own portion, but vinext emits font preloads through a
22
+ * separate channel. Mirroring Next.js — where every preload flows through a
23
+ * single capped `onHeaders` callback — we cap the *combined* header here,
24
+ * keeping only whole entries that fit and dropping the rest once the limit is
25
+ * exceeded. `0` disables emission entirely (matches React); `undefined` falls
26
+ * back to the React default of 6000.
27
+ *
28
+ * React's hints (scripts/modules/styles) come first so that under a tight cap
29
+ * the render-critical entries survive and trailing font preloads are dropped
30
+ * first.
31
+ */
32
+ function buildAppPageLinkHeader(reactLinkHeader, fontLinkHeader, maxHeadersLength) {
33
+ const limit = typeof maxHeadersLength === "number" ? maxHeadersLength : 6e3;
34
+ if (limit <= 0) return "";
35
+ const entries = [];
36
+ for (const source of [reactLinkHeader, fontLinkHeader]) {
37
+ if (!source) continue;
38
+ for (const entry of source.split(", ")) if (entry.length > 0) entries.push(entry);
39
+ }
40
+ let header = "";
41
+ for (const entry of entries) {
42
+ const next = header.length === 0 ? entry : `${header}, ${entry}`;
43
+ if (next.length > limit) break;
44
+ header = next;
45
+ }
46
+ return header;
47
+ }
17
48
  function createAppPageFontData(options) {
18
49
  return {
19
50
  links: options.getLinks(),
@@ -27,6 +58,7 @@ async function renderAppPageHtmlStream(options) {
27
58
  scriptNonce: options.scriptNonce,
28
59
  basePath: options.basePath,
29
60
  clientTraceMetadata: options.clientTraceMetadata,
61
+ reactMaxHeadersLength: options.reactMaxHeadersLength,
30
62
  rootParams: options.rootParams,
31
63
  sideStream: options.sideStream,
32
64
  capturedRscDataRef: options.capturedRscDataRef,
@@ -89,13 +121,14 @@ async function renderAppPageHtmlResponse(options) {
89
121
  }
90
122
  async function renderAppPageHtmlStreamWithRecovery(options) {
91
123
  try {
92
- const { htmlStream, metadataReady, capturedRscData } = normalizeAppSsrRenderResult(await options.renderHtmlStream());
124
+ const { htmlStream, metadataReady, capturedRscData, linkHeader } = normalizeAppSsrRenderResult(await options.renderHtmlStream());
93
125
  options.onShellRendered?.();
94
126
  return {
95
127
  htmlStream,
96
128
  response: null,
97
129
  metadataReady,
98
- capturedRscData
130
+ capturedRscData,
131
+ linkHeader
99
132
  };
100
133
  } catch (error) {
101
134
  const specialError = options.resolveSpecialError(error);
@@ -137,4 +170,4 @@ function shouldRerenderAppPageWithGlobalError(options) {
137
170
  return Boolean(options.capturedError) && !options.hasLocalBoundary;
138
171
  }
139
172
  //#endregion
140
- export { createAppPageFontData, createAppPageRscErrorTracker, deferUntilStreamConsumed, isAppSsrRenderResult, renderAppPageHtmlResponse, renderAppPageHtmlStream, renderAppPageHtmlStreamWithRecovery, shouldRerenderAppPageWithGlobalError };
173
+ export { buildAppPageLinkHeader, createAppPageFontData, createAppPageRscErrorTracker, deferUntilStreamConsumed, isAppSsrRenderResult, renderAppPageHtmlResponse, renderAppPageHtmlStream, renderAppPageHtmlStreamWithRecovery, shouldRerenderAppPageWithGlobalError };
@@ -10,6 +10,22 @@ type RenderPagesFallbackDependencies = {
10
10
  buildRequestHeaders: (requestHeaders: Headers, middlewareRequestHeaders: Headers) => Headers | null;
11
11
  decodePathParams: (pathname: string) => string;
12
12
  applyRouteHandlerMiddlewareContext: (response: Response, middlewareContext: AppMiddlewareContext) => Response;
13
+ /**
14
+ * Returns the `__prerender_bypass` Set-Cookie header emitted by a
15
+ * `draftMode().enable()`/`disable()` call inside middleware, if any. Reading
16
+ * it clears it. Mirrors how App Router route handlers and page renders surface
17
+ * the middleware-enabled draft cookie so the same flow works when the request
18
+ * falls through to a Pages Router route.
19
+ *
20
+ * Note: this closes the draft-mode flow for production (Cloudflare Workers /
21
+ * Node), where middleware runs inline in the same RSC handler context that
22
+ * builds this fallback. In hybrid *dev*, middleware runs in a separate Vite
23
+ * Pages SSR runner and `draftMode()` inside middleware is not yet permitted
24
+ * there (it throws a scope error before any cookie is set), so this getter
25
+ * returns `null` and no cookie is appended. That dev limitation is pre-existing
26
+ * and tracked separately from #1520.
27
+ */
28
+ getDraftModeCookieHeader: () => string | null | undefined;
13
29
  };
14
30
  type RenderPagesFallbackOptions = {
15
31
  isRscRequest: boolean;
@@ -4,7 +4,7 @@
4
4
  */
5
5
  async function renderPagesFallback(options, dependencies) {
6
6
  const { isRscRequest, middlewareContext, request, url } = options;
7
- const { loadPagesEntry, buildRequestHeaders, decodePathParams, applyRouteHandlerMiddlewareContext } = dependencies;
7
+ const { loadPagesEntry, buildRequestHeaders, decodePathParams, applyRouteHandlerMiddlewareContext, getDraftModeCookieHeader } = dependencies;
8
8
  if (isRscRequest) return null;
9
9
  const pagesEntry = await loadPagesEntry();
10
10
  const pagesRequestHeaders = middlewareContext.requestHeaders ? buildRequestHeaders(request.headers, middlewareContext.requestHeaders) : null;
@@ -24,11 +24,31 @@ async function renderPagesFallback(options, dependencies) {
24
24
  const pagesPathname = url.pathname;
25
25
  if (pagesPathname.startsWith("/api/") || pagesPathname === "/api") {
26
26
  if (typeof pagesEntry.handleApiRoute !== "function") return null;
27
- return applyRouteHandlerMiddlewareContext(await pagesEntry.handleApiRoute(pagesRequest, pagesUrl), middlewareContext);
27
+ const pagesApiResponse = await pagesEntry.handleApiRoute(pagesRequest, pagesUrl);
28
+ const draftCookie = getDraftModeCookieHeader();
29
+ return applyDraftModeCookie(applyRouteHandlerMiddlewareContext(pagesApiResponse, middlewareContext), draftCookie);
28
30
  }
29
31
  if (typeof pagesEntry.renderPage !== "function") return null;
30
32
  const pagesRes = await pagesEntry.renderPage(pagesRequest, pagesUrl, {}, void 0, middlewareContext.requestHeaders);
31
- return pagesRes.status !== 404 ? pagesRes : null;
33
+ if (pagesRes.status === 404) return null;
34
+ return applyDraftModeCookie(pagesRes, getDraftModeCookieHeader());
35
+ }
36
+ /**
37
+ * Append a middleware-emitted `__prerender_bypass` Set-Cookie header to a Pages
38
+ * Router fallback response. Returns the response unchanged when there is no
39
+ * draft cookie to add. App Router route handlers/page renders surface this same
40
+ * cookie via `finalizeRouteHandlerResponse`/the page response builder; this
41
+ * keeps draft-mode parity for requests that fall through to the Pages Router.
42
+ */
43
+ function applyDraftModeCookie(response, draftCookie) {
44
+ if (!draftCookie) return response;
45
+ const headers = new Headers(response.headers);
46
+ headers.append("Set-Cookie", draftCookie);
47
+ return new Response(response.body, {
48
+ status: response.status,
49
+ statusText: response.statusText,
50
+ headers
51
+ });
32
52
  }
33
53
  //#endregion
34
54
  export { renderPagesFallback };
@@ -18,6 +18,7 @@ type ReadAppRouteHandlerCacheOptions = {
18
18
  getCollectedFetchTags: () => string[];
19
19
  handlerFn: AppRouteHandlerFunction;
20
20
  i18n?: NextI18nConfig | null;
21
+ trailingSlash?: boolean;
21
22
  isAutoHead: boolean;
22
23
  isrDebug?: AppRouteDebugLogger;
23
24
  isrGet: RouteHandlerCacheGetter;
@@ -39,6 +39,7 @@ async function readAppRouteHandlerCacheResponse(options) {
39
39
  dynamicConfig: options.dynamicConfig,
40
40
  handlerFn: options.handlerFn,
41
41
  i18n: options.i18n,
42
+ trailingSlash: options.trailingSlash,
42
43
  markDynamicUsage: options.markDynamicUsage,
43
44
  params: options.params === null ? null : makeThenableParams(options.params),
44
45
  request: new Request(options.requestUrl, { method: "GET" }),
@@ -30,6 +30,7 @@ type DispatchAppRouteHandlerOptions = {
30
30
  isrGet: RouteHandlerCacheGetter;
31
31
  isrRouteKey: (pathname: string) => string;
32
32
  isrSet: RouteHandlerCacheSetter;
33
+ trailingSlash?: boolean;
33
34
  middlewareContext: RouteHandlerMiddlewareContext;
34
35
  middlewareRequestHeaders?: Headers | null;
35
36
  /**
@@ -77,6 +77,7 @@ async function dispatchAppRouteHandler(options) {
77
77
  getCollectedFetchTags,
78
78
  handlerFn: resolvedHandlerFn,
79
79
  i18n: options.i18n,
80
+ trailingSlash: options.trailingSlash,
80
81
  isAutoHead,
81
82
  isrDebug: options.isrDebug,
82
83
  isrGet: options.isrGet,
@@ -127,6 +128,7 @@ async function dispatchAppRouteHandler(options) {
127
128
  handler,
128
129
  handlerFn: resolvedHandlerFn,
129
130
  i18n: options.i18n,
131
+ trailingSlash: options.trailingSlash,
130
132
  isAutoHead,
131
133
  isProduction,
132
134
  isrDebug: options.isrDebug,
@@ -42,6 +42,7 @@ type RunAppRouteHandlerOptions = {
42
42
  dynamicConfig?: string;
43
43
  handlerFn: AppRouteHandlerFunction;
44
44
  i18n?: NextI18nConfig | null;
45
+ trailingSlash?: boolean;
45
46
  markDynamicUsage: MarkAppRouteDynamicUsageFn;
46
47
  middlewareRequestHeaders?: Headers | null;
47
48
  /**
@@ -21,6 +21,7 @@ async function runAppRouteHandler(options) {
21
21
  const trackedRequest = createTrackedAppRouteRequest(options.request, {
22
22
  basePath: options.basePath,
23
23
  i18n: options.i18n,
24
+ trailingSlash: options.trailingSlash,
24
25
  middlewareHeaders: options.middlewareRequestHeaders,
25
26
  onDynamicAccess() {
26
27
  options.markDynamicUsage();
@@ -24,6 +24,7 @@ type AppRouteRequestMode = "auto" | "force-static" | "error";
24
24
  type TrackedAppRouteRequestOptions = {
25
25
  basePath?: string;
26
26
  i18n?: NextI18nConfig | null;
27
+ trailingSlash?: boolean;
27
28
  middlewareHeaders?: Headers | null;
28
29
  onDynamicAccess?: (access: AppRouteDynamicRequestAccess) => void;
29
30
  requestMode?: AppRouteRequestMode;
@@ -43,10 +43,11 @@ function bindMethodIfNeeded(value, target) {
43
43
  return typeof value === "function" ? value.bind(target) : value;
44
44
  }
45
45
  function buildNextConfig(options) {
46
- if (!options.basePath && !options.i18n) return null;
46
+ if (!options.basePath && !options.i18n && !options.trailingSlash) return null;
47
47
  return {
48
48
  basePath: options.basePath,
49
- i18n: options.i18n ?? void 0
49
+ i18n: options.i18n ?? void 0,
50
+ trailingSlash: options.trailingSlash
50
51
  };
51
52
  }
52
53
  function rebuildRequestWithHeaders(input, headers) {
@@ -30,6 +30,7 @@ type AppRscRouteMatch<TRoute> = {
30
30
  type DispatchMatchedPageOptions<TRoute> = {
31
31
  clientReuseManifest: ClientReuseManifestParseResult;
32
32
  cleanPathname: string;
33
+ displayPathname: string;
33
34
  formState: ReactFormState | null;
34
35
  actionError?: unknown;
35
36
  actionFailed?: boolean;
@@ -2,7 +2,7 @@ import { createRequestContext, runWithRequestContext } from "../shims/unified-re
2
2
  import { hasBasePath } from "../utils/base-path.js";
3
3
  import { getRequestExecutionContext } from "../shims/request-context.js";
4
4
  import { ACTION_REVALIDATED_HEADER, VINEXT_MW_CTX_HEADER, VINEXT_PRERENDER_ROUTE_PARAMS_HEADER } from "./headers.js";
5
- import { isExternalUrl, matchRedirect, matchRewrite, proxyExternalRequest, requestContextFromRequest, sanitizeDestination } from "../config/config-matchers.js";
5
+ import { isExternalUrl, matchRedirect, matchRewrite, preserveRedirectDestinationQuery, proxyExternalRequest, requestContextFromRequest, sanitizeDestination } from "../config/config-matchers.js";
6
6
  import { notFoundResponse } from "./http-error-responses.js";
7
7
  import { applyConfigHeadersToResponse, cloneRequestWithHeaders, cloneRequestWithUrl, filterInternalHeaders, normalizeTrailingSlash, resolvePublicFileRoute, validateImageUrl } from "./request-pipeline.js";
8
8
  import { headersContextFromRequest } from "../shims/headers.js";
@@ -15,11 +15,11 @@ import { mergeMiddlewareResponseHeaders } from "./middleware-response-headers.js
15
15
  import "./app-page-response.js";
16
16
  import { prerenderRouteParamsPayloadMatchesRoute, readTrustedPrerenderRouteParams, serializePrerenderRouteParamsHeader } from "./prerender-route-params.js";
17
17
  import { pickRootParams, setRootParams } from "../shims/root-params.js";
18
- import { flattenErrorCauses } from "../utils/error-cause.js";
19
18
  import { applyAppMiddleware } from "./app-middleware.js";
20
19
  import { buildPageCacheTags } from "./implicit-tags.js";
21
20
  import { buildPostMwRequestContext } from "./app-post-middleware-context.js";
22
21
  import { handleAppPrerenderEndpoint } from "./app-prerender-endpoints.js";
22
+ import { flattenErrorCauses } from "../utils/error-cause.js";
23
23
  import { finalizeAppRscResponse } from "./app-rsc-response-finalizer.js";
24
24
  import { normalizeRscRequest } from "./app-rsc-request-normalization.js";
25
25
  import { handleMetadataRouteRequest } from "./metadata-route-response.js";
@@ -113,7 +113,7 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
113
113
  const redirect = matchRedirect(matchPathname(stripRscSuffix(pathname)), options.configRedirects, preMiddlewareRequestContext, basePathState);
114
114
  if (redirect) {
115
115
  const destination = sanitizeDestination(redirectDestinationWithBasePath(redirect.destination, options.basePath));
116
- const location = isRscRequest && request.headers.get("RSC") === "1" ? await createRscRedirectLocation(destination, request) : destination;
116
+ const location = isRscRequest && request.headers.get("RSC") === "1" ? await createRscRedirectLocation(destination, request) : preserveRedirectDestinationQuery(destination, url.search);
117
117
  return new Response(null, {
118
118
  status: redirect.permanent ? 308 : 307,
119
119
  headers: { Location: location }
@@ -302,6 +302,7 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
302
302
  const pageResponse = await options.dispatchMatchedPage({
303
303
  clientReuseManifest,
304
304
  cleanPathname,
305
+ displayPathname: canonicalPathname,
305
306
  formState,
306
307
  actionError,
307
308
  actionFailed,
@@ -1,6 +1,12 @@
1
1
  import { RoutePatternParams } from "../routing/route-pattern.js";
2
2
 
3
3
  //#region src/server/app-rsc-route-matching.d.ts
4
+ /**
5
+ * Sentinel slot key used for sibling-style interception entries.
6
+ * When a matched intercept carries this key, the render layer replaces the
7
+ * route's main page element instead of a parallel slot.
8
+ */
9
+ declare const SIBLING_PAGE_INTERCEPT_SLOT_KEY = "__vinext_page_intercept";
4
10
  type AppRscRouteParams = RoutePatternParams;
5
11
  type AppRscInterceptForMatching = {
6
12
  targetPattern: string;
@@ -25,15 +31,27 @@ type AppRscInterceptForMatching = {
25
31
  sourceMatchPattern?: string;
26
32
  interceptLayouts: readonly unknown[];
27
33
  page: unknown;
34
+ __pageLoader?: (() => Promise<unknown>) | null;
28
35
  params: readonly string[];
29
36
  };
30
37
  type AppRscSlotForMatching = {
31
38
  id?: string | null;
32
39
  intercepts?: readonly AppRscInterceptForMatching[];
33
40
  };
41
+ type AppRscSiblingInterceptForMatching = {
42
+ targetPattern: string;
43
+ sourceMatchPattern: string | null;
44
+ slotId: string | null;
45
+ interceptLayouts: readonly unknown[];
46
+ page: unknown;
47
+ __pageLoader?: (() => Promise<unknown>) | null;
48
+ params: readonly string[];
49
+ };
34
50
  type AppRscRouteForMatching = {
51
+ pattern: string;
35
52
  patternParts: string[];
36
53
  slots?: Record<string, AppRscSlotForMatching>;
54
+ siblingIntercepts?: AppRscSiblingInterceptForMatching[];
37
55
  };
38
56
  type AppRscInterceptMatch = AppRscInterceptLookupEntry & {
39
57
  matchedParams: AppRscRouteParams;
@@ -47,6 +65,7 @@ type AppRscInterceptLookupEntry = {
47
65
  sourceMatchPatternParts: string[] | null;
48
66
  interceptLayouts: readonly unknown[];
49
67
  page: unknown;
68
+ __pageLoader?: (() => Promise<unknown>) | null;
50
69
  params: readonly string[];
51
70
  slotId: string | null;
52
71
  };
@@ -59,4 +78,4 @@ declare function createAppRscRouteMatcher<Route extends AppRscRouteForMatching>(
59
78
  };
60
79
  declare function matchAppRscRoutePattern(urlParts: string[], patternParts: string[]): AppRscRouteParams | null;
61
80
  //#endregion
62
- export { createAppRscRouteMatcher, matchAppRscRoutePattern };
81
+ export { SIBLING_PAGE_INTERCEPT_SLOT_KEY, createAppRscRouteMatcher, matchAppRscRoutePattern };
@@ -2,6 +2,12 @@ import { splitPathnameForRouteMatch } from "../routing/utils.js";
2
2
  import { buildRouteTrie, trieMatch } from "../routing/route-trie.js";
3
3
  import { matchRoutePattern, matchRoutePatternPrefix } from "../routing/route-pattern.js";
4
4
  //#region src/server/app-rsc-route-matching.ts
5
+ /**
6
+ * Sentinel slot key used for sibling-style interception entries.
7
+ * When a matched intercept carries this key, the render layer replaces the
8
+ * route's main page element instead of a parallel slot.
9
+ */
10
+ const SIBLING_PAGE_INTERCEPT_SLOT_KEY = "__vinext_page_intercept";
5
11
  function createRouteParams() {
6
12
  return Object.create(null);
7
13
  }
@@ -54,17 +60,18 @@ function matchInterceptSource(sourceParts, entry) {
54
60
  return matchRoutePatternPrefix(sourceParts, patternParts);
55
61
  }
56
62
  function createInterceptLookup(routes) {
63
+ const patternToIndex = new Map(routes.map((r, i) => [r.pattern, i]));
57
64
  const interceptLookup = [];
58
65
  for (let routeIndex = 0; routeIndex < routes.length; routeIndex++) {
59
66
  const route = routes[routeIndex];
60
- if (!route.slots) continue;
61
- for (const [slotKey, slotModule] of Object.entries(route.slots)) {
67
+ if (route.slots) for (const [slotKey, slotModule] of Object.entries(route.slots)) {
62
68
  if (!slotModule.intercepts) continue;
63
69
  for (const intercept of slotModule.intercepts) {
64
70
  const sourceMatchPattern = intercept.sourceMatchPattern ?? null;
65
71
  const sourceMatchPatternParts = sourceMatchPattern ? sourceMatchPattern.split("/").filter(Boolean) : null;
72
+ const ownerRouteIndex = sourceMatchPattern !== null ? patternToIndex.get(sourceMatchPattern) ?? routeIndex : routeIndex;
66
73
  interceptLookup.push({
67
- sourceRouteIndex: routeIndex,
74
+ sourceRouteIndex: ownerRouteIndex,
68
75
  slotKey,
69
76
  slotId: typeof slotModule.id === "string" ? slotModule.id : null,
70
77
  targetPattern: intercept.targetPattern,
@@ -73,10 +80,28 @@ function createInterceptLookup(routes) {
73
80
  sourceMatchPatternParts,
74
81
  interceptLayouts: intercept.interceptLayouts,
75
82
  page: intercept.page,
83
+ __pageLoader: intercept.__pageLoader,
76
84
  params: intercept.params
77
85
  });
78
86
  }
79
87
  }
88
+ if (route.siblingIntercepts) for (const intercept of route.siblingIntercepts) {
89
+ const sourceMatchPattern = intercept.sourceMatchPattern ?? null;
90
+ const sourceMatchPatternParts = sourceMatchPattern ? sourceMatchPattern.split("/").filter(Boolean) : null;
91
+ interceptLookup.push({
92
+ sourceRouteIndex: routeIndex,
93
+ slotKey: SIBLING_PAGE_INTERCEPT_SLOT_KEY,
94
+ slotId: typeof intercept.slotId === "string" ? intercept.slotId : null,
95
+ targetPattern: intercept.targetPattern,
96
+ targetPatternParts: intercept.targetPattern.split("/").filter(Boolean),
97
+ sourceMatchPattern,
98
+ sourceMatchPatternParts,
99
+ interceptLayouts: intercept.interceptLayouts,
100
+ page: intercept.page,
101
+ __pageLoader: intercept.__pageLoader,
102
+ params: intercept.params
103
+ });
104
+ }
80
105
  }
81
106
  return interceptLookup;
82
107
  }
@@ -87,4 +112,4 @@ function mergeMatchedParams(sourceParams, targetParams) {
87
112
  return Object.assign(createRouteParams(), sourceParams, targetParams);
88
113
  }
89
114
  //#endregion
90
- export { createAppRscRouteMatcher, matchAppRscRoutePattern };
115
+ export { SIBLING_PAGE_INTERCEPT_SLOT_KEY, createAppRscRouteMatcher, matchAppRscRoutePattern };
@@ -108,6 +108,15 @@ type HandleProgressiveServerActionRequestOptions = {
108
108
  decodeFormState: AppServerActionFormStateDecoder;
109
109
  getAndClearPendingCookies: () => string[];
110
110
  getDraftModeCookieHeader: () => string | null | undefined;
111
+ /**
112
+ * Whether the posted-to route resolves to an App Router *page* (as opposed to
113
+ * a route handler or no match). Multipart form POSTs to a page are always
114
+ * server-action attempts in Next.js, so a body that decodes to no action must
115
+ * surface as 404 action-not-found rather than rendering the page. Route
116
+ * handlers (which run *after* this dispatch in vinext) legitimately receive
117
+ * raw multipart POSTs, so they must still fall through. See issue #1340.
118
+ */
119
+ hasPageRoute: boolean;
111
120
  maxActionBodySize: number;
112
121
  middlewareHeaders: Headers | null;
113
122
  readFormDataWithLimit: ReadFormDataWithLimit;
@@ -143,7 +152,8 @@ type HandleServerActionRscRequestOptions<TElement, TRoute extends AppServerActio
143
152
  isRscRequest: boolean;
144
153
  loadServerAction: (actionId: string) => Promise<unknown>;
145
154
  matchRoute: (pathname: string) => AppServerActionMatch<TRoute> | null;
146
- maxActionBodySize: number;
155
+ maxActionBodySize: number; /** Verbatim `serverActions.bodySizeLimit` config string (e.g. "2mb") for the body-exceeded error. */
156
+ maxActionBodySizeLabel: string;
147
157
  middlewareHeaders: Headers | null;
148
158
  middlewareStatus: number | null | undefined;
149
159
  mountedSlotsHeader: string | null;