vinext 1.1.0 → 1.2.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 (288) hide show
  1. package/README.md +22 -10
  2. package/dist/build/client-build-config.d.ts +10 -0
  3. package/dist/build/client-build-config.js +18 -2
  4. package/dist/build/output-root-externals.d.ts +21 -0
  5. package/dist/build/output-root-externals.js +40 -0
  6. package/dist/build/pages-client-assets-module.d.ts +8 -0
  7. package/dist/build/pages-client-assets-module.js +21 -1
  8. package/dist/build/prerender-paths.js +6 -4
  9. package/dist/build/prerender.js +1 -1
  10. package/dist/build/run-prerender.js +1 -1
  11. package/dist/build/ssr-manifest.d.ts +2 -0
  12. package/dist/build/ssr-manifest.js +6 -2
  13. package/dist/check.js +12 -12
  14. package/dist/cli-dev-config.js +3 -3
  15. package/dist/cli.js +3 -2
  16. package/dist/client/client-redirect-matcher.d.ts +14 -0
  17. package/dist/client/client-redirect-matcher.js +37 -0
  18. package/dist/client/client-redirects.d.ts +27 -0
  19. package/dist/client/client-redirects.js +28 -0
  20. package/dist/client/client-rewrites.d.ts +2 -1
  21. package/dist/client/client-rewrites.js +1 -1
  22. package/dist/config/config-matchers.d.ts +26 -11
  23. package/dist/config/config-matchers.js +90 -88
  24. package/dist/config/destination-params.d.ts +47 -0
  25. package/dist/config/destination-params.js +116 -0
  26. package/dist/config/next-config.d.ts +7 -0
  27. package/dist/config/next-config.js +3 -0
  28. package/dist/entries/app-rsc-entry.d.ts +4 -0
  29. package/dist/entries/app-rsc-entry.js +20 -6
  30. package/dist/entries/pages-client-entry.js +3 -1
  31. package/dist/entries/pages-server-entry.js +13 -37
  32. package/dist/index.d.ts +3 -1
  33. package/dist/index.js +246 -136
  34. package/dist/init-cloudflare.d.ts +9 -0
  35. package/dist/init-cloudflare.js +107 -31
  36. package/dist/init-platform.d.ts +4 -0
  37. package/dist/init-platform.js +27 -2
  38. package/dist/init.d.ts +2 -1
  39. package/dist/init.js +153 -9
  40. package/dist/plugins/ast-scope.d.ts +7 -0
  41. package/dist/plugins/ast-scope.js +59 -3
  42. package/dist/plugins/ast-utils.d.ts +53 -2
  43. package/dist/plugins/ast-utils.js +90 -2
  44. package/dist/plugins/cloudflare-dev-module-urls.d.ts +41 -0
  45. package/dist/plugins/cloudflare-dev-module-urls.js +271 -0
  46. package/dist/plugins/commonjs-esm-facade.js +2 -2
  47. package/dist/plugins/css-module-imports.js +13 -20
  48. package/dist/plugins/dynamic-preload-metadata.d.ts +1 -1
  49. package/dist/plugins/dynamic-preload-metadata.js +11 -11
  50. package/dist/plugins/extensionless-dynamic-import.js +2 -3
  51. package/dist/plugins/fonts.d.ts +1 -1
  52. package/dist/plugins/fonts.js +3 -3
  53. package/dist/plugins/ignore-dynamic-requests.d.ts +1 -1
  54. package/dist/plugins/ignore-dynamic-requests.js +5 -7
  55. package/dist/plugins/import-meta-url.js +3 -4
  56. package/dist/plugins/instrumentation-client.js +3 -2
  57. package/dist/plugins/middleware-export-validation.js +2 -3
  58. package/dist/plugins/middleware-server-only.d.ts +5 -4
  59. package/dist/plugins/middleware-server-only.js +8 -5
  60. package/dist/plugins/og-harfbuzz.js +2 -3
  61. package/dist/plugins/optimize-imports.js +3 -4
  62. package/dist/plugins/pages-node-externals.js +3 -4
  63. package/dist/plugins/remove-console.js +2 -18
  64. package/dist/plugins/require-condition-resolution.js +3 -3
  65. package/dist/plugins/require-context.js +2 -3
  66. package/dist/plugins/rsc-scan-glob-imports.d.ts +19 -0
  67. package/dist/plugins/rsc-scan-glob-imports.js +50 -0
  68. package/dist/plugins/strip-server-exports.js +265 -102
  69. package/dist/plugins/styled-jsx.js +2 -3
  70. package/dist/plugins/transform-result.d.ts +9 -5
  71. package/dist/plugins/transitive-externals.js +1 -1
  72. package/dist/plugins/typeof-window.d.ts +2 -2
  73. package/dist/plugins/use-cache-callable.d.ts +21 -2
  74. package/dist/plugins/use-cache-callable.js +84 -35
  75. package/dist/plugins/worker-image-imports.js +30 -33
  76. package/dist/plugins/worker-server-only-imports.d.ts +20 -0
  77. package/dist/plugins/worker-server-only-imports.js +31 -0
  78. package/dist/plugins/worker-use-server.d.ts +25 -0
  79. package/dist/plugins/worker-use-server.js +153 -0
  80. package/dist/server/api-handler.d.ts +1 -0
  81. package/dist/server/api-handler.js +3 -2
  82. package/dist/server/app-action-forwarding.js +2 -2
  83. package/dist/server/app-browser-action-result.d.ts +1 -1
  84. package/dist/server/app-browser-action-result.js +9 -2
  85. package/dist/server/app-browser-entry.js +3 -3
  86. package/dist/server/app-browser-interception-context.js +1 -1
  87. package/dist/server/app-browser-navigation-controller.js +1 -1
  88. package/dist/server/app-browser-prefetch-response.js +2 -2
  89. package/dist/server/app-browser-server-action-client.js +1 -1
  90. package/dist/server/app-browser-state.js +1 -1
  91. package/dist/server/app-browser-supplemental-refresh.js +1 -1
  92. package/dist/server/app-middleware.d.ts +11 -0
  93. package/dist/server/app-middleware.js +17 -2
  94. package/dist/server/app-optimistic-routing.js +1 -1
  95. package/dist/server/app-page-cache.d.ts +1 -1
  96. package/dist/server/app-page-cache.js +1 -1
  97. package/dist/server/app-page-dispatch.d.ts +3 -1
  98. package/dist/server/app-page-dispatch.js +5 -4
  99. package/dist/server/app-page-ppr-runtime.js +1 -1
  100. package/dist/server/app-page-render-observation.js +1 -1
  101. package/dist/server/app-page-render.js +1 -1
  102. package/dist/server/app-page-request.js +53 -15
  103. package/dist/server/app-page-route-wiring.js +2 -2
  104. package/dist/server/app-pages-bridge.d.ts +1 -1
  105. package/dist/server/app-pages-bridge.js +3 -4
  106. package/dist/server/app-prerender-endpoints.js +9 -6
  107. package/dist/server/app-render-abort-error.d.ts +19 -0
  108. package/dist/server/app-render-abort-error.js +31 -1
  109. package/dist/server/app-request-stage-dispatch.js +6 -2
  110. package/dist/server/app-request-stage-independent-entry.js +29 -9
  111. package/dist/server/app-response-stage-entry.js +11 -3
  112. package/dist/server/app-route-handler-cache.d.ts +1 -0
  113. package/dist/server/app-route-handler-cache.js +1 -0
  114. package/dist/server/app-route-handler-dispatch.d.ts +1 -0
  115. package/dist/server/app-route-handler-dispatch.js +5 -2
  116. package/dist/server/app-route-handler-execution.d.ts +1 -0
  117. package/dist/server/app-route-handler-execution.js +1 -0
  118. package/dist/server/app-route-handler-runtime.d.ts +4 -1
  119. package/dist/server/app-route-handler-runtime.js +46 -25
  120. package/dist/server/app-router-entry.js +29 -10
  121. package/dist/server/app-rsc-cache-busting.d.ts +16 -0
  122. package/dist/server/app-rsc-cache-busting.js +30 -13
  123. package/dist/server/app-rsc-errors.js +20 -16
  124. package/dist/server/app-rsc-handler.d.ts +7 -0
  125. package/dist/server/app-rsc-handler.js +89 -39
  126. package/dist/server/app-rsc-request-normalization.js +3 -3
  127. package/dist/server/app-rsc-response-stage.js +5 -3
  128. package/dist/server/app-segment-config.d.ts +2 -1
  129. package/dist/server/app-server-action-execution.d.ts +2 -1
  130. package/dist/server/app-server-action-execution.js +62 -23
  131. package/dist/server/app-ssr-entry.js +7 -5
  132. package/dist/server/app-visited-response-cache.js +2 -2
  133. package/dist/server/app-worker-stages.d.ts +5 -0
  134. package/dist/server/app-worker-stages.js +1 -1
  135. package/dist/server/cacheability-request.js +2 -2
  136. package/dist/server/client-build-files.d.ts +16 -0
  137. package/dist/server/client-build-files.js +36 -0
  138. package/dist/server/client-reuse-manifest.js +1 -1
  139. package/dist/server/config-headers.js +1 -1
  140. package/dist/server/defer-until-stream-consumed.d.ts +2 -1
  141. package/dist/server/defer-until-stream-consumed.js +3 -1
  142. package/dist/server/dev-server.js +39 -28
  143. package/dist/server/dev-stack-sourcemap.js +18 -23
  144. package/dist/server/document-initial-head.d.ts +0 -11
  145. package/dist/server/document-initial-head.js +1 -1
  146. package/dist/server/forwarded-middleware-context.d.ts +5 -0
  147. package/dist/server/forwarded-middleware-context.js +22 -0
  148. package/dist/server/headers.d.ts +4 -1
  149. package/dist/server/headers.js +4 -1
  150. package/dist/server/http-conditional.d.ts +11 -0
  151. package/dist/server/http-conditional.js +10 -1
  152. package/dist/server/http-error-responses.d.ts +1 -1
  153. package/dist/server/image-optimization.d.ts +38 -0
  154. package/dist/server/image-optimization.js +94 -4
  155. package/dist/server/isr-cache.d.ts +10 -0
  156. package/dist/server/isr-cache.js +41 -6
  157. package/dist/server/metadata-route-response.js +11 -10
  158. package/dist/server/middleware-matcher.d.ts +2 -0
  159. package/dist/server/middleware-matcher.js +23 -2
  160. package/dist/server/middleware-path-to-regexp.js +1 -1
  161. package/dist/server/middleware-runtime.d.ts +13 -0
  162. package/dist/server/middleware-runtime.js +38 -25
  163. package/dist/server/middleware.d.ts +1 -1
  164. package/dist/server/middleware.js +3 -1
  165. package/dist/server/multi-stage.d.ts +11 -0
  166. package/dist/server/navigation-planner.js +1 -1
  167. package/dist/server/normalize-path.d.ts +0 -13
  168. package/dist/server/normalize-path.js +1 -34
  169. package/dist/server/open-redirect.d.ts +11 -4
  170. package/dist/server/open-redirect.js +11 -4
  171. package/dist/server/pages-api-route.d.ts +3 -2
  172. package/dist/server/pages-api-route.js +6 -5
  173. package/dist/server/pages-cache-tag.d.ts +12 -0
  174. package/dist/server/pages-cache-tag.js +19 -0
  175. package/dist/server/pages-client-assets.d.ts +2 -0
  176. package/dist/server/pages-data-route.d.ts +10 -0
  177. package/dist/server/pages-data-route.js +21 -3
  178. package/dist/server/pages-document-asset-props.d.ts +5 -1
  179. package/dist/server/pages-document-asset-props.js +13 -5
  180. package/dist/server/pages-document-splice.d.ts +19 -0
  181. package/dist/server/pages-document-splice.js +55 -0
  182. package/dist/server/pages-i18n.d.ts +12 -1
  183. package/dist/server/pages-i18n.js +18 -4
  184. package/dist/server/pages-node-compat.d.ts +16 -5
  185. package/dist/server/pages-node-compat.js +20 -12
  186. package/dist/server/pages-page-data.d.ts +32 -24
  187. package/dist/server/pages-page-data.js +51 -121
  188. package/dist/server/pages-page-handler.d.ts +0 -6
  189. package/dist/server/pages-page-handler.js +189 -127
  190. package/dist/server/pages-page-response.d.ts +12 -6
  191. package/dist/server/pages-page-response.js +42 -35
  192. package/dist/server/pages-request-pipeline.d.ts +10 -2
  193. package/dist/server/pages-request-pipeline.js +41 -22
  194. package/dist/server/pages-request-stage-entry.d.ts +6 -0
  195. package/dist/server/pages-request-stage-entry.js +75 -16
  196. package/dist/server/pages-response-stage-entry.js +4 -0
  197. package/dist/server/pages-revalidate.js +3 -3
  198. package/dist/server/pages-router-entry.js +5 -4
  199. package/dist/server/prerender-manifest.d.ts +3 -3
  200. package/dist/server/prerender-manifest.js +4 -3
  201. package/dist/server/prerender-work-unit-setup.js +4 -0
  202. package/dist/server/prod-server.d.ts +12 -0
  203. package/dist/server/prod-server.js +204 -74
  204. package/dist/server/proper-error.d.ts +4 -0
  205. package/dist/server/proper-error.js +31 -0
  206. package/dist/server/request-pipeline.d.ts +4 -1
  207. package/dist/server/request-pipeline.js +3 -2
  208. package/dist/server/revalidation-request.d.ts +1 -0
  209. package/dist/server/revalidation-request.js +1 -1
  210. package/dist/server/rsc-stream-hints.d.ts +1 -1
  211. package/dist/server/rsc-stream-hints.js +52 -5
  212. package/dist/server/seed-cache.d.ts +1 -0
  213. package/dist/server/seed-cache.js +5 -4
  214. package/dist/server/server-reference-loader.d.ts +4 -0
  215. package/dist/server/server-reference-loader.js +29 -0
  216. package/dist/server/socket-error-backstop.d.ts +9 -1
  217. package/dist/server/socket-error-backstop.js +46 -17
  218. package/dist/server/static-file-signal.d.ts +15 -4
  219. package/dist/server/static-file-signal.js +46 -12
  220. package/dist/server/worker-image-optimization.d.ts +34 -0
  221. package/dist/server/worker-image-optimization.js +99 -0
  222. package/dist/server/worker-stages.d.ts +1 -1
  223. package/dist/server/worker-stages.js +2 -2
  224. package/dist/server/worker-utils.d.ts +8 -3
  225. package/dist/server/worker-utils.js +12 -7
  226. package/dist/shims/app-router-scroll.js +4 -1
  227. package/dist/shims/app-router-tree-context.d.ts +5 -0
  228. package/dist/shims/app-router-tree-context.js +27 -0
  229. package/dist/shims/cache-callable-runtime.d.ts +4 -2
  230. package/dist/shims/cache-callable-runtime.js +21 -12
  231. package/dist/shims/cache-handler.js +1 -9
  232. package/dist/shims/cache-runtime.d.ts +6 -1
  233. package/dist/shims/cache-runtime.js +2 -1
  234. package/dist/shims/cache.d.ts +2 -2
  235. package/dist/shims/cache.js +40 -13
  236. package/dist/shims/cdn-cache.d.ts +8 -0
  237. package/dist/shims/cdn-cache.js +17 -1
  238. package/dist/shims/disable-smooth-scroll.d.ts +3 -0
  239. package/dist/shims/disable-smooth-scroll.js +36 -0
  240. package/dist/shims/document-placeholders.d.ts +19 -0
  241. package/dist/shims/document-placeholders.js +45 -0
  242. package/dist/shims/document.js +21 -5
  243. package/dist/shims/dynamic-preload-chunks.d.ts +1 -0
  244. package/dist/shims/dynamic-preload-chunks.js +13 -9
  245. package/dist/shims/dynamic.d.ts +1 -1
  246. package/dist/shims/dynamic.js +60 -8
  247. package/dist/shims/fetch-cache-state.d.ts +66 -0
  248. package/dist/shims/fetch-cache-state.js +92 -0
  249. package/dist/shims/fetch-cache.d.ts +3 -67
  250. package/dist/shims/fetch-cache.js +160 -125
  251. package/dist/shims/head.d.ts +10 -6
  252. package/dist/shims/head.js +16 -14
  253. package/dist/shims/headers.js +2 -2
  254. package/dist/shims/image.js +2 -1
  255. package/dist/shims/internal/hybrid-client-route-owner-direct.js +1 -1
  256. package/dist/shims/internal/hybrid-client-route-owner.js +1 -0
  257. package/dist/shims/internal/work-store-scope.d.ts +5 -0
  258. package/dist/shims/internal/work-store-scope.js +24 -0
  259. package/dist/shims/link-prefetch.js +5 -4
  260. package/dist/shims/link.js +2 -2
  261. package/dist/shims/navigation.js +34 -9
  262. package/dist/shims/request-context.d.ts +2 -0
  263. package/dist/shims/request-state-types.d.ts +2 -1
  264. package/dist/shims/router.js +21 -32
  265. package/dist/shims/server.d.ts +27 -4
  266. package/dist/shims/server.js +65 -9
  267. package/dist/shims/unified-request-context.d.ts +1 -1
  268. package/dist/shims/url-utils.d.ts +9 -0
  269. package/dist/shims/url-utils.js +15 -4
  270. package/dist/utils/client-runtime-metadata.js +1 -1
  271. package/dist/utils/domain-locale.js +1 -1
  272. package/dist/utils/hash.d.ts +7 -0
  273. package/dist/utils/hash.js +11 -1
  274. package/dist/utils/package-name.d.ts +6 -0
  275. package/dist/utils/package-name.js +12 -1
  276. package/dist/utils/plugin-rsc-version.d.ts +6 -0
  277. package/dist/utils/plugin-rsc-version.js +41 -0
  278. package/dist/utils/prerender-output-paths.js +15 -4
  279. package/dist/utils/public-file-path.d.ts +10 -2
  280. package/dist/utils/public-file-path.js +13 -9
  281. package/dist/utils/public-routes.js +2 -2
  282. package/dist/utils/react-version.d.ts +10 -0
  283. package/dist/utils/react-version.js +39 -16
  284. package/package.json +11 -7
  285. package/dist/build/nitro-route-rules.d.ts +0 -47
  286. package/dist/build/nitro-route-rules.js +0 -79
  287. package/dist/plugins/cache-flight-codec.d.ts +0 -19
  288. package/dist/plugins/cache-flight-codec.js +0 -68
