vinext 1.4.2 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +4 -1
  2. package/dist/build/prerender-paths.d.ts +11 -1
  3. package/dist/build/prerender-paths.js +41 -11
  4. package/dist/cli.js +1 -1
  5. package/dist/client/dev-error-overlay.d.ts +2 -1
  6. package/dist/client/dev-error-overlay.js +8 -3
  7. package/dist/config/config-matchers.js +6 -2
  8. package/dist/config/next-config.d.ts +9 -0
  9. package/dist/entries/app-rsc-entry.js +24 -12
  10. package/dist/entries/pages-server-entry.js +17 -4
  11. package/dist/index.js +48 -39
  12. package/dist/init-platform.js +2 -2
  13. package/dist/init.js +42 -5
  14. package/dist/server/api-handler.js +8 -5
  15. package/dist/server/app-browser-entry.js +26 -47
  16. package/dist/server/app-browser-error.d.ts +1 -1
  17. package/dist/server/app-browser-error.js +13 -4
  18. package/dist/server/app-browser-navigation-controller.js +2 -2
  19. package/dist/server/app-browser-server-action-client.js +2 -2
  20. package/dist/server/app-browser-state.js +2 -2
  21. package/dist/server/app-browser-supplemental-refresh.d.ts +13 -0
  22. package/dist/server/app-browser-supplemental-refresh.js +36 -4
  23. package/dist/server/app-browser-visible-commit.js +1 -1
  24. package/dist/server/app-history-state.js +1 -1
  25. package/dist/server/app-middleware.d.ts +2 -0
  26. package/dist/server/app-middleware.js +1 -0
  27. package/dist/server/app-page-boundary-render.js +4 -2
  28. package/dist/server/app-page-dispatch.d.ts +1 -0
  29. package/dist/server/app-page-dispatch.js +5 -1
  30. package/dist/server/app-page-element-builder.d.ts +2 -0
  31. package/dist/server/app-page-element-builder.js +6 -5
  32. package/dist/server/app-page-render.js +1 -1
  33. package/dist/server/app-page-route-wiring.d.ts +7 -5
  34. package/dist/server/app-page-route-wiring.js +11 -9
  35. package/dist/server/app-pages-bridge.d.ts +6 -0
  36. package/dist/server/app-pages-bridge.js +8 -3
  37. package/dist/server/app-request-context.js +1 -1
  38. package/dist/server/app-request-stage-independent-entry.js +2 -3
  39. package/dist/server/app-route-handler-dispatch.js +1 -1
  40. package/dist/server/app-route-handler-execution.js +1 -1
  41. package/dist/server/app-route-handler-response.d.ts +1 -0
  42. package/dist/server/app-route-handler-response.js +27 -2
  43. package/dist/server/app-router-entry.js +2 -3
  44. package/dist/server/app-rsc-cache-busting.d.ts +8 -0
  45. package/dist/server/app-rsc-cache-busting.js +13 -1
  46. package/dist/server/app-rsc-combined-handler.js +2 -1
  47. package/dist/server/app-rsc-handler.d.ts +4 -0
  48. package/dist/server/app-rsc-handler.js +41 -24
  49. package/dist/server/app-rsc-response-finalizer.d.ts +15 -3
  50. package/dist/server/app-rsc-response-finalizer.js +51 -7
  51. package/dist/server/app-rsc-response-stage.js +1 -1
  52. package/dist/server/app-server-action-execution.js +1 -1
  53. package/dist/server/app-ssr-entry.js +18 -10
  54. package/dist/server/config-headers.d.ts +7 -0
  55. package/dist/server/config-headers.js +20 -1
  56. package/dist/server/edge-api-runtime.d.ts +6 -2
  57. package/dist/server/edge-api-runtime.js +21 -4
  58. package/dist/server/image-animation.d.ts +9 -0
  59. package/dist/server/image-animation.js +118 -0
  60. package/dist/server/image-optimization.d.ts +25 -5
  61. package/dist/server/image-optimization.js +305 -21
  62. package/dist/server/image-source-reader.d.ts +34 -0
  63. package/dist/server/image-source-reader.js +126 -0
  64. package/dist/server/metadata-route-response.d.ts +6 -1
  65. package/dist/server/metadata-route-response.js +24 -6
  66. package/dist/server/middleware-matcher.d.ts +1 -0
  67. package/dist/server/middleware-matcher.js +1 -1
  68. package/dist/server/middleware-runtime.d.ts +5 -0
  69. package/dist/server/middleware-runtime.js +48 -15
  70. package/dist/server/middleware.d.ts +1 -1
  71. package/dist/server/middleware.js +2 -1
  72. package/dist/server/navigation-planner.js +14 -6
  73. package/dist/server/nitro-request-target-plugin.d.ts +17 -0
  74. package/dist/server/nitro-request-target-plugin.js +22 -0
  75. package/dist/server/open-redirect.d.ts +2 -1
  76. package/dist/server/open-redirect.js +2 -1
  77. package/dist/server/pages-client-assets.d.ts +1 -0
  78. package/dist/server/pages-i18n.d.ts +7 -0
  79. package/dist/server/pages-i18n.js +14 -1
  80. package/dist/server/pages-node-compat.js +7 -3
  81. package/dist/server/pages-page-handler.js +4 -4
  82. package/dist/server/pages-request-pipeline.d.ts +9 -0
  83. package/dist/server/pages-request-pipeline.js +29 -10
  84. package/dist/server/pages-request-stage-entry.js +7 -2
  85. package/dist/server/pages-router-entry.js +3 -0
  86. package/dist/server/prod-server.d.ts +1 -1
  87. package/dist/server/prod-server.js +48 -54
  88. package/dist/server/remove-request-headers.d.ts +16 -0
  89. package/dist/server/remove-request-headers.js +23 -0
  90. package/dist/server/srvx-middleware-origin.d.ts +13 -0
  91. package/dist/server/srvx-middleware-origin.js +19 -0
  92. package/dist/server/srvx-request-target.d.ts +12 -0
  93. package/dist/server/srvx-request-target.js +20 -0
  94. package/dist/server/static-file-cache.js +10 -22
  95. package/dist/server/worker-image-optimization.d.ts +8 -1
  96. package/dist/server/worker-image-optimization.js +25 -6
  97. package/dist/server/worker-revalidation-context.js +1 -0
  98. package/dist/shims/cache-runtime.js +1 -1
  99. package/dist/shims/cache.js +6 -3
  100. package/dist/shims/client-page-root.js +2 -2
  101. package/dist/shims/error-boundary.d.ts +6 -2
  102. package/dist/shims/error-boundary.js +6 -4
  103. package/dist/shims/internal/navigation-untracked.js +1 -1
  104. package/dist/shims/metadata.js +1 -1
  105. package/dist/shims/navigation-server.js +1 -1
  106. package/dist/shims/navigation.js +4 -4
  107. package/dist/shims/navigation.react-server.js +3 -3
  108. package/dist/shims/request-context.d.ts +3 -1
  109. package/dist/shims/root-params.js +16 -2
  110. package/dist/shims/slot.js +3 -3
  111. package/dist/shims/url-utils.js +6 -1
  112. package/dist/utils/client-entry-manifest.d.ts +5 -0
  113. package/dist/utils/client-entry-manifest.js +1 -0
  114. package/dist/utils/client-runtime-metadata.d.ts +5 -0
  115. package/dist/utils/client-runtime-metadata.js +33 -7
  116. package/package.json +1 -1
