vinext 1.4.1 → 1.4.3

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 (143) hide show
  1. package/README.md +5 -2
  2. package/dist/build/action-owner-manifest.js +1 -0
  3. package/dist/build/prerender-paths.d.ts +11 -1
  4. package/dist/build/prerender-paths.js +36 -10
  5. package/dist/check.js +1 -1
  6. package/dist/cli.js +1 -1
  7. package/dist/client/dev-error-overlay.d.ts +2 -1
  8. package/dist/client/dev-error-overlay.js +8 -3
  9. package/dist/config/config-matchers.js +8 -4
  10. package/dist/config/destination-params.js +5 -2
  11. package/dist/config/next-config.d.ts +25 -1
  12. package/dist/config/next-config.js +31 -1
  13. package/dist/entries/app-rsc-entry.js +65 -30
  14. package/dist/entries/app-rsc-manifest.js +4 -0
  15. package/dist/entries/pages-server-entry.js +19 -4
  16. package/dist/index.js +37 -37
  17. package/dist/init-cloudflare.js +18 -2
  18. package/dist/init-platform.js +2 -2
  19. package/dist/init.js +42 -5
  20. package/dist/plugins/css-target.d.ts +18 -0
  21. package/dist/plugins/css-target.js +93 -0
  22. package/dist/routing/app-route-graph.d.ts +10 -0
  23. package/dist/routing/app-route-graph.js +15 -1
  24. package/dist/server/api-handler.d.ts +2 -0
  25. package/dist/server/api-handler.js +10 -8
  26. package/dist/server/app-action-forwarding.js +3 -1
  27. package/dist/server/app-browser-entry.js +37 -41
  28. package/dist/server/app-browser-error.d.ts +1 -1
  29. package/dist/server/app-browser-error.js +13 -4
  30. package/dist/server/app-browser-supplemental-refresh.d.ts +13 -0
  31. package/dist/server/app-browser-supplemental-refresh.js +36 -4
  32. package/dist/server/app-elements.d.ts +3 -0
  33. package/dist/server/app-elements.js +14 -1
  34. package/dist/server/app-fallback-renderer.js +2 -2
  35. package/dist/server/app-middleware.d.ts +2 -0
  36. package/dist/server/app-middleware.js +1 -0
  37. package/dist/server/app-optimistic-routing.d.ts +12 -1
  38. package/dist/server/app-optimistic-routing.js +38 -16
  39. package/dist/server/app-page-boundary-render.js +4 -2
  40. package/dist/server/app-page-cache-render.js +1 -1
  41. package/dist/server/app-page-dispatch.d.ts +11 -1
  42. package/dist/server/app-page-dispatch.js +78 -31
  43. package/dist/server/app-page-element-builder.d.ts +11 -20
  44. package/dist/server/app-page-element-builder.js +39 -50
  45. package/dist/server/app-page-intercept-segment-config.d.ts +56 -0
  46. package/dist/server/app-page-intercept-segment-config.js +90 -0
  47. package/dist/server/app-page-navigation-params.d.ts +23 -0
  48. package/dist/server/app-page-navigation-params.js +49 -0
  49. package/dist/server/app-page-render.d.ts +5 -2
  50. package/dist/server/app-page-render.js +17 -3
  51. package/dist/server/app-page-request.d.ts +24 -2
  52. package/dist/server/app-page-request.js +10 -10
  53. package/dist/server/app-page-route-wiring.d.ts +17 -5
  54. package/dist/server/app-page-route-wiring.js +19 -13
  55. package/dist/server/app-page-segment-plan.js +9 -1
  56. package/dist/server/app-page-segment-state.d.ts +9 -0
  57. package/dist/server/app-page-segment-state.js +54 -1
  58. package/dist/server/app-pages-bridge.d.ts +6 -0
  59. package/dist/server/app-pages-bridge.js +8 -3
  60. package/dist/server/app-request-stage-independent-entry.js +2 -3
  61. package/dist/server/app-response-stage-entry.js +1 -0
  62. package/dist/server/app-route-module-loader.d.ts +2 -0
  63. package/dist/server/app-route-module-loader.js +1 -0
  64. package/dist/server/app-router-entry.js +2 -3
  65. package/dist/server/app-rsc-cache-busting.d.ts +8 -0
  66. package/dist/server/app-rsc-cache-busting.js +13 -1
  67. package/dist/server/app-rsc-combined-handler.js +2 -1
  68. package/dist/server/app-rsc-handler.d.ts +14 -1
  69. package/dist/server/app-rsc-handler.js +111 -62
  70. package/dist/server/app-rsc-response-finalizer.d.ts +18 -3
  71. package/dist/server/app-rsc-response-finalizer.js +61 -7
  72. package/dist/server/app-rsc-route-matching.d.ts +14 -0
  73. package/dist/server/app-rsc-route-matching.js +42 -18
  74. package/dist/server/app-server-action-execution.js +16 -10
  75. package/dist/server/app-ssr-entry.js +8 -0
  76. package/dist/server/cache-control.d.ts +2 -0
  77. package/dist/server/cache-control.js +3 -1
  78. package/dist/server/config-headers.d.ts +7 -0
  79. package/dist/server/config-headers.js +20 -1
  80. package/dist/server/image-animation.d.ts +9 -0
  81. package/dist/server/image-animation.js +118 -0
  82. package/dist/server/image-local-patterns.d.ts +21 -0
  83. package/dist/server/image-local-patterns.js +59 -0
  84. package/dist/server/image-optimization.d.ts +48 -27
  85. package/dist/server/image-optimization.js +316 -22
  86. package/dist/server/image-source-reader.d.ts +34 -0
  87. package/dist/server/image-source-reader.js +126 -0
  88. package/dist/server/metadata-route-response.d.ts +6 -1
  89. package/dist/server/metadata-route-response.js +46 -10
  90. package/dist/server/middleware-matcher.d.ts +1 -0
  91. package/dist/server/middleware-matcher.js +1 -1
  92. package/dist/server/middleware-runtime.d.ts +5 -0
  93. package/dist/server/middleware-runtime.js +48 -15
  94. package/dist/server/middleware.d.ts +1 -1
  95. package/dist/server/middleware.js +2 -1
  96. package/dist/server/nitro-public-files-plugin.js +20 -8
  97. package/dist/server/nitro-request-target-plugin.d.ts +17 -0
  98. package/dist/server/nitro-request-target-plugin.js +22 -0
  99. package/dist/server/open-redirect.d.ts +2 -1
  100. package/dist/server/open-redirect.js +2 -1
  101. package/dist/server/pages-client-assets.d.ts +1 -0
  102. package/dist/server/pages-data-route.d.ts +5 -1
  103. package/dist/server/pages-data-route.js +14 -6
  104. package/dist/server/pages-i18n.d.ts +7 -0
  105. package/dist/server/pages-i18n.js +14 -1
  106. package/dist/server/pages-node-compat.js +7 -3
  107. package/dist/server/pages-page-handler.js +4 -4
  108. package/dist/server/pages-request-pipeline.d.ts +9 -0
  109. package/dist/server/pages-request-pipeline.js +35 -15
  110. package/dist/server/pages-request-stage-entry.js +8 -2
  111. package/dist/server/pages-router-entry.js +3 -0
  112. package/dist/server/prod-server.d.ts +1 -1
  113. package/dist/server/prod-server.js +64 -62
  114. package/dist/server/request-pipeline.d.ts +5 -0
  115. package/dist/server/request-pipeline.js +11 -1
  116. package/dist/server/response-stage-cacheability.d.ts +7 -0
  117. package/dist/server/response-stage-cacheability.js +6 -5
  118. package/dist/server/srvx-middleware-origin.d.ts +13 -0
  119. package/dist/server/srvx-middleware-origin.js +19 -0
  120. package/dist/server/srvx-request-target.d.ts +12 -0
  121. package/dist/server/srvx-request-target.js +20 -0
  122. package/dist/server/static-file-cache.js +10 -22
  123. package/dist/server/worker-image-optimization.d.ts +10 -3
  124. package/dist/server/worker-image-optimization.js +43 -9
  125. package/dist/server/worker-revalidation-context.js +1 -0
  126. package/dist/server/worker-utils.d.ts +8 -0
  127. package/dist/server/worker-utils.js +19 -2
  128. package/dist/shims/error-boundary.d.ts +6 -2
  129. package/dist/shims/error-boundary.js +4 -2
  130. package/dist/shims/image-props.d.ts +6 -0
  131. package/dist/shims/image-props.js +39 -3
  132. package/dist/shims/image.js +3 -3
  133. package/dist/shims/request-context.d.ts +3 -1
  134. package/dist/shims/slot.d.ts +3 -0
  135. package/dist/shims/slot.js +67 -9
  136. package/dist/shims/url-utils.js +6 -1
  137. package/dist/utils/base-path.d.ts +7 -0
  138. package/dist/utils/base-path.js +11 -1
  139. package/dist/utils/client-entry-manifest.d.ts +5 -0
  140. package/dist/utils/client-entry-manifest.js +1 -0
  141. package/dist/utils/client-runtime-metadata.d.ts +5 -0
  142. package/dist/utils/client-runtime-metadata.js +33 -7
  143. 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";