@@ -31,6 +31,15 @@ function matchesIfNoneMatch(ifNoneMatch, etag) {
31
31
  }
32
32
  return sawTag && matched;
33
33
  }
34
+ /**
35
+ * Whether a page answers its request with a 304, as Next.js's
36
+ * `sendEtagResponse` does with `fresh(req.headers, { etag })`. Next.js gives a
37
+ * page no Last-Modified, so a request with If-Modified-Since gets the page, and
38
+ * a request `Cache-Control: no-cache` always does.
39
+ */
40
+ function isPageNotModified(request, etag) {
41
+ return !request.ifModifiedSince && !/(?:^|,)\s*no-cache\s*(?:,|$)/.test(request.cacheControl ?? "") && matchesIfNoneMatch(request.ifNoneMatch, etag);
42
+ }
34
43
  /** Test an If-Match field using Next.js's weak/strong-equivalent comparison. */
35
44
  function matchesIfMatch(ifMatch, etag) {
36
45
  if (!ifMatch) return false;
@@ -114,4 +123,4 @@ function joinHeader(value) {
114
123
  return Array.isArray(value) ? value.join(", ") : value;
115
124
  }
116
125
  //#endregion
117
- export { evaluateStaticPreconditions, matchesIfMatch, matchesIfNoneMatch };
126
+ export { evaluateStaticPreconditions, isPageNotModified, matchesIfMatch, matchesIfNoneMatch };
@@ -9,7 +9,7 @@
9
9
  * `new Response(...)` boilerplate.
10
10
  *
11
11
  * Sites with route-specific bodies (e.g. `"404 - API route not found"`,
12
- * `"Image not found"`, generated worker templates) intentionally remain inline
12
+ * generated worker templates) intentionally remain inline
13
13
  * because their bodies are either tested-against fixtures or run inside template