@@ -0,0 +1,126 @@
1
+ //#region src/server/image-source-reader.ts
2
+ /**
3
+ * Streamed reads of an image source for the image optimization endpoint.
4
+ *
5
+ * Animation detection parses a GIF, PNG or WebP source as it streams in rather
6
+ * than buffering it, so a large source cannot exhaust a Worker isolate's
7
+ * memory. Only the source's first bytes are kept, to replay without a refetch.
8
+ */
9
+ /** Most bytes of a source kept to replay it after animation detection. */
10
+ const HELD_SOURCE_LIMIT = 1048576;
11
+ /** Thrown once a source grows past `images.maximumResponseBody`. */
12
+ var ImageSourceTooLargeError = class extends Error {
13
+ constructor() {
14
+ super("Image source exceeds images.maximumResponseBody");
15
+ }
16
+ };
17
+ var ImageSourceReader = class {
18
+ #reader;
19
+ #limit;
20
+ #read = 0;
21
+ #done = false;
22
+ #tooLarge = false;
23
+ #window = [];
24
+ #windowStart = 0;
25
+ #windowEnd = 0;
26
+ #held = [];
27
+ #replayed = false;
28
+ constructor(body, limit) {
29
+ this.#reader = body.getReader();
30
+ this.#limit = limit;
31
+ }
32
+ /** Whether the source has grown past the limit. */
33
+ get tooLarge() {
34
+ return this.#tooLarge;
35
+ }
36
+ /** The source's length once it has all been read and held, else null. */
37
+ get completeLength() {
38
+ return this.#done && this.#held ? this.#read : null;
39
+ }
40
+ /** Stop reading the source without waiting on its cancellation. */
41
+ cancel() {
42
+ this.#reader.cancel().catch(() => {});
43
+ }
44
+ /** Up to `length` bytes at `offset`, fewer where the source ends. */
45
+ async bytes(offset, length) {
46
+ const end = offset + length;
47
+ await this.#reach(offset, end);
48
+ const available = Math.min(end, this.#windowEnd);
49
+ const bytes = new Uint8Array(Math.max(0, available - offset));
50
+ let start = this.#windowStart;
51
+ for (const chunk of this.#window) {
52
+ if (start >= available) break;
53
+ const from = Math.max(offset - start, 0);
54
+ const to = Math.min(available - start, chunk.byteLength);
55
+ if (from < to) bytes.set(chunk.subarray(from, to), start + from - offset);
56
+ start += chunk.byteLength;
57
+ }
58
+ return bytes;
59
+ }
60
+ /** The byte at `offset`, or undefined past the source's end. */
61
+ async byte(offset) {
62
+ return (await this.bytes(offset, 1))[0];
63
+ }
64
+ /**
65
+ * The whole source as a stream, replaying the bytes already read, or null
66
+ * once they are no longer all held. An incompletely read source replays
67
+ * once; the rest streams from the source, still within the limit, and is
68
+ * held too while the whole source fits.
69
+ */
70
+ body() {
71
+ const held = this.#held;
72
+ if (held === null || this.#replayed && !this.#done) return null;
73
+ this.#replayed = true;
74
+ this.#window = [];
75
+ let index = 0;
76
+ return new ReadableStream({
77
+ pull: async (controller) => {
78
+ if (index < held.length) {
79
+ controller.enqueue(held[index++]);
80
+ return;
81
+ }
82
+ const chunk = await this.#next();
83
+ if (!chunk) return controller.close();
84
+ this.#hold(chunk);
85
+ index = held.length;
86
+ controller.enqueue(chunk);
87
+ },
88
+ cancel: (reason) => this.#reader.cancel(reason)
89
+ });
90
+ }
91
+ async #next() {
92
+ if (this.#done) return void 0;
93
+ const { done, value } = await this.#reader.read();
94
+ if (done) {
95
+ this.#done = true;
96
+ return;
97
+ }
98
+ this.#read += value.byteLength;
99
+ if (this.#read > this.#limit) {
100
+ this.#tooLarge = true;
101
+ this.cancel();
102
+ throw new ImageSourceTooLargeError();
103
+ }
104
+ return value;
105
+ }
106
+ #hold(chunk) {
107
+ if (!this.#held) return;
108
+ if (this.#read <= HELD_SOURCE_LIMIT) this.#held.push(chunk);
109
+ else this.#held = null;
110
+ }
111
+ /** Read until `end` is in the window, dropping chunks wholly before `offset`. */
112
+ async #reach(offset, end) {
113
+ if (offset < this.#windowStart) throw new Error("Image source reads must not move backwards");
114
+ for (;;) {
115
+ while (this.#window.length > 0 && this.#windowStart + this.#window[0].byteLength <= offset) this.#windowStart += this.#window.shift().byteLength;
116
+ if (this.#windowEnd >= end) return;
117
+ const chunk = await this.#next();
118
+ if (!chunk) return;
119
+ this.#hold(chunk);
120
+ this.#window.push(chunk);
121
+ this.#windowEnd += chunk.byteLength;
122
+ }
123
+ }
124
+ };
125
+ //#endregion
126
+ export { ImageSourceReader, ImageSourceTooLargeError };
@@ -21,6 +21,11 @@ type MetadataRouteRequestOptions = {
21
21
  isrRouteKey?: (pathname: string) => string;
22
22
  isrSet?: MetadataRouteCacheSetter;
23
23
  makeThenableParams: MetadataRouteMakeThenableParams;
24
+ /**
25
+ * The pathname params are read from. A direct request passes its raw path
26
+ * here and its decoded path as `cleanPathname`; a rewrite passes its
27
+ * destination, still percent-encoded, as both.
28
+ */
24
29
  routePathname?: string;
25
30
  scheduleBackgroundRegeneration?: MetadataRouteBackgroundRegenerator;
26
31
  };
@@ -36,6 +41,6 @@ export type PrerenderableMetadataRoute = {
36
41
  */
37
42
  export declare function getPrerenderableMetadataRoutePaths(metadataRoutes: readonly MetadataRuntimeRoute[]): Promise<PrerenderableMetadataRoute[]>;
38
43
  export declare function isMetadataRouteRequestPath(metadataRoutes: readonly MetadataRuntimeRoute[], cleanPathname: string): boolean;
39
- export declare function handleMetadataRouteRequest(options: MetadataRouteRequestOptions): Promise<Response | null>;
44
+ export declare function handleMetadataRouteRequest(requestOptions: MetadataRouteRequestOptions): Promise<Response | null>;
40
45
  //#endregion
41
46
  export {};
@@ -2,7 +2,7 @@ import { markFullyBufferedBody } from "./fully-buffered-response.js";
2
2
  import { closeAfterResponse, createRequestContext, runWithRequestContext } from "../shims/unified-request-context.js";
3
3
  import { VINEXT_METADATA_ROUTE_CACHE_HEADER } from "./headers.js";
4
4
  import { notFoundResponse } from "./http-error-responses.js";
5
- import { decodeMatchedParams } from "../routing/utils.js";
5
+ import { decodeMatchedParams, normalizePathnameForRouteMatch } from "../routing/utils.js";
6
6
  import { getRequestExecutionContext } from "../shims/request-context.js";
7
7
  import { runInWorkStoreScope } from "../shims/internal/work-store-scope.js";
8
8
  import { beginRouteCacheability, markRouteCacheabilityExplicitResponsePolicy, markRouteCacheabilityResponseBodyComplete, recordRouteCacheability } from "../shims/cacheability-classification.js";
@@ -16,7 +16,7 @@ import { _consumeRequestScopedCacheLife, _isOnDemandRevalidate } from "../shims/
16
16
  import { ensureFetchPatch, getCollectedFetchTags, setCurrentFetchSoftTags } from "../shims/fetch-cache.js";
17
17
  import { applyPrerenderCacheLifeHeader, applyPrerenderCacheTagsHeader } from "./prerender-cache-life-header.js";
18
18
  import { isValidMetadataImageId, manifestToJson, matchMetadataRoutePattern, robotsToText, sitemapToXml } from "./metadata-routes.js";
19
- import { buildAppRouteCacheValue, buildRouteHandlerCachedResponse } from "./app-route-handler-response.js";
19
+ import { buildAppRouteCacheValue, buildRouteHandlerCachedResponse, stripRouteHandlerFramingHeaders } from "./app-route-handler-response.js";
20
20
  import { canonicalizeAppPageParams } from "./app-page-segment-state.js";
21
21
  import { buildPageCacheTags } from "./implicit-tags.js";
22
22
  import { createStaticGenerationHeadersContext } from "./app-static-generation.js";
@@ -89,6 +89,20 @@ function getMetadataRouteFunctions(route) {
89
89
  routeFunctionCache.set(route, functions);
90
90
  return functions;
91
91
  }
92
+ /**
93
+ * Next.js compiles a metadata route into a Route Handler, and under a dynamic
94
+ * segment its build stores it only when the route's own module generates its
95
+ * params or is `force-static`/`error` (collectAppRouteSegments reads no parent
96
+ * layout). Its metadata loader adds generateStaticParams for
97
+ * generateImageMetadata. Probed on next@16.2.7: a parent layout's
98
+ * generateStaticParams, `revalidate` or `dynamicParams` leave the route
99
+ * dynamic. Generated sitemaps are only served below a static prefix.
100
+ */
101
+ function isMetadataRouteStaticGenEnabled(route) {
102
+ if (!route.isDynamic || !route.patternParts) return true;
103
+ const functions = getMetadataRouteFunctions(route);
104
+ return route.module?.dynamic === "force-static" || route.module?.dynamic === "error" || functions.generateStaticParams !== null || functions.generateImageMetadata !== null;
105
+ }
92
106
  function isUseCacheFunction(value) {
93
107
  return value !== null && Reflect.get(value, USE_CACHE_FUNCTION_SYMBOL) === true;
94
108
  }
@@ -165,7 +179,7 @@ async function captureRenderedMetadataRoute(response, route, cleanPathname) {
165
179
  ...observedCacheLife,
166
180
  revalidate: frameworkRevalidate
167
181
  };
168
- const cacheable = (_isOnDemandRevalidate() || response.status < 400 || response.status === 404) && completed && !isRenderDynamicLatched() && !getActiveDraftModeState() && !hasDraftModeCookieHeader() && route.module?.dynamic !== "force-dynamic" && frameworkRevalidate > 0 && !response.headers.has("set-cookie");
182
+ const cacheable = (_isOnDemandRevalidate() || response.status < 400 || response.status === 404) && completed && !isRenderDynamicLatched() && !getActiveDraftModeState() && !hasDraftModeCookieHeader() && route.module?.dynamic !== "force-dynamic" && isMetadataRouteStaticGenEnabled(route) && frameworkRevalidate > 0 && !response.headers.has("set-cookie");
169
183
  const cacheControl = cacheable ? buildAppRouteMissIsrCacheControl(frameworkRevalidate, cacheLife?.expire) : NEVER_CACHE_CONTROL;
170
184
  if (isUserResponse && !hasUserPolicy) {
171
185
  response = new Response(response.body, {
@@ -233,7 +247,7 @@ async function runMetadataRouteRegeneration(options, route, render, routeKey, pr
233
247
  }
234
248
  /** The outer ISR key for a matched metadata route, or null if it's never cached. */
235
249
  function getOuterMetadataCacheKey(options, route) {
236
- if (!isOuterMetadataCacheEnabled() || !options.isrRouteKey || getActiveDraftModeState() || hasDraftModeCookieHeader() || route.module?.dynamic === "force-dynamic" || route.module?.revalidate === 0) return null;
250
+ if (!isOuterMetadataCacheEnabled() || !options.isrRouteKey || getActiveDraftModeState() || hasDraftModeCookieHeader() || route.module?.dynamic === "force-dynamic" || route.module?.revalidate === 0 || !isMetadataRouteStaticGenEnabled(route)) return null;
237
251
  return options.isrRouteKey(options.cleanPathname);
238
252
  }
239
253
  /**
@@ -247,7 +261,7 @@ async function renderMetadataRouteMiss(options, route, render, cachedEntry) {
247
261
  const rendered = await render();
248
262
  if (!rendered) return null;
249
263
  await writeMetadataRouteMiss(options, route, rendered, cachedEntry);
250
- return rendered.response;
264
+ return stripRouteHandlerFramingHeaders(rendered.response);
251
265
  };
252
266
  const routeKey = _isOnDemandRevalidate() ? getOuterMetadataCacheKey(options, route) : null;
253
267
  if (routeKey === null || !options.isrSet) return renderAndStore();
@@ -501,7 +515,11 @@ async function writeMetadataRouteMiss(options, route, rendered, cachedEntry) {
501
515
  console.error("[vinext] Metadata route ISR cache write error:", error);
502
516
  }
503
517
  }
504
- async function handleMetadataRouteRequest(options) {
518
+ async function handleMetadataRouteRequest(requestOptions) {
519
+ const options = requestOptions.routePathname === requestOptions.cleanPathname ? {
520
+ ...requestOptions,
521
+ cleanPathname: normalizePathnameForRouteMatch(requestOptions.cleanPathname)
522
+ } : requestOptions;
505
523
  let urlParts;
506
524
  const getUrlParts = () => urlParts ??= options.cleanPathname.split("/").filter(Boolean);
507
525
  const routePathname = options.routePathname ?? options.cleanPathname;
@@ -5,6 +5,7 @@ export type MatcherConfig = string | Array<string | MiddlewareMatcherObject>;
5
5
  export type MiddlewareLocaleMatchContext = {
6
6
  kind: "defaulted";
7
7
  defaultLocale: string;
8
+ restoreRootTrailingSlash?: boolean;
8
9
  } | {
9
10
  kind: "internal" | "literal";
10
11
  } | {
@@ -60,7 +60,7 @@ function matchMatcherPattern(pathname, pattern, i18nConfig, localeContext) {
60
60
  function matchObjectMatcher(pathname, matcher, i18nConfig, localeContext) {
61
61
  if (localeContext?.kind === "next-data") return matchNextDataPattern(pathname, matcher.source, i18nConfig, matcher.locale !== false);
62
62
  if (matcher.locale !== false) return matchMatcherPattern(pathname, matcher.source, i18nConfig, localeContext);
63
- return matchRequestPattern(localeContext?.kind === "defaulted" ? `/${localeContext.defaultLocale}${pathname === "/" ? "" : pathname}` : pathname, matcher.source);
63
+ return matchRequestPattern(localeContext?.kind === "defaulted" ? `/${localeContext.defaultLocale}${pathname !== "/" ? pathname : localeContext.restoreRootTrailingSlash ? "/" : ""}` : pathname, matcher.source);
64
64
  }
65
65
  const APP_ROUTE_TRANSPORT_SUFFIX = "\\.rsc|\\.segments/.+\\.segment\\.rsc";
66
66
  const APP_ROUTE_TRANSPORT_SUFFIX_RE = /(?:\.rsc|\.segments\/.+\.segment\.rsc)$/;
@@ -61,6 +61,11 @@ type ExecuteMiddlewareOptions = {
61
61
  * second tee whose preserved side would never be consumed.
62
62
  */
63
63
  requestBodyAlreadyIsolated?: boolean;
64
+ /**
65
+ * `shouldRestoreLocaleRootTrailingSlash` for the raw request pathname. Next.js
66
+ * then tests the as-requested root as `/en/` after default-locale insertion.
67
+ */
68
+ restoreLocaleRootTrailingSlash?: boolean;
64
69
  request: Request;
65
70
  /**
66
71
  * The user's `trailingSlash` config. Plumbed into the NextRequest's NextURL
@@ -69,9 +69,26 @@ function middlewareMatcher(mod) {
69
69
  if (!isMiddlewareConfigExport(config)) return void 0;
70
70
  return config.matcher;
71
71
  }
72
+ const MIDDLEWARE_MESSAGE_HEADER_NAMES = [
73
+ "accept-encoding",
74
+ "connection",
75
+ "content-encoding",
76
+ "content-length",
77
+ "expect",
78
+ "keep-alive",
79
+ "keepalive",
80
+ "transfer-encoding"
81
+ ];
82
+ function deleteMiddlewareMessageHeaders(headers) {
83
+ for (const name of MIDDLEWARE_MESSAGE_HEADER_NAMES) headers.delete(name);
84
+ }
85
+ function processMiddlewareResponseHeaders(headers) {
86
+ processMiddlewareHeaders(headers);
87
+ deleteMiddlewareMessageHeaders(headers);
88
+ }
72
89
  function stripMiddlewareHeadersFromResponse(response) {
73
90
  const headers = new Headers(response.headers);
74
- processMiddlewareHeaders(headers);
91
+ processMiddlewareResponseHeaders(headers);
75
92
  return new Response(response.body, {
76
93
  status: response.status,
77
94
  statusText: response.statusText,
@@ -108,7 +125,7 @@ function relativizeLocation(location, requestUrl) {
108
125
  */
109
126
  function dataRedirectResponse(target, originalResponse) {
110
127
  const headers = new Headers(originalResponse.headers);
111
- processMiddlewareHeaders(headers);
128
+ processMiddlewareResponseHeaders(headers);
112
129
  headers.delete("Location");
113
130
  headers.set("x-nextjs-redirect", target);
114
131
  return new Response(null, {
@@ -120,6 +137,7 @@ function collectMiddlewareHeaders(response) {
120
137
  const responseHeaders = new Headers();
121
138
  for (const [key, value] of response.headers) if (!key.startsWith("x-middleware-") || shouldKeepMiddlewareHeader(key)) responseHeaders.append(key, value);
122
139
  dropEmptyMiddlewareSetCookie(responseHeaders);
140
+ deleteMiddlewareMessageHeaders(responseHeaders);
123
141
  return responseHeaders;
124
142
  }
125
143
  function drainFetchEvent(fetchEvent) {
@@ -209,12 +227,19 @@ async function executeMiddlewareModule(options) {
209
227
  };
210
228
  const isStaleDataRequest = options.isDataRequest === true && options.isStaleDataRequest === true;
211
229
  const toPageMatcherPathname = (candidate) => candidate !== null && options.isDataRequest && !isStaleDataRequest ? nextDataMiddlewareMatchPathname(candidate) ?? candidate : candidate;
212
- const encodedMatchPathname = toPageMatcherPathname(prepareMatcherPathname(encodedRequestPathname));
213
- let decodedMatchPathname = encodedMatchPathname;
214
- try {
215
- if (encodedMatchPathname !== null) decodedMatchPathname = decodeURIComponent(encodedMatchPathname);
216
- else if (!options.i18nConfig) decodedMatchPathname = toPageMatcherPathname(prepareMatcherPathname(decodeURIComponent(encodedRequestPathname)));
217
- } catch {}
230
+ const toMatchPathnames = (candidate) => {
231
+ const encoded = toPageMatcherPathname(prepareMatcherPathname(candidate));
232
+ let decoded = encoded;
233
+ try {
234
+ if (encoded !== null) decoded = decodeURIComponent(encoded);
235
+ else if (!options.i18nConfig) decoded = toPageMatcherPathname(prepareMatcherPathname(decodeURIComponent(candidate)));
236
+ } catch {}
237
+ return {
238
+ decoded,
239
+ encoded
240
+ };
241
+ };
242
+ const { decoded: decodedMatchPathname, encoded: encodedMatchPathname } = toMatchPathnames(encodedRequestPathname);
218
243
  let localeContext;
219
244
  if (isStaleDataRequest) localeContext = { kind: "next-data" };
220
245
  else if (options.i18nConfig && encodedMatchPathname !== null) {
@@ -226,12 +251,20 @@ async function executeMiddlewareModule(options) {
226
251
  kind: "defaulted"
227
252
  };
228
253
  }
229
- const encodedPathMatches = encodedMatchPathname !== null && matchesMiddlewarePathname(encodedMatchPathname, matcher, options.i18nConfig, localeContext);
230
- const decodedPathMatches = !encodedPathMatches && decodedMatchPathname !== null && decodedMatchPathname !== encodedMatchPathname && matchesMiddlewarePathname(decodedMatchPathname, matcher, options.i18nConfig, localeContext);
231
- if (encodedPathMatches || decodedPathMatches) options.onPathMatch?.();
232
- const encodedMatches = encodedMatchPathname !== null && matchesMiddleware(encodedMatchPathname, matcher, options.request, options.i18nConfig, localeContext);
233
- const decodedMatches = !encodedMatches && decodedMatchPathname !== null && decodedMatchPathname !== encodedMatchPathname && matchesMiddleware(decodedMatchPathname, matcher, options.request, options.i18nConfig, localeContext);
234
- if (!encodedMatches && !decodedMatches) return { continue: true };
254
+ const slashedRequestPathname = normalizePath(requestPathname);
255
+ const requestedMatchPathnames = slashedRequestPathname !== encodedRequestPathname ? toMatchPathnames(slashedRequestPathname) : {
256
+ decoded: decodedMatchPathname,
257
+ encoded: encodedMatchPathname
258
+ };
259
+ const candidatePathnames = [];
260
+ if (requestedMatchPathnames.encoded !== null) candidatePathnames.push(requestedMatchPathnames.encoded);
261
+ if (requestedMatchPathnames.decoded !== null && requestedMatchPathnames.decoded !== requestedMatchPathnames.encoded) candidatePathnames.push(requestedMatchPathnames.decoded);
262
+ const requestedLocaleContext = options.restoreLocaleRootTrailingSlash && localeContext?.kind === "defaulted" ? {
263
+ ...localeContext,
264
+ restoreRootTrailingSlash: true
265
+ } : localeContext;
266
+ if (candidatePathnames.some((candidate) => matchesMiddlewarePathname(candidate, matcher, options.i18nConfig, requestedLocaleContext))) options.onPathMatch?.();
267
+ if (!candidatePathnames.some((candidate) => matchesMiddleware(candidate, matcher, options.request, options.i18nConfig, requestedLocaleContext))) return { continue: true };
235
268
  options.onMatch?.();
236
269
  const middlewareUrl = withOrigin(requestUrl, getRequestExecutionContext()?.middlewareOrigin ?? requestUrl.origin);
237
270
  const nextRequest = createNextRequest(options.request, middlewareUrl, options.i18nConfig, options.basePath, options.trailingSlash, hadBasePath, options.requestBodyAlreadyIsolated, options.skipTrailingSlashRedirect);
@@ -305,7 +338,7 @@ async function executeMiddlewareModule(options) {
305
338
  }
306
339
  const responseHeaders = new Headers(response.headers);
307
340
  responseHeaders.delete("location");
308
- processMiddlewareHeaders(responseHeaders);
341
+ processMiddlewareResponseHeaders(responseHeaders);
309
342
  const relativizedResponseHeaders = new Headers(response.headers);
310
343
  relativizedResponseHeaders.set("Location", normalizedLocation);
311
344
  const relativizedResponse = new Response(response.body, {
@@ -44,6 +44,6 @@ export declare function findMiddlewareFile(root: string, fileMatcher: ValidFileM
44
44
  * @param request - The incoming Request object
45
45
  * @returns Middleware result describing what action to take
46
46
  */
47
- export declare function runMiddleware(runner: ModuleRunner, middlewarePath: string, request: Request, i18nConfig?: NextI18nConfig | null, basePath?: string, trailingSlash?: boolean, isDataRequest?: boolean, normalizedPathname?: string, isStaleDataRequest?: boolean, skipTrailingSlashRedirect?: boolean): Promise<MiddlewareResult>;
47
+ export declare function runMiddleware(runner: ModuleRunner, middlewarePath: string, request: Request, i18nConfig?: NextI18nConfig | null, basePath?: string, trailingSlash?: boolean, isDataRequest?: boolean, normalizedPathname?: string, isStaleDataRequest?: boolean, skipTrailingSlashRedirect?: boolean, restoreLocaleRootTrailingSlash?: boolean): Promise<MiddlewareResult>;
48
48
  //#endregion
49
49
  export { matchPattern, matchesMiddleware };
@@ -77,7 +77,7 @@ function isMiddlewareModule(value) {
77
77
  * @param request - The incoming Request object
78
78
  * @returns Middleware result describing what action to take
79
79
  */
80
- async function runMiddleware(runner, middlewarePath, request, i18nConfig, basePath, trailingSlash, isDataRequest, normalizedPathname, isStaleDataRequest, skipTrailingSlashRedirect) {
80
+ async function runMiddleware(runner, middlewarePath, request, i18nConfig, basePath, trailingSlash, isDataRequest, normalizedPathname, isStaleDataRequest, skipTrailingSlashRedirect, restoreLocaleRootTrailingSlash) {
81
81
  const mod = await runner.import(middlewarePath);
82
82
  if (!isMiddlewareModule(mod)) throw new Error(`Middleware module "${middlewarePath}" did not evaluate to an object.`);
83
83
  return runGeneratedMiddleware({
@@ -92,6 +92,7 @@ async function runMiddleware(runner, middlewarePath, request, i18nConfig, basePa
92
92
  module: mod,
93
93
  normalizedPathname,
94
94
  request,
95
+ restoreLocaleRootTrailingSlash,
95
96
  skipTrailingSlashRedirect,
96
97
  trailingSlash
97
98
  });
@@ -262,11 +262,12 @@ function createFreshFetchDecision(facts, reason) {
262
262
  }
263
263
  function classifyNavigationReuse(facts) {
264
264
  if (facts.navigationKind === "refresh") return createFreshFetchDecision(facts, "refresh");
265
- if (!facts.bypassNavigationCache && facts.visitedResponse.status === "available") return {
265
+ if (facts.bypassNavigationCache) return createFreshFetchDecision(facts, "cacheBypassed");
266
+ if (facts.visitedResponse.status === "available") return {
266
267
  kind: "reuseVisitedResponse",
267
268
  trace: createNavigationReuseTrace(NavigationTraceReasonCodes.visitedResponseReuse, facts)
268
269
  };
269
- if (!facts.bypassNavigationCache && facts.prefetch.status === "available") return {
270
+ if (facts.prefetch.status === "available") return {
270
271
  kind: "consumePrefetch",
271
272
  trace: createNavigationReuseTrace(NavigationTraceReasonCodes.prefetchResponseReuse, facts)
272
273
  };
@@ -275,9 +276,8 @@ function classifyNavigationReuse(facts) {
275
276
  kind: "attemptOptimisticRouteShell",
276
277
  trace: createNavigationReuseTrace(NavigationTraceReasonCodes.optimisticRouteShell, facts)
277
278
  };
278
- return createFreshFetchDecision(facts, facts.bypassNavigationCache ? "cacheBypassed" : facts.optimisticRouteShell.reason);
279
+ return createFreshFetchDecision(facts, facts.optimisticRouteShell.reason);
279
280
  }
280
- if (facts.bypassNavigationCache) return createFreshFetchDecision(facts, "cacheBypassed");
281
281
  return createFreshFetchDecision(facts, "cacheMiss");
282
282
  }
283
283
  function classifyNavigationPrefetchProbe(facts) {
@@ -676,10 +676,11 @@ function planFlightResponseArrived(options) {
676
676
  slotBindingSource: "manifestTarget",
677
677
  snapshot: targetSnapshot
678
678
  });
679
+ const renderedRootLayoutChanged = classifyRootBoundaryTransition(options.state.visibleSnapshot.rootBoundaryId, targetSnapshot.rootBoundaryId) === "rootBoundaryChanged";
679
680
  const traceFields = createRootBoundaryTraceFields({
680
- currentRootLayoutTreePath: currentTopology.kind === "known" ? currentTopology.topology.rootLayoutTreePath : null,
681
+ currentRootLayoutTreePath: renderedRootLayoutChanged ? options.state.visibleSnapshot.rootBoundaryId : currentTopology.kind === "known" ? currentTopology.topology.rootLayoutTreePath : null,
681
682
  event: options.event,
682
- nextRootLayoutTreePath: targetTopology.kind === "known" ? targetTopology.topology.rootLayoutTreePath : null,
683
+ nextRootLayoutTreePath: renderedRootLayoutChanged ? targetSnapshot.rootBoundaryId : targetTopology.kind === "known" ? targetTopology.topology.rootLayoutTreePath : null,
683
684
  state: options.state
684
685
  });
685
686
  if (options.event.token.lane === "prefetch") return {
@@ -708,6 +709,13 @@ function planFlightResponseArrived(options) {
708
709
  }
709
710
  const commitTraceFields = createAcceptedCacheProofTraceFields(traceFields, acceptedCacheEntryDecision);
710
711
  const cacheEntryProposalFields = createCacheEntryProposalFields(acceptedCacheEntryDecision);
712
+ if (renderedRootLayoutChanged) return {
713
+ kind: "hardNavigate",
714
+ reason: "rootBoundaryChanged",
715
+ token: options.event.token,
716
+ trace: createNavigationTrace(NavigationTraceReasonCodes.rootBoundaryChanged, commitTraceFields),
717
+ url: options.event.result.href
718
+ };
711
719
  if (targetSnapshot.interception !== null) {
712
720
  if (currentTopology.kind === "unknown" || targetTopology.kind === "unknown") return createInterceptionProofRejectedDecision({
713
721
  event: options.event,
@@ -0,0 +1,17 @@
1
+ //#region src/server/nitro-request-target-plugin.d.ts
2
+ /**
3
+ * Nitro runtime plugin that records each request's raw target before h3 sees
4
+ * it. srvx 0.11 writes the pathname h3 decodes (one with a `%`) back into the
5
+ * Node request's `url`, dropping the authority of an absolute-form target,
6
+ * so vinext's request-based entries read the target this plugin saved.
7
+ *
8
+ * This module runs in Nitro's server bundle, separate from vinext's request
9
+ * handlers, so it imports nothing and shares state through `Symbol.for`.
10
+ */
11
+ export declare const NITRO_REQUEST_TARGET_KEY = "vinext.nitro.requestTarget";
12
+ type NitroApp = {
13
+ fetch: (request: Request) => Response | Promise<Response>;
14
+ };
15
+ export default function nitroRequestTargetPlugin(nitroApp: NitroApp): void;
16
+ //#endregion
17
+ export {};
@@ -0,0 +1,22 @@
1
+ //#region src/server/nitro-request-target-plugin.ts
2
+ /**
3
+ * Nitro runtime plugin that records each request's raw target before h3 sees
4
+ * it. srvx 0.11 writes the pathname h3 decodes (one with a `%`) back into the
5
+ * Node request's `url`, dropping the authority of an absolute-form target,
6
+ * so vinext's request-based entries read the target this plugin saved.
7
+ *
8
+ * This module runs in Nitro's server bundle, separate from vinext's request
9
+ * handlers, so it imports nothing and shares state through `Symbol.for`.
10
+ */
11
+ const NITRO_REQUEST_TARGET_KEY = "vinext.nitro.requestTarget";
12
+ function nitroRequestTargetPlugin(nitroApp) {
13
+ const appFetch = nitroApp.fetch;
14
+ const key = Symbol.for(NITRO_REQUEST_TARGET_KEY);
15
+ nitroApp.fetch = (request) => {
16
+ const req = request.runtime?.node?.req;
17
+ if (req && !Reflect.has(req, key)) Reflect.set(req, key, req.url);
18
+ return appFetch(request);
19
+ };
20
+ }
21
+ //#endregion
22
+ export { NITRO_REQUEST_TARGET_KEY, nitroRequestTargetPlugin as default };
@@ -43,7 +43,8 @@ export type RepeatedSlashRedirect = {
43
43
  * `//` after the scheme trips the same check) and 500s `OPTIONS *`. Routing
44
44
  * them would let the request line choose the origin of `request.url` that
45
45
  * middleware and handlers see, bypassing the Host trust policy. Request-based
46
- * entries always pass a pathname.
46
+ * entries pass a pathname. Where srvx builds `request.url` from the raw target
47
+ * (Nitro's Node server), `isSrvxAbsoluteFormTarget` 404s these instead.
47
48
  *
48
49
  * @param rawUrl - The raw request target: pathname plus optional `?query`
49
50
  */
@@ -44,7 +44,8 @@ const REDIRECT_LOCATION_BASE = "http://vinext.invalid";
44
44
  * `//` after the scheme trips the same check) and 500s `OPTIONS *`. Routing
45
45
  * them would let the request line choose the origin of `request.url` that
46
46
  * middleware and handlers see, bypassing the Host trust policy. Request-based
47
- * entries always pass a pathname.
47
+ * entries pass a pathname. Where srvx builds `request.url` from the raw target
48
+ * (Nitro's Node server), `isSrvxAbsoluteFormTarget` 404s these instead.
48
49
  *
49
50
  * @param rawUrl - The raw request target: pathname plus optional `?query`
50
51
  */
@@ -4,6 +4,7 @@ export type AssetCrossOrigin = "" | "anonymous" | "use-credentials";
4
4
  export type PagesClientAssets = {
5
5
  clientEntry?: string;
6
6
  appBootstrapPreinitModules?: string[];
7
+ appBootstrapPreloadModules?: string[];
7
8
  appRouteManifestModule?: string;
8
9
  /** What each App Router client reference loads in the browser, by URL. */
9
10
  appClientReferenceChunks?: ClientReferenceChunks;
@@ -24,6 +24,13 @@ type PagesI18nRequestInfo = {
24
24
  domainLocale?: DomainLocale;
25
25
  redirectUrl?: string;
26
26
  };
27
+ /**
28
+ * Mark a locale-detection redirect. Next.js answers it before next.config
29
+ * headers are staged, so it goes out without them. The global symbol crosses
30
+ * the App Router / Pages Router module graph boundary.
31
+ */
32
+ export declare function markLocaleRedirect(response: Response): Response;
33
+ export declare function isLocaleRedirect(response: Response): boolean;
27
34
  /**
28
35
  * Return the configured locale that the first pathname segment names, compared
29
36
  * case-insensitively (`/EN/about` -> `"en"`), or `undefined` when there is none.
@@ -8,6 +8,19 @@ function readHeader(headers, name) {
8
8
  return direct;
9
9
  }
10
10
  const normalizeHostname = normalizeDomainHostname;
11
+ const LOCALE_REDIRECT = Symbol.for("vinext.pages-locale-redirect");
12
+ /**
13
+ * Mark a locale-detection redirect. Next.js answers it before next.config
14
+ * headers are staged, so it goes out without them. The global symbol crosses
15
+ * the App Router / Pages Router module graph boundary.
16
+ */
17
+ function markLocaleRedirect(response) {
18
+ if (!isLocaleRedirect(response)) Object.defineProperty(response, LOCALE_REDIRECT, { value: true });
19
+ return response;
20
+ }
21
+ function isLocaleRedirect(response) {
22
+ return LOCALE_REDIRECT in response;
23
+ }
11
24
  /**
12
25
  * Return the configured locale that the first pathname segment names, compared
13
26
  * case-insensitively (`/EN/about` -> `"en"`), or `undefined` when there is none.
@@ -259,4 +272,4 @@ function resolvePagesI18nRequest(url, i18nConfig, headers, hostname, basePath =
259
272
  };
260
273
  }
261
274
  //#endregion
262
- export { detectDomainLocale, detectLocaleFromAcceptLanguage, detectPathnameLocale, extractLocaleFromUrl, getLocaleRedirect, normalizeDefaultLocalePathname, parseCookieLocaleFromHeader, resolvePagesI18nRequest, shouldRestoreLocaleRootTrailingSlash, stripI18nLocaleForApiRoute };
275
+ export { detectDomainLocale, detectLocaleFromAcceptLanguage, detectPathnameLocale, extractLocaleFromUrl, getLocaleRedirect, isLocaleRedirect, markLocaleRedirect, normalizeDefaultLocalePathname, parseCookieLocaleFromHeader, resolvePagesI18nRequest, shouldRestoreLocaleRootTrailingSlash, stripI18nLocaleForApiRoute };
@@ -201,10 +201,14 @@ var PagesResponseStream = class extends Writable {
201
201
  return this;
202
202
  }
203
203
  json(data) {
204
- this.resHeaders["content-type"] = "application/json";
204
+ this.resHeaders["content-type"] = "application/json; charset=utf-8";
205
205
  this.end(JSON.stringify(data));
206
206
  }
207
207
  send(data) {
208
+ if (data === null || data === void 0) {
209
+ this.end();
210
+ return;
211
+ }
208
212
  if (NO_BODY_RESPONSE_STATUSES.has(this.resStatusCode)) {
209
213
  this.end();
210
214
  return;
@@ -220,8 +224,8 @@ var PagesResponseStream = class extends Writable {
220
224
  this.end(data);
221
225
  return;
222
226
  }
223
- if (typeof data === "object" && data !== null) {
224
- this.resHeaders["content-type"] = "application/json";
227
+ if (typeof data === "object" || typeof data === "number" || typeof data === "boolean") {
228
+ this.resHeaders["content-type"] = "application/json; charset=utf-8";
225
229
  this.end(JSON.stringify(data));
226
230
  return;
227
231
  }
@@ -5,7 +5,7 @@ import { NEXTJS_CACHE_HEADER, NEXTJS_DEPLOYMENT_ID_HEADER, VINEXT_CACHE_HEADER,
5
5
  import { normalizePathnameForRouteMatch } from "../routing/utils.js";
6
6
  import { patternToNextFormat } from "../routing/route-validation.js";
7
7
  import { getRequestExecutionContext } from "../shims/request-context.js";
8
- import { extractLocaleFromUrl, resolvePagesI18nRequest } from "./pages-i18n.js";
8
+ import { extractLocaleFromUrl, markLocaleRedirect, resolvePagesI18nRequest } from "./pages-i18n.js";
9
9
  import { buildNextDataNotFoundResponse, buildNextDataPropsJsonResponse, normalizePagesDataRequest, parseNextDataPathname } from "./pages-data-route.js";
10
10
  import { reportRequestError } from "./instrumentation.js";
11
11
  import { logRequestError } from "./request-error-log.js";
@@ -154,7 +154,7 @@ function createPagesPageHandler(opts) {
154
154
  const originalRequestPathAndSearch = originalRequestUrl.pathname + originalRequestUrl.search;
155
155
  let dataRequestPathname = null;
156
156
  let dataRequestSearch = "";
157
- const initialDataNorm = normalizePagesDataRequest(request, buildId, vinextConfig.basePath, hasMiddleware && vinextConfig.trailingSlash);
157
+ const initialDataNorm = normalizePagesDataRequest(request, buildId, "", hasMiddleware && vinextConfig.trailingSlash);
158
158
  if (!isDataReq) {
159
159
  if (initialDataNorm.notFoundResponse) return initialDataNorm.notFoundResponse;
160
160
  if (initialDataNorm.isDataReq) {
@@ -195,10 +195,10 @@ function createPagesPageHandler(opts) {
195
195
  const currentDefaultLocale = i18nConfig ? localeInfo.domainLocale ? localeInfo.domainLocale.defaultLocale : i18nConfig.defaultLocale : void 0;
196
196
  const domainLocales = i18nConfig ? i18nConfig.domains : void 0;
197
197
  const pageIsrCacheKey = isrCacheKeyForRequest(i18nConfig ? localeInfo.domainLocale ? JSON.stringify([String(localeInfo.domainLocale.domain).toLowerCase(), locale]) : "locale:" + String(locale) : null);
198
- if (localeInfo.redirectUrl) return new Response(null, {
198
+ if (localeInfo.redirectUrl) return markLocaleRedirect(new Response(null, {
199
199
  status: 307,
200
200
  headers: { Location: localeInfo.redirectUrl }
201
- });
201
+ }));
202
202
  let match = options && options.__forcedRoute ? {
203
203
  route: options.__forcedRoute,
204
204
  params: {}
@@ -15,6 +15,14 @@ type PageRouteMatch = {
15
15
  dataKind?: PagesRouteDataKind;
16
16
  };
17
17
  };
18
+ /**
19
+ * Whether a filesystem route may serve `requestPathname` from the client
20
+ * build directory. Like Next.js, only a public file or a build asset resolves
21
+ * there: the directory also holds build metadata and private artifacts. A
22
+ * direct GET/HEAD reaches this route only for a public file, since servers
23
+ * answer existing build assets before middleware.
24
+ */
25
+ export declare function isServableFilesystemRoute(requestPathname: string, phase: FilesystemRoutePhase, method: string, publicFiles: ReadonlySet<string>, basePath?: string, assetPathPrefix?: string, pathnameHasBasePath?: boolean): boolean;
18
26
  export declare function fetchWorkerFilesystemRoute(request: Request, requestPathname: string, phase: FilesystemRoutePhase, fetchAsset: (request: Request) => Promise<Response>, publicFiles: ReadonlySet<string>, basePath?: string, assetPathPrefix?: string, pathnameHasBasePath?: boolean): Promise<Response | false>;
19
27
  export type MiddlewareResult = {
20
28
  continue: boolean;
@@ -77,6 +85,7 @@ export type PagesPipelineDeps = {
77
85
  runMiddleware?: ((request: Request, ctx: unknown, opts: {
78
86
  isDataRequest: boolean;
79
87
  isStaleDataRequest?: boolean;
88
+ restoreLocaleRootTrailingSlash?: boolean;
80
89
  }) => Promise<MiddlewareResult>) | null;
81
90
  /** Original URL presented to middleware when URL normalization is disabled. */
82
91
  middlewareRequest?: Request;