@@ -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
  /**
@@ -501,23 +515,41 @@ 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);
525
+ const routePathname = options.routePathname ?? options.cleanPathname;
526
+ let routeUrlParts;
527
+ const getRouteUrlParts = () => routeUrlParts ??= routePathname.split("/").filter(Boolean);
528
+ let cacheOptions = options;
529
+ const skipDecodedOwner = () => {
530
+ cacheOptions = {
531
+ ...options,
532
+ isrGet: void 0,
533
+ isrRouteKey: void 0,
534
+ isrSet: void 0
535
+ };
536
+ };
507
537
  for (const route of options.metadataRoutes) {
508
538
  const functions = getMetadataRouteFunctions(route);
509
539
  if (route.type === "sitemap" && route.isDynamic) {
510
540
  if (functions.generateSitemaps) {
511
- if (isGeneratedSitemapPath(route, options.cleanPathname)) {
541
+ if (!isGeneratedSitemapPath(route, options.cleanPathname)) continue;
542
+ if (!isGeneratedSitemapPath(route, routePathname)) skipDecodedOwner();
543
+ else {
512
544
  beginMetadataRouteCacheability(route);
513
545
  const render = async () => {
514
546
  setCurrentFetchSoftTags(buildMetadataRouteTags(route, options.cleanPathname, []));
515
547
  const response = await handleGeneratedSitemap(route, options.cleanPathname, functions);
516
548
  return response ? captureRenderedMetadataRoute(response, route, options.cleanPathname) : null;
517
549
  };
518
- const cached = await readMatchedPrerenderedMetadataRouteResponse(options, route, render);
550
+ const cached = await readMatchedPrerenderedMetadataRouteResponse(cacheOptions, route, render);
519
551
  if (cached.response) return cached.response;
520
- const response = await renderMetadataRouteMiss(options, route, render, cached.cachedEntry);
552
+ const response = await renderMetadataRouteMiss(cacheOptions, route, render, cached.cachedEntry);
521
553
  if (response) return response;
522
554
  }
523
555
  continue;
@@ -525,8 +557,12 @@ async function handleMetadataRouteRequest(options) {
525
557
  }
526
558
  const match = matchMetadataRoute(route, options.cleanPathname, functions, getUrlParts);
527
559
  if (!match) continue;
560
+ if (!matchMetadataRoute(route, routePathname, functions, getRouteUrlParts)) {
561
+ skipDecodedOwner();
562
+ continue;
563
+ }
528
564
  beginMetadataRouteCacheability(route);
529
- const rawParams = route.patternParts ? metadataRouteRawParams(route, match, options.routePathname ?? options.cleanPathname) : Object.create(null);
565
+ const rawParams = route.patternParts ? metadataRouteRawParams(route, match, routePathname) : Object.create(null);
530
566
  if (route.patternParts) {
531
567
  const runtimeParams = { ...rawParams };
532
568
  decodeMatchedParams(runtimeParams);
@@ -536,7 +572,7 @@ async function handleMetadataRouteRequest(options) {
536
572
  setCurrentFetchSoftTags(buildMetadataRouteTags(route, options.cleanPathname, []));
537
573
  return captureRenderedMetadataRoute(route.isDynamic ? await callDynamicMetadataRoute(route, match, options.makeThenableParams, functions) : serveStaticMetadataRoute(route), route, options.cleanPathname);
538
574
  };
539
- const cached = await readMatchedPrerenderedMetadataRouteResponse(options, route, render);
575
+ const cached = await readMatchedPrerenderedMetadataRouteResponse(cacheOptions, route, render);
540
576
  if (cached.response) return cached.response;
541
577
  if (route.isDynamic && isImageMetadataRoute(route) && functions.dynamicParams === false) {
542
578
  const validationParams = { ...rawParams };
@@ -552,7 +588,7 @@ async function handleMetadataRouteRequest(options) {
552
588
  });
553
589
  if (dynamicParamsResponse) return dynamicParamsResponse;
554
590
  }
555
- return renderMetadataRouteMiss(options, route, render, cached.cachedEntry);
591
+ return renderMetadataRouteMiss(cacheOptions, route, render, cached.cachedEntry);
556
592
  }
557
593
  return null;
558
594
  }
@@ -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
  });
@@ -53,17 +53,26 @@ function toH3Pathname(pathname) {
53
53
  }
54
54
  }
55
55
  /**
56
+ * The key Nitro's static handler looks a pathname up under, following
57
+ * `runtime/internal/static.mjs`: without one trailing slash and decoded
58
+ * except for `%2F`.
59
+ */
60
+ function toAssetId(pathname) {
61
+ const id = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
62
+ try {
63
+ return decodeURIComponent(id.replace(/%2f/gi, "%252F"));
64
+ } catch {
65
+ return id;
66
+ }
67
+ }
68
+ /**
56
69
  * Whether Nitro's static handler would answer a request for a pathname with
57
- * a public file, following its lookup in `runtime/internal/static.mjs`. It
58
- * looks h3's pathname up without one trailing slash and decoded except for
59
- * `%2F`, and then as a directory with an `index.html`, each first with the
60
- * extension of a compressed copy for every encoding the request accepts.
70
+ * a public file. It looks h3's pathname up under its key, and then as a
71
+ * directory with an `index.html`, each first with the extension of a
72
+ * compressed copy for every encoding the request accepts.
61
73
  */