14
14
  * strings that have no access to runtime imports.
15
15
  *
@@ -82,6 +82,44 @@ export type ParseImageParamsOptions = {
82
82
  isDev?: boolean;
83
83
  };
84
84
  export declare function resolveDevImageRedirect(requestUrl: URL, allowedWidths?: number[], allowedQualities?: number[], options?: ParseImageParamsOptions): string | null;
85
+ export type ImageOptimizationSignal = {
86
+ /** Middleware response headers to send with the image. */
87
+ middlewareHeaders: Headers;
88
+ /** The request headers after middleware's overrides, when it set any. */
89
+ requestHeaders: Headers | null;
90
+ /** The image query after middleware and rewrites. */
91
+ search: string;
92
+ };
93
+ /**
94
+ * Mark the App Router's image endpoint response once the request has passed
95
+ * middleware and rewrites. A host that serves images itself (the Node
96
+ * production server) replaces the response with the image; other hosts send
97
+ * the response unchanged. The symbol carries the signal across the built RSC
98
+ * module boundary, so response headers cannot forge it.
99
+ */
100
+ export declare function markImageOptimizationSignal(response: Response, signal: ImageOptimizationSignal): Response;
101
+ /** Return the signal of a framework-created image endpoint response. */
102
+ export declare function readImageOptimizationSignal(response: Response): ImageOptimizationSignal | null;
103
+ /**
104
+ * Carry a signal through the response-stage transport, which drops the
105
+ * symbol. The signal rides in the body of the bodiless carrier, with the
106
+ * carrier's status in the token-authenticated marker.
107
+ */
108
+ export declare function serializeImageOptimizationSignalForTransport(response: Response, token: string): Response;
109
+ /** Restore and consume a signal returned by the trusted response-stage wrapper. */
110
+ export declare function restoreImageOptimizationSignalFromTransport(response: Response, token: string): Promise<Response>;
111
+ /**
112
+ * Whether a local source's pipeline status carries the file's bytes. A no-body
113
+ * success such as 204 serves no file, which Next.js rejects as an invalid
114
+ * image.
115
+ */
116
+ export declare function isImageSourceBodyStatus(status: number): boolean;
117
+ /**
118
+ * Body Next.js answers, with a 400, for a local source that yields no image:
119
+ * a missing file, or anything middleware or routing answers in its place.
120
+ */
121
+ export declare const INVALID_IMAGE_SOURCE_MESSAGE = "The requested resource isn't a valid image.";
122
+ export declare function invalidImageSourceResponse(): Response;
85
123
  /**
86
124
  * Parse and validate image optimization query parameters.
87
125
  * Returns null if the request is malformed.
@@ -78,6 +78,93 @@ function resolveDevImageRedirect(requestUrl, allowedWidths = [...DEFAULT_DEVICE_
78
78
  if (resolved.origin !== requestUrl.origin) return null;
79
79
  return resolved.pathname + resolved.search;
80
80
  }
81
+ const IMAGE_OPTIMIZATION_SIGNAL = Symbol.for("vinext.image-optimization-signal");
82
+ /**
83
+ * Mark the App Router's image endpoint response once the request has passed
84
+ * middleware and rewrites. A host that serves images itself (the Node
85
+ * production server) replaces the response with the image; other hosts send
86
+ * the response unchanged. The symbol carries the signal across the built RSC
87
+ * module boundary, so response headers cannot forge it.
88
+ */
89
+ function markImageOptimizationSignal(response, signal) {
90
+ Object.defineProperty(response, IMAGE_OPTIMIZATION_SIGNAL, { value: signal });
91
+ return response;
92
+ }
93
+ /** Return the signal of a framework-created image endpoint response. */
94
+ function readImageOptimizationSignal(response) {
95
+ const signal = Reflect.get(response, IMAGE_OPTIMIZATION_SIGNAL);
96
+ if (typeof signal !== "object" || signal === null) return null;
97
+ const { middlewareHeaders, requestHeaders, search } = signal;
98
+ return middlewareHeaders instanceof Headers && (requestHeaders === null || requestHeaders instanceof Headers) && typeof search === "string" ? {
99
+ middlewareHeaders,
100
+ requestHeaders,
101
+ search
102
+ } : null;
103
+ }
104
+ const IMAGE_OPTIMIZATION_SIGNAL_TRANSPORT_HEADER = "x-vinext-stage-image-optimization";
105
+ function isHeaderEntries(value) {
106
+ return Array.isArray(value) && value.every((entry) => Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string" && typeof entry[1] === "string");
107
+ }
108
+ /**
109
+ * Carry a signal through the response-stage transport, which drops the
110
+ * symbol. The signal rides in the body of the bodiless carrier, with the
111
+ * carrier's status in the token-authenticated marker.
112
+ */
113
+ function serializeImageOptimizationSignalForTransport(response, token) {
114
+ const signal = readImageOptimizationSignal(response);
115
+ if (signal === null) return response;
116
+ const headers = new Headers(response.headers);
117
+ headers.set(IMAGE_OPTIMIZATION_SIGNAL_TRANSPORT_HEADER, `${token}:${response.status}`);
118
+ return new Response(JSON.stringify({
119
+ middlewareHeaders: [...signal.middlewareHeaders],
120
+ requestHeaders: signal.requestHeaders ? [...signal.requestHeaders] : null,
121
+ search: signal.search
122
+ }), { headers });
123
+ }
124
+ /** Restore and consume a signal returned by the trusted response-stage wrapper. */
125
+ async function restoreImageOptimizationSignalFromTransport(response, token) {
126
+ const marker = response.headers.get(IMAGE_OPTIMIZATION_SIGNAL_TRANSPORT_HEADER);
127
+ if (marker === null || response.status < 200 || response.status > 599) return response;
128
+ const headers = new Headers(response.headers);
129
+ headers.delete(IMAGE_OPTIMIZATION_SIGNAL_TRANSPORT_HEADER);
130
+ const prefix = `${token}:`;
131
+ const status = marker.startsWith(prefix) ? Number(marker.slice(prefix.length)) : NaN;
132
+ if (!Number.isInteger(status) || status < 200 || status > 599) {
133
+ const body = response.status === 204 || response.status === 205 || response.status === 304 ? null : response.body;
134
+ return new Response(body, {
135
+ headers,
136
+ status: response.status,
137
+ statusText: response.statusText
138
+ });
139
+ }
140
+ const { middlewareHeaders, requestHeaders, search } = JSON.parse(await response.text()) ?? {};
141
+ if (!isHeaderEntries(middlewareHeaders) || requestHeaders !== null && !isHeaderEntries(requestHeaders) || typeof search !== "string") throw new Error("Invalid image optimization signal from the response stage");
142
+ return markImageOptimizationSignal(new Response(null, {
143
+ headers,
144
+ status
145
+ }), {
146
+ middlewareHeaders: new Headers(middlewareHeaders),
147
+ requestHeaders: requestHeaders ? new Headers(requestHeaders) : null,
148
+ search
149
+ });
150
+ }
151
+ /**
152
+ * Whether a local source's pipeline status carries the file's bytes. A no-body
153
+ * success such as 204 serves no file, which Next.js rejects as an invalid
154
+ * image.
155
+ */
156
+ function isImageSourceBodyStatus(status) {
157
+ return status >= 200 && status < 300 && status !== 204 && status !== 205;
158
+ }
159
+ /**
160
+ * Body Next.js answers, with a 400, for a local source that yields no image:
161
+ * a missing file, or anything middleware or routing answers in its place.
162
+ */
163
+ const INVALID_IMAGE_SOURCE_MESSAGE = "The requested resource isn't a valid image.";
164
+ function invalidImageSourceResponse() {
165
+ return new Response(INVALID_IMAGE_SOURCE_MESSAGE, { status: 400 });
166
+ }
167
+ const RECURSIVE_IMAGE_URL_RE = /\/_(?:next|vinext)\/image(?:$|\/)/;
81
168
  /**
82
169
  * Parse and validate image optimization query parameters.
83
170
  * Returns null if the request is malformed.
@@ -113,7 +200,10 @@ function parseImageParams(url, allowedWidths = [...DEFAULT_DEVICE_SIZES, ...DEFA
113
200
  if (!normalizedUrl.startsWith("/") || normalizedUrl.startsWith("//")) return null;
114
201
  try {
115
202
  const base = "https://localhost";
116
- if (new URL(normalizedUrl, base).origin !== base) return null;
203
+ const resolved = new URL(normalizedUrl, base);
204
+ if (resolved.origin !== base) return null;
205
+ if (resolved.pathname.startsWith("//")) return null;
206
+ if (RECURSIVE_IMAGE_URL_RE.test(decodeURIComponent(resolved.pathname))) return null;
117
207
  } catch {
118
208
  return null;
119
209
  }
@@ -226,7 +316,7 @@ async function handleImageOptimization(request, handlers, allowedWidths, imageCo
226
316
  if (!params) return badRequestResponse();
227
317
  const { imageUrl, width, quality } = params;
228
318
  const source = await handlers.fetchAsset(imageUrl, request);
229
- if (!source.ok || !source.body) return new Response("Image not found", { status: 404 });
319
+ if (!source.ok || !source.body) return invalidImageSourceResponse();
230
320
  const format = negotiateImageFormat(request.headers.get("Accept"));
231
321
  const sourceContentType = source.headers.get("Content-Type");
232
322
  if (!isSafeImageContentType(sourceContentType, imageConfig?.dangerouslyAllowSVG)) return new Response("The requested resource is not an allowed image type", { status: 400 });
@@ -252,7 +342,7 @@ async function handleImageOptimization(request, handlers, allowedWidths, imageCo
252
342
  } catch (e) {
253
343
  console.error("[vinext] Image fallback error, refetching source image:", e);
254
344
  const refetchedSource = await handlers.fetchAsset(imageUrl, request);
255
- if (!refetchedSource.ok || !refetchedSource.body) return new Response("Image not found", { status: 404 });
345
+ if (!refetchedSource.ok || !refetchedSource.body) return invalidImageSourceResponse();
256
346
  if (!isSafeImageContentType(refetchedSource.headers.get("Content-Type"), imageConfig?.dangerouslyAllowSVG)) return new Response("The requested resource is not an allowed image type", { status: 400 });
257
347
  return createPassthroughImageResponse(refetchedSource, imageConfig);
258
348
  }
@@ -307,4 +397,4 @@ function handleConfiguredImageOptimization(request, fetchAsset, allowedWidths, i
307
397
  }, allowedWidths, imageConfig);
308
398
  }
309
399
  //#endregion
310
- export { DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES, IMAGE_CACHE_CONTROL, IMAGE_CONTENT_SECURITY_POLICY, IMAGE_OPTIMIZATION_PATH, IMAGE_RESPONSE_CACHE_STATE, VINEXT_IMAGE_OPTIMIZATION_PATH, getImageOptimizer, handleConfiguredImageOptimization, handleImageOptimization, isImageOptimizationPath, isSafeImageContentType, negotiateImageFormat, parseImageParams, resolveDevImageRedirect, setImageOptimizer };
400
+ export { DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES, IMAGE_CACHE_CONTROL, IMAGE_CONTENT_SECURITY_POLICY, IMAGE_OPTIMIZATION_PATH, IMAGE_RESPONSE_CACHE_STATE, INVALID_IMAGE_SOURCE_MESSAGE, VINEXT_IMAGE_OPTIMIZATION_PATH, getImageOptimizer, handleConfiguredImageOptimization, handleImageOptimization, invalidImageSourceResponse, isImageOptimizationPath, isImageSourceBodyStatus, isSafeImageContentType, markImageOptimizationSignal, negotiateImageFormat, parseImageParams, readImageOptimizationSignal, resolveDevImageRedirect, restoreImageOptimizationSignalFromTransport, serializeImageOptimizationSignalForTransport, setImageOptimizer };
@@ -112,7 +112,17 @@ export declare function pagesIsrCacheKey(pathname: string, buildId?: string, i18
112
112
  * Cloudflare KV key under its 512-byte limit for long pathnames.
113
113
  */
114
114
  export declare function appIsrCacheKey(pathname: string, suffix: string, buildId?: string | undefined): string;
115
+ /**
116
+ * Compute the ISR key for a PPR fallback shell artifact, such as the HTML for
117
+ * `/en/[slug]`. Shells get their own namespace: a request for the literal path
118
+ * `/en/%5Bslug%5D` decodes to the same pathname, and its render must not be
119
+ * stored where every unknown `[slug]` reads its shell. The namespace is a
120
+ * prefix of its own rather than a part after the build ID, which can contain
121
+ * `:` and so could spell it.
122
+ */
123
+ export declare function appIsrFallbackShellKey(pathname: string, suffix: "html" | "rsc", buildId?: string | undefined): string;
115
124
  export declare function appIsrHtmlKey(pathname: string): string;
125
+ export declare function appIsrFallbackShellHtmlKey(pathname: string): string;
116
126
  /**
117
127
  * Build the ISR cache key for an RSC payload.
118
128
  *
@@ -4,12 +4,26 @@ import { getRequestExecutionContext } from "../shims/request-context.js";
4
4
  import { isInterceptionMatchedUrlPath } from "./normalize-path.js";
5
5
  import { reportRequestError } from "./instrumentation.js";
6
6
  import { getCdnCacheAdapter } from "../shims/cdn-cache.js";
7
- import { fnv1a64 } from "../utils/hash.js";
8
7
  import { normalizeMountedSlotsHeader } from "./app-mounted-slots-header.js";
9
8
  import { APP_RSC_RENDER_MODE_NAVIGATION, getRscRenderModeCacheVariant } from "./app-rsc-render-mode.js";
10
9
  import { getRevalidateSecret, isOnDemandRevalidateRequest, isRevalidateSecret } from "./revalidation-request.js";
10
+ import { createHash } from "node:crypto";
11
11
  //#region src/server/isr-cache.ts
12
12
  /**
13
+ * ISR (Incremental Static Regeneration) cache layer.
14
+ *
15
+ * Wraps the pluggable CacheHandler with stale-while-revalidate semantics:
16
+ * - Fresh hit: serve immediately
17
+ * - Stale hit: serve immediately + trigger background regeneration
18
+ * - Miss: render synchronously, cache, serve
19
+ *
20
+ * Background regeneration is deduped — only one regeneration per cache key
21
+ * runs at a time, preventing thundering herd on popular pages.
22
+ *
23
+ * This layer works with any CacheHandler backend (memory, Redis, KV, etc.)
24
+ * because it only uses the standard get/set interface.
25
+ */
26
+ /**
13
27
  * Get a cache entry with staleness information.
14
28
  *
15
29
  * Returns { value, isStale: false } for fresh entries,
@@ -151,12 +165,19 @@ function buildAppPageCacheValue(html, rscData, status, renderObservation, header
151
165
  function normalizeCachePathname(pathname) {
152
166
  return pathname === "/" ? "/" : pathname.replace(/\/$/, "");
153
167
  }
168
+ /**
169
+ * Digest a request-controlled key part. It must be collision resistant: a
170
+ * crafted value that shared a weaker hash with another would share its entry.
171
+ */
172
+ function digestKeyPart(value) {
173
+ return createHash("sha256").update(value).digest("hex");
174
+ }
154
175
  function buildCacheKey(prefix, pathname, suffix) {
155
176
  const normalized = normalizeCachePathname(pathname);
156
177
  const suffixPart = suffix ? `:${suffix}` : "";
157
178
  const key = `${prefix}:${normalized}${suffixPart}`;
158
179
  if (key.length <= 200) return key;
159
- return `${prefix}:__hash:${fnv1a64(normalized)}${suffixPart}`;
180
+ return `${prefix}:__hash:${digestKeyPart(normalized)}${suffixPart}`;
160
181
  }
161
182
  /**
162
183
  * Compute an ISR cache key for a given router type and pathname.
@@ -179,9 +200,23 @@ function pagesIsrCacheKey(pathname, buildId, i18nCacheVariant) {
179
200
  function appIsrCacheKey(pathname, suffix, buildId = process.env.__VINEXT_BUILD_ID) {
180
201
  return buildCacheKey(buildId ? `app:v2:${buildId}` : "app:v2", pathname, suffix);
181
202
  }
203
+ /**
204
+ * Compute the ISR key for a PPR fallback shell artifact, such as the HTML for
205
+ * `/en/[slug]`. Shells get their own namespace: a request for the literal path
206
+ * `/en/%5Bslug%5D` decodes to the same pathname, and its render must not be
207
+ * stored where every unknown `[slug]` reads its shell. The namespace is a
208
+ * prefix of its own rather than a part after the build ID, which can contain
209
+ * `:` and so could spell it.
210
+ */
211
+ function appIsrFallbackShellKey(pathname, suffix, buildId = process.env.__VINEXT_BUILD_ID) {
212
+ return buildCacheKey(buildId ? `app-fallback-shell:v1:${buildId}` : "app-fallback-shell:v1", pathname, suffix);
213
+ }
182
214
  function appIsrHtmlKey(pathname) {
183
215
  return appIsrCacheKey(pathname, "html");
184
216
  }
217
+ function appIsrFallbackShellHtmlKey(pathname) {
218
+ return appIsrFallbackShellKey(pathname, "html");
219
+ }
185
220
  function normalizeInterceptionContextForCacheKey(interceptionContext) {
186
221
  return isInterceptionMatchedUrlPath(interceptionContext) ? interceptionContext : null;
187
222
  }
@@ -200,9 +235,9 @@ function appIsrRscKey(pathname, mountedSlotsHeader, renderMode = APP_RSC_RENDER_
200
235
  const normalizedMountedSlotsHeader = normalizeMountedSlotsHeader(mountedSlotsHeader);
201
236
  const sourceVariant = interceptionContext === void 0 || interceptionContext === null ? null : normalizeInterceptionContextForCacheKey(interceptionContext);
202
237
  const variant = [
203
- sourceVariant ? `source:${fnv1a64(sourceVariant)}` : null,
204
- interceptionId ? `selector:${fnv1a64(interceptionId)}` : null,
205
- normalizedMountedSlotsHeader ? `slots:${fnv1a64(normalizedMountedSlotsHeader)}` : null,
238
+ sourceVariant ? `source:${digestKeyPart(sourceVariant)}` : null,
239
+ interceptionId ? `selector:${digestKeyPart(interceptionId)}` : null,
240
+ normalizedMountedSlotsHeader ? `slots:${digestKeyPart(normalizedMountedSlotsHeader)}` : null,
206
241
  getRscRenderModeCacheVariant(renderMode)
207
242
  ].filter((part) => part !== null).join(":");
208
243
  return appIsrCacheKey(pathname, variant ? `rsc:${variant}` : "rsc");
@@ -211,4 +246,4 @@ function appIsrRouteKey(pathname) {
211
246
  return appIsrCacheKey(pathname, pathname !== "/" && pathname.endsWith("/") ? "route:trailing-slash" : "route");
212
247
  }
213
248
  //#endregion
214
- export { PRERENDER_REVALIDATE_HEADER, PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER, appIsrCacheKey, appIsrHtmlKey, appIsrRouteKey, appIsrRscKey, buildAppPageCacheValue, buildPagesCacheValue, coalesceOnDemandRevalidation, getRevalidateSecret, isOnDemandRevalidateRequest, isRevalidateSecret, isrCacheControl, isrCacheKey, isrGet, isrSet, isrSetPrerenderedAppPage, normalizeMountedSlotsHeader, pagesIsrCacheKey, resolveRouteExpireSeconds, triggerBackgroundRegeneration };
249
+ export { PRERENDER_REVALIDATE_HEADER, PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER, appIsrCacheKey, appIsrFallbackShellHtmlKey, appIsrFallbackShellKey, appIsrHtmlKey, appIsrRouteKey, appIsrRscKey, buildAppPageCacheValue, buildPagesCacheValue, coalesceOnDemandRevalidation, getRevalidateSecret, isOnDemandRevalidateRequest, isRevalidateSecret, isrCacheControl, isrCacheKey, isrGet, isrSet, isrSetPrerenderedAppPage, normalizeMountedSlotsHeader, pagesIsrCacheKey, resolveRouteExpireSeconds, triggerBackgroundRegeneration };
@@ -1,9 +1,10 @@
1
1
  import { markFullyBufferedBody } from "./fully-buffered-response.js";
2
2
  import { closeAfterResponse, createRequestContext, runWithRequestContext } from "../shims/unified-request-context.js";
3
- import { decodeMatchedParams } from "../routing/utils.js";
4
- import { getRequestExecutionContext } from "../shims/request-context.js";
5
3
  import { VINEXT_METADATA_ROUTE_CACHE_HEADER } from "./headers.js";
6
4
  import { notFoundResponse } from "./http-error-responses.js";
5
+ import { decodeMatchedParams } from "../routing/utils.js";
6
+ import { getRequestExecutionContext } from "../shims/request-context.js";
7
+ import { runInWorkStoreScope } from "../shims/internal/work-store-scope.js";
7
8
  import { beginRouteCacheability, markRouteCacheabilityExplicitResponsePolicy, markRouteCacheabilityResponseBodyComplete, recordRouteCacheability } from "../shims/cacheability-classification.js";
8
9
  import { NEVER_CACHE_CONTROL, applyCdnResponseHeaders, hasCdnResponsePolicy } from "./cache-control.js";
9
10
  import { buildAppRouteMissIsrCacheControl } from "./isr-decision.js";
@@ -215,7 +216,7 @@ async function writeRenderedMetadataRoute(options, route, routeKey, rendered, pr
215
216
  async function runMetadataRouteRegeneration(options, route, render, routeKey, previousEntry, executionContext) {
216
217
  const requestContext = createRequestContext({ executionContext });
217
218
  try {
218
- await runWithRequestContext(requestContext, async () => {
219
+ await runWithRequestContext(requestContext, () => runInWorkStoreScope(async () => {
219
220
  ensureFetchPatch();
220
221
  beginMetadataRouteCacheability(route);
221
222
  setCurrentFetchSoftTags(buildMetadataRouteTags(route, options.cleanPathname, []));
@@ -225,7 +226,7 @@ async function runMetadataRouteRegeneration(options, route, render, routeKey, pr
225
226
  } finally {
226
227
  rendered.response.body?.cancel().catch(() => {});
227
228
  }
228
- });
229
+ }));
229
230
  } finally {
230
231
  await closeAfterResponse(requestContext);
231
232
  }
@@ -512,6 +513,12 @@ async function handleMetadataRouteRequest(options) {
512
513
  decodeMatchedParams(runtimeParams);
513
514
  match.params = runtimeParams;
514
515
  }
516
+ const render = async () => {
517
+ setCurrentFetchSoftTags(buildMetadataRouteTags(route, options.cleanPathname, []));
518
+ return captureRenderedMetadataRoute(route.isDynamic ? await callDynamicMetadataRoute(route, match, options.makeThenableParams, functions) : serveStaticMetadataRoute(route), route, options.cleanPathname);
519
+ };
520
+ const cached = await readMatchedPrerenderedMetadataRouteResponse(options, route, render);
521
+ if (cached.response) return cached.response;
515
522
  if (route.isDynamic && isImageMetadataRoute(route) && functions.dynamicParams === false) {
516
523
  const validationParams = { ...rawParams };
517
524
  canonicalizeAppPageParams(validationParams);
@@ -526,12 +533,6 @@ async function handleMetadataRouteRequest(options) {
526
533
  });
527
534
  if (dynamicParamsResponse) return dynamicParamsResponse;
528
535
  }
529
- const render = async () => {
530
- setCurrentFetchSoftTags(buildMetadataRouteTags(route, options.cleanPathname, []));
531
- return captureRenderedMetadataRoute(route.isDynamic ? await callDynamicMetadataRoute(route, match, options.makeThenableParams, functions) : serveStaticMetadataRoute(route), route, options.cleanPathname);
532
- };
533
- const cached = await readMatchedPrerenderedMetadataRouteResponse(options, route, render);
534
- if (cached.response) return cached.response;
535
536
  const rendered = await render();
536
537
  await writeMetadataRouteMiss(options, route, rendered, cached.cachedEntry);
537
538
  return rendered.response;
@@ -7,6 +7,8 @@ export type MiddlewareLocaleMatchContext = {
7
7
  defaultLocale: string;
8
8
  } | {
9
9
  kind: "internal" | "literal";
10
+ } | {
11
+ kind: "next-data";
10
12
  };
11
13
  export declare function matchesMiddleware(pathname: string, matcher: MatcherConfig | undefined, request?: Request, i18nConfig?: NextI18nConfig | null, localeContext?: MiddlewareLocaleMatchContext): boolean;
12
14
  /**
@@ -49,6 +49,7 @@ function matchesMiddlewarePathname(pathname, matcher, i18nConfig, localeContext)
49
49
  return false;
50
50
  }
51
51
  function matchMatcherPattern(pathname, pattern, i18nConfig, localeContext) {
52
+ if (localeContext?.kind === "next-data") return matchNextDataPattern(pathname, pattern, i18nConfig, true);
52
53
  if (!i18nConfig) return matchPattern(pathname, pattern);
53
54
  if (localeContext) {
54
55
  if (localeContext.kind === "internal") return false;
@@ -57,9 +58,25 @@ function matchMatcherPattern(pathname, pattern, i18nConfig, localeContext) {
57
58
  return matchPattern(stripLocalePrefix(pathname, i18nConfig) ?? pathname, pattern);
58
59
  }
59
60
  function matchObjectMatcher(pathname, matcher, i18nConfig, localeContext) {
61
+ if (localeContext?.kind === "next-data") return matchNextDataPattern(pathname, matcher.source, i18nConfig, matcher.locale !== false);
60
62
  if (matcher.locale !== false) return matchMatcherPattern(pathname, matcher.source, i18nConfig, localeContext);
61
63
  return matchPattern(localeContext?.kind === "defaulted" ? `/${localeContext.defaultLocale}${pathname === "/" ? "" : pathname}` : pathname, matcher.source);
62
64
  }
65
+ function matchNextDataPattern(pathname, source, i18nConfig, localized) {
66
+ if (compiledPattern(source) === UNSAFE_MATCHER_PATTERN) return true;
67
+ return matchPattern(pathname, nextDataMatcherSource(source, i18nConfig, localized));
68
+ }
69
+ /**
70
+ * The source Next.js compiles for a matcher: an optional `_next/data/<buildId>`
71
+ * prefix, an internal locale segment under i18n, and an optional data suffix.
72
+ * Next.js also accepts App Router transport suffixes, which end in `.rsc` and
73
+ * so can never match a `.json` data URL.
74
+ * https://github.com/vercel/next.js/blob/v16.2.7/packages/next/src/build/analysis/get-page-static-info.ts
75
+ */
76
+ function nextDataMatcherSource(source, i18nConfig, localized) {
77
+ const isRoot = source === "/";
78
+ return `/:nextData(_next/data/[^/]{1,})?${i18nConfig && localized ? `/:nextInternalLocale((?!_next/)[^/.]{1,})${isRoot ? "" : source}` : source}${isRoot ? `(${i18nConfig ? "|\\.json|" : ""}/?index|/?index\\.json)?` : "{(\\.json)}?"}`;
79
+ }
63
80
  function stripLocalePrefix(pathname, i18nConfig) {
64
81
  if (pathname === "/") return null;
65
82
  const lowerFirstSegment = pathname.split("/")[1]?.toLowerCase();
@@ -71,13 +88,17 @@ function stripFirstPathSegment(pathname) {
71
88
  }
72
89
  function matchPattern(pathname, pattern) {
73
90
  if (pattern === "/" && (pathname === "//" || pathname === "/?" || pathname === "/#")) return false;
91
+ const compiled = compiledPattern(pattern);
92
+ if (compiled === UNSAFE_MATCHER_PATTERN) return true;
93
+ return compiled.test(pathname);
94
+ }
95
+ function compiledPattern(pattern) {
74
96
  let cached = _mwPatternCache.get(pattern);
75
97
  if (cached === void 0) {
76
98
  cached = compileMatcherPattern(pattern);
77
99
  _mwPatternCache.set(pattern, cached);
78
100
  }
79
- if (cached === UNSAFE_MATCHER_PATTERN) return true;
80
- return cached.test(pathname);
101
+ return cached;
81
102
  }
82
103
  function compileMatcherPattern(pattern) {
83
104
  const result = compileMiddlewareMatcherPattern(pattern);
@@ -224,7 +224,7 @@ function middlewarePathTokensToRegExp(tokens) {
224
224
  } else route += `(?:${prefix}${suffix})${token.modifier}`;
225
225
  }
226
226
  route += `${delimiterRegex}?$`;
227
- return new RegExp(route, "i");
227
+ return new RegExp(route);
228
228
  }
229
229
  //#endregion
230
230
  export { middlewarePathTokensToRegExp, normalizeMiddlewarePathTokens, parseMiddlewarePath };
@@ -10,6 +10,8 @@ export type MiddlewareResult = {
10
10
  redirectUrl?: string;
11
11
  redirectStatus?: number;
12
12
  rewriteUrl?: string;
13
+ /** False when a same-origin rewrite target was outside the basePath. */
14
+ rewriteHadBasePath?: boolean;
13
15
  rewriteStatus?: number;
14
16
  status?: number;
15
17
  responseHeaders?: Headers;
@@ -41,6 +43,11 @@ type ExecuteMiddlewareOptions = {
41
43
  * must derive and forward this from trusted URL normalization.
42
44
  */
43
45
  isDataRequest?: boolean;
46
+ /**
47
+ * The data request names a buildId other than the current build. Adapters
48
+ * pass it raw to middleware only under `skipProxyUrlNormalize`.
49
+ */
50
+ isStaleDataRequest?: boolean;
44
51
  isProxy: boolean;
45
52
  module: MiddlewareModule;
46
53
  normalizedPathname?: string;
@@ -63,6 +70,12 @@ type ExecuteMiddlewareOptions = {
63
70
  * `new URL('/x', req.url)`.
64
71
  */
65
72
  trailingSlash?: boolean;
73
+ /**
74
+ * next.config `skipTrailingSlashRedirect`. Middleware then sees requests in
75
+ * either slash form, so NextURL keeps each request's own trailing slash and
76
+ * redirect Locations keep theirs, as Next.js's `getNextPathnameInfo` does.
77
+ */
78
+ skipTrailingSlashRedirect?: boolean;
66
79
  };
67
80
  type RunGeneratedMiddlewareOptions = ExecuteMiddlewareOptions & {
68
81
  ctx?: ExecutionContextLike;
@@ -2,18 +2,20 @@ import path from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
2
2
  import "../utils/protocol-headers.js";
3
3
  import { shouldKeepMiddlewareHeader } from "../utils/middleware-request-headers.js";
4
4
  import { normalizeHost } from "../config/request-context.js";
5
- import { normalizePathnameForRouteMatchStrict } from "../routing/utils.js";
6
5
  import { addBasePathToPathname, hasBasePath, removeTrailingSlash, stripBasePath } from "../utils/base-path.js";
6
+ import { MIDDLEWARE_REWRITE_HEADER } from "./headers.js";
7
+ import { badRequestResponse, internalServerErrorResponse } from "./http-error-responses.js";
8
+ import { isOpenRedirectShaped } from "./open-redirect.js";
9
+ import { processMiddlewareHeaders } from "./request-pipeline.js";
10
+ import { normalizePathnameForRouteMatchStrict } from "../routing/utils.js";
7
11
  import { getRequestExecutionContext, runWithExecutionContext } from "../shims/request-context.js";
8
12
  import "./server-globals.js";
9
- import { MIDDLEWARE_REWRITE_HEADER } from "./headers.js";
10
13
  import { NextFetchEvent, NextRequest } from "../shims/server.js";
14
+ import { runInWorkStoreScope } from "../shims/internal/work-store-scope.js";
11
15
  import { normalizePath } from "./normalize-path.js";
12
16
  import { matchesMiddleware, matchesMiddlewarePathname } from "./middleware-matcher.js";
13
- import { badRequestResponse, internalServerErrorResponse } from "./http-error-responses.js";
14
- import { isOpenRedirectShaped } from "./open-redirect.js";
15
- import { processMiddlewareHeaders } from "./request-pipeline.js";
16
17
  import { normalizeDefaultLocalePathname } from "./pages-i18n.js";
18
+ import { nextDataMiddlewareMatchPathname } from "./pages-data-route.js";
17
19
  import { reportRequestError } from "./instrumentation.js";
18
20
  import { isValidNavigationSignalError } from "../utils/navigation-signal.js";
19
21
  import { frameworkTracer } from "./tracer.js";
@@ -133,7 +135,7 @@ function resolveMiddlewarePathname(request) {
133
135
  return badRequestResponse();
134
136
  }
135
137
  }
136
- function createNextRequest(request, i18nConfig, basePath, trailingSlash, hadBasePath, requestBodyAlreadyIsolated = false) {
138
+ function createNextRequest(request, i18nConfig, basePath, trailingSlash, hadBasePath, requestBodyAlreadyIsolated = false, skipTrailingSlashRedirect = false) {
137
139
  const url = new URL(request.url);
138
140
  let mwRequest = !requestBodyAlreadyIsolated && request.body && !request.bodyUsed ? request.clone() : request;
139
141
  const requestPathname = url.pathname;
@@ -143,10 +145,11 @@ function createNextRequest(request, i18nConfig, basePath, trailingSlash, hadBase
143
145
  mwUrl.pathname = mwPathname;
144
146
  mwRequest = new Request(mwUrl, mwRequest);
145
147
  }
146
- const nextConfig = basePath || i18nConfig || trailingSlash ? {
148
+ const nextConfig = basePath || i18nConfig || trailingSlash || skipTrailingSlashRedirect ? {
147
149
  basePath: basePath ?? "",
148
150
  i18n: i18nConfig ?? void 0,
149
- trailingSlash: trailingSlash ?? void 0
151
+ trailingSlash: trailingSlash ?? void 0,
152
+ skipTrailingSlashRedirect
150
153
  } : void 0;
151
154
  const nextRequest = mwRequest instanceof NextRequest && (!mwRequest.body || mwRequest.bodyUsed) ? mwRequest : new NextRequest(mwRequest, nextConfig ? { nextConfig } : void 0);
152
155
  if (mwRequest !== request && mwRequest.body && !mwRequest.bodyUsed && !mwRequest.body.locked) mwRequest.body.cancel().catch(() => {});
@@ -164,7 +167,10 @@ function releaseMiddlewareRequestBody(request, waitUntilPromises, retainedByResp
164
167
  }
165
168
  Promise.allSettled(waitUntilPromises).then(cancel);
166
169
  }
167
- async function executeMiddleware(options) {
170
+ function executeMiddleware(options) {
171
+ return runInWorkStoreScope(() => executeMiddlewareModule(options));
172
+ }
173
+ async function executeMiddlewareModule(options) {
168
174
  const middlewareFn = resolveMiddlewareModuleHandler(options.module, {
169
175
  filePath: options.filePath,
170
176
  isProxy: options.isProxy
@@ -187,14 +193,17 @@ async function executeMiddleware(options) {
187
193
  if (options.hadBasePath === true) return candidate;
188
194
  return matcher === void 0 ? candidate : null;
189
195
  };
190
- const encodedMatchPathname = prepareMatcherPathname(encodedRequestPathname);
196
+ const isStaleDataRequest = options.isDataRequest === true && options.isStaleDataRequest === true;
197
+ const toPageMatcherPathname = (candidate) => candidate !== null && options.isDataRequest && !isStaleDataRequest ? nextDataMiddlewareMatchPathname(candidate) ?? candidate : candidate;
198
+ const encodedMatchPathname = toPageMatcherPathname(prepareMatcherPathname(encodedRequestPathname));
191
199
  let decodedMatchPathname = encodedMatchPathname;
192
200
  try {
193
201
  if (encodedMatchPathname !== null) decodedMatchPathname = decodeURIComponent(encodedMatchPathname);
194
- else if (!options.i18nConfig) decodedMatchPathname = prepareMatcherPathname(decodeURIComponent(encodedRequestPathname));
202
+ else if (!options.i18nConfig) decodedMatchPathname = toPageMatcherPathname(prepareMatcherPathname(decodeURIComponent(encodedRequestPathname)));
195
203
  } catch {}
196
204
  let localeContext;
197
- if (options.i18nConfig && encodedMatchPathname !== null) {
205
+ if (isStaleDataRequest) localeContext = { kind: "next-data" };
206
+ else if (options.i18nConfig && encodedMatchPathname !== null) {
198
207
  const hostname = normalizeHost(options.request.headers.get("host"), requestUrl.hostname);
199
208
  const firstSegment = encodedMatchPathname.split("/", 3)[1];
200
209
  if (firstSegment !== void 0 && options.i18nConfig.locales.some((locale) => locale.toLowerCase() === firstSegment.toLowerCase())) localeContext = { kind: "literal" };
@@ -210,7 +219,7 @@ async function executeMiddleware(options) {
210
219
  const decodedMatches = !encodedMatches && decodedMatchPathname !== null && decodedMatchPathname !== encodedMatchPathname && matchesMiddleware(decodedMatchPathname, matcher, options.request, options.i18nConfig, localeContext);
211
220
  if (!encodedMatches && !decodedMatches) return { continue: true };
212
221
  options.onMatch?.();
213
- const nextRequest = createNextRequest(options.request, options.i18nConfig, options.basePath, options.trailingSlash, hadBasePath, options.requestBodyAlreadyIsolated);
222
+ const nextRequest = createNextRequest(options.request, options.i18nConfig, options.basePath, options.trailingSlash, hadBasePath, options.requestBodyAlreadyIsolated, options.skipTrailingSlashRedirect);
214
223
  if (options.isDataRequest) Object.defineProperty(nextRequest, "__isData", {
215
224
  enumerable: false,
216
225
  value: true
@@ -248,21 +257,12 @@ async function executeMiddleware(options) {
248
257
  waitUntilPromises
249
258
  };
250
259
  }
251
- if (response.headers.get("x-middleware-next") === "1") {
252
- releaseMiddlewareRequestBody(nextRequest, waitUntilPromises);
253
- return {
254
- continue: true,
255
- responseHeaders: collectMiddlewareHeaders(response),
256
- status: response.status !== 200 ? response.status : void 0,
257
- waitUntilPromises
258
- };
259
- }
260
260
  if (response.status >= 300 && response.status < 400) {
261
261
  const location = response.headers.get("Location") ?? response.headers.get("location");
262
262
  if (location) {
263
263
  const relativeLocation = relativizeLocation(location, options.request.url);
264
264
  let normalizedLocation = relativeLocation;
265
- if (options.trailingSlash !== void 0) try {
265
+ if (options.trailingSlash !== void 0 && !options.skipTrailingSlashRedirect) try {
266
266
  const loc = new URL(relativeLocation, options.request.url);
267
267
  if (loc.origin === new URL(options.request.url).origin) {
268
268
  const p = loc.pathname;
@@ -309,11 +309,14 @@ async function executeMiddleware(options) {
309
309
  const rewriteUrl = response.headers.get(MIDDLEWARE_REWRITE_HEADER);
310
310
  if (rewriteUrl) {
311
311
  let rewritePath;
312
+ let rewriteHadBasePath;
312
313
  try {
313
314
  const rewriteParsed = new URL(rewriteUrl, options.request.url);
314
315
  const requestOrigin = new URL(options.request.url).origin;
315
- if (rewriteParsed.origin === requestOrigin) rewritePath = (options.basePath ? stripBasePath(rewriteParsed.pathname, options.basePath) : rewriteParsed.pathname) + rewriteParsed.search;
316
- else rewritePath = rewriteParsed.href;
316
+ if (rewriteParsed.origin === requestOrigin) {
317
+ rewriteHadBasePath = !options.basePath || hasBasePath(rewriteParsed.pathname, options.basePath);
318
+ rewritePath = (options.basePath ? stripBasePath(rewriteParsed.pathname, options.basePath) : rewriteParsed.pathname) + rewriteParsed.search;
319
+ } else rewritePath = rewriteParsed.href;
317
320
  } catch {
318
321
  rewritePath = rewriteUrl;
319
322
  }
@@ -321,12 +324,22 @@ async function executeMiddleware(options) {
321
324
  return {
322
325
  continue: true,
323
326
  rewriteUrl: rewritePath,
327
+ rewriteHadBasePath,
324
328
  rewriteStatus: response.status !== 200 ? response.status : void 0,
325
329
  responseHeaders: collectMiddlewareHeaders(response),
326
330
  status: response.status !== 200 ? response.status : void 0,
327
331
  waitUntilPromises
328
332
  };
329
333
  }
334
+ if (response.headers.get("x-middleware-next") === "1") {
335
+ releaseMiddlewareRequestBody(nextRequest, waitUntilPromises);
336
+ return {
337
+ continue: true,
338
+ responseHeaders: collectMiddlewareHeaders(response),
339
+ status: response.status !== 200 ? response.status : void 0,
340
+ waitUntilPromises
341
+ };
342
+ }
330
343
  releaseMiddlewareRequestBody(nextRequest, waitUntilPromises, response.body === nextRequest.body);
331
344
  return {
332
345
  continue: false,
@@ -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): 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): Promise<MiddlewareResult>;
48
48
  //#endregion
49
49
  export { matchPattern, matchesMiddleware };