62
74
  function isPublicFilePathname(pathname, extensions, publicAssets) {
63
- let id = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
64
- try {
65
- id = decodeURIComponent(id.replace(/%2f/gi, "%252F"));
66
- } catch {}
75
+ const id = toAssetId(pathname);
67
76
  const index = id.endsWith("/") ? `${id}index.html` : `${id}/index.html`;
68
77
  return extensions.some((extension) => publicAssets.has(id + extension) || publicAssets.has(index + extension));
69
78
  }
@@ -86,6 +95,9 @@ function createNitroPublicFilesPlugin(options) {
86
95
  const token = crypto.randomUUID();
87
96
  Reflect.set(globalThis, Symbol.for(NITRO_ASSETS_KEY), {
88
97
  async fetch(request) {
98
+ const { pathname } = new URL(request.url);
99
+ const h3Pathname = toH3Pathname(pathname);
100
+ if (h3Pathname === void 0 || toAssetId(h3Pathname) !== toAssetId(pathname)) return new Response(null, { status: 404 });
89
101
  const headers = new Headers(request.headers);
90
102
  headers.set(ASSET_REQUEST_HEADER, token);
91
103
  return appFetch(new Request(request, { headers }));
@@ -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;
@@ -59,8 +59,12 @@ export declare function encodeUrlParserIgnoredCharacters(pathname: string): stri
59
59
  * `/_next/data/<buildId>/en.json` → `/en`
60
60
  * `/_next/data/<wrong-id>/about.json` → null
61
61
  * `/_next/data/<buildId>/about` → null (missing .json suffix)
62
+ * `/_next/data/<buildId>/blog/...json` → null (strips to a dot segment)
63
+ *
64
+ * `decoded` marks a percent-decoded copy of a pathname whose raw form already
65
+ * parsed: a `%2e` left in it was `%252e` on the wire, an ordinary character.
62
66
  */
63
- export declare function parseNextDataPathname(pathname: string, buildId: string): NextDataMatch | null;
67
+ export declare function parseNextDataPathname(pathname: string, buildId: string, decoded?: boolean): NextDataMatch | null;
64
68
  /**
65
69
  * The page pathname a `_next/data` URL addresses, whatever its buildId. Next.js
66
70
  * compiles every middleware matcher with an optional `/_next/data/<buildId>`
@@ -1,7 +1,7 @@
1
1
  import { MIDDLEWARE_SKIP_HEADER } from "../utils/protocol-headers.js";
2
- import { addBasePathToPathname, hasBasePath, stripBasePath } from "../utils/base-path.js";
2
+ import { hasBasePath, restoreBasePath, stripBasePath } from "../utils/base-path.js";
3
3
  import { NEXTJS_DEPLOYMENT_ID_HEADER } from "./headers.js";
4
- import { cloneRequestWithUrl } from "./request-pipeline.js";
4
+ import { cloneRequestWithUrl, hasUrlParserDotSegment } from "./request-pipeline.js";
5
5
  //#region src/server/pages-data-route.ts
6
6
  /**
7
7
  * Helpers for the Pages Router `/_next/data/{buildId}/{...page}.json` endpoint.
@@ -62,13 +62,19 @@ function encodeUrlParserIgnoredCharacters(pathname) {
62
62
  * `/_next/data/<buildId>/en.json` → `/en`
63
63
  * `/_next/data/<wrong-id>/about.json` → null
64
64
  * `/_next/data/<buildId>/about` → null (missing .json suffix)
65
+ * `/_next/data/<buildId>/blog/...json` → null (strips to a dot segment)
66
+ *
67
+ * `decoded` marks a percent-decoded copy of a pathname whose raw form already
68
+ * parsed: a `%2e` left in it was `%252e` on the wire, an ordinary character.
65
69
  */
66
- function parseNextDataPathname(pathname, buildId) {
70
+ function parseNextDataPathname(pathname, buildId, decoded = false) {
67
71
  if (!buildId) return null;
68
72
  if (!isNextDataPathname(pathname)) return null;
69
73
  const expectedPrefix = `${NEXT_DATA_PREFIX}${buildId}/`;
70
74
  if (!pathname.startsWith(expectedPrefix)) return null;
71
- return pageFromNextDataRest(pathname.slice(expectedPrefix.length, -5));
75
+ const rest = pathname.slice(expectedPrefix.length, -5);
76
+ if (!decoded && hasUrlParserDotSegment(rest)) return null;
77
+ return pageFromNextDataRest(rest);
72
78
  }
73
79
  /**
74
80
  * The page pathname a `_next/data` URL addresses, whatever its buildId. Next.js
@@ -83,7 +89,9 @@ function nextDataMiddlewareMatchPathname(pathname) {
83
89
  if (!isNextDataPathname(pathname)) return null;
84
90
  const buildIdEnd = pathname.indexOf("/", 12);
85
91
  if (buildIdEnd <= 12) return null;
86
- return pageFromNextDataRest(pathname.slice(buildIdEnd + 1, -5))?.pagePathname ?? null;
92
+ const rest = pathname.slice(buildIdEnd + 1, -5);
93
+ if (hasUrlParserDotSegment(rest)) return null;
94
+ return pageFromNextDataRest(rest)?.pagePathname ?? null;
87
95
  }
88
96
  function pageFromNextDataRest(rest) {
89
97
  if (rest.length === 0) return null;
@@ -189,7 +197,7 @@ function normalizePagesDataRequest(request, buildId, basePath = "", trailingSlas
189
197
  };
190
198
  const pagePathname = normalizeNextDataPagePathname(dataMatch.pagePathname, trailingSlash);
191
199
  const normalizedUrl = new URL(reqUrl);
192
- normalizedUrl.pathname = hadBasePath ? addBasePathToPathname(pagePathname, basePath) : pagePathname;
200
+ normalizedUrl.pathname = hadBasePath ? restoreBasePath(pagePathname, basePath) : pagePathname;
193
201
  return {
194
202
  isDataReq: true,
195
203
  request: cloneRequestWithUrl(request, normalizedUrl.href),
@@ -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.