vinext 1.4.2 → 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 (88) hide show
  1. package/README.md +4 -1
  2. package/dist/build/prerender-paths.d.ts +11 -1
  3. package/dist/build/prerender-paths.js +36 -10
  4. package/dist/cli.js +1 -1
  5. package/dist/client/dev-error-overlay.d.ts +2 -1
  6. package/dist/client/dev-error-overlay.js +8 -3
  7. package/dist/config/config-matchers.js +6 -2
  8. package/dist/config/next-config.d.ts +9 -0
  9. package/dist/entries/app-rsc-entry.js +24 -12
  10. package/dist/entries/pages-server-entry.js +17 -4
  11. package/dist/index.js +22 -25
  12. package/dist/init-platform.js +2 -2
  13. package/dist/init.js +42 -5
  14. package/dist/server/api-handler.js +7 -3
  15. package/dist/server/app-browser-entry.js +16 -37
  16. package/dist/server/app-browser-error.d.ts +1 -1
  17. package/dist/server/app-browser-error.js +13 -4
  18. package/dist/server/app-browser-supplemental-refresh.d.ts +13 -0
  19. package/dist/server/app-browser-supplemental-refresh.js +36 -4
  20. package/dist/server/app-middleware.d.ts +2 -0
  21. package/dist/server/app-middleware.js +1 -0
  22. package/dist/server/app-page-boundary-render.js +2 -0
  23. package/dist/server/app-page-dispatch.d.ts +1 -0
  24. package/dist/server/app-page-dispatch.js +5 -1
  25. package/dist/server/app-page-element-builder.d.ts +2 -0
  26. package/dist/server/app-page-element-builder.js +2 -1
  27. package/dist/server/app-page-route-wiring.d.ts +7 -5
  28. package/dist/server/app-page-route-wiring.js +3 -1
  29. package/dist/server/app-pages-bridge.d.ts +6 -0
  30. package/dist/server/app-pages-bridge.js +8 -3
  31. package/dist/server/app-request-stage-independent-entry.js +2 -3
  32. package/dist/server/app-router-entry.js +2 -3
  33. package/dist/server/app-rsc-cache-busting.d.ts +8 -0
  34. package/dist/server/app-rsc-cache-busting.js +13 -1
  35. package/dist/server/app-rsc-combined-handler.js +2 -1
  36. package/dist/server/app-rsc-handler.d.ts +4 -0
  37. package/dist/server/app-rsc-handler.js +39 -22
  38. package/dist/server/app-rsc-response-finalizer.d.ts +15 -3
  39. package/dist/server/app-rsc-response-finalizer.js +51 -7
  40. package/dist/server/app-ssr-entry.js +8 -0
  41. package/dist/server/config-headers.d.ts +7 -0
  42. package/dist/server/config-headers.js +20 -1
  43. package/dist/server/image-animation.d.ts +9 -0
  44. package/dist/server/image-animation.js +118 -0
  45. package/dist/server/image-optimization.d.ts +25 -5
  46. package/dist/server/image-optimization.js +305 -21
  47. package/dist/server/image-source-reader.d.ts +34 -0
  48. package/dist/server/image-source-reader.js +126 -0
  49. package/dist/server/metadata-route-response.d.ts +6 -1
  50. package/dist/server/metadata-route-response.js +22 -4
  51. package/dist/server/middleware-matcher.d.ts +1 -0
  52. package/dist/server/middleware-matcher.js +1 -1
  53. package/dist/server/middleware-runtime.d.ts +5 -0
  54. package/dist/server/middleware-runtime.js +48 -15
  55. package/dist/server/middleware.d.ts +1 -1
  56. package/dist/server/middleware.js +2 -1
  57. package/dist/server/nitro-request-target-plugin.d.ts +17 -0
  58. package/dist/server/nitro-request-target-plugin.js +22 -0
  59. package/dist/server/open-redirect.d.ts +2 -1
  60. package/dist/server/open-redirect.js +2 -1
  61. package/dist/server/pages-client-assets.d.ts +1 -0
  62. package/dist/server/pages-i18n.d.ts +7 -0
  63. package/dist/server/pages-i18n.js +14 -1
  64. package/dist/server/pages-node-compat.js +7 -3
  65. package/dist/server/pages-page-handler.js +4 -4
  66. package/dist/server/pages-request-pipeline.d.ts +9 -0
  67. package/dist/server/pages-request-pipeline.js +29 -10
  68. package/dist/server/pages-request-stage-entry.js +7 -2
  69. package/dist/server/pages-router-entry.js +3 -0
  70. package/dist/server/prod-server.d.ts +1 -1
  71. package/dist/server/prod-server.js +48 -54
  72. package/dist/server/srvx-middleware-origin.d.ts +13 -0
  73. package/dist/server/srvx-middleware-origin.js +19 -0
  74. package/dist/server/srvx-request-target.d.ts +12 -0
  75. package/dist/server/srvx-request-target.js +20 -0
  76. package/dist/server/static-file-cache.js +10 -22
  77. package/dist/server/worker-image-optimization.d.ts +8 -1
  78. package/dist/server/worker-image-optimization.js +25 -6
  79. package/dist/server/worker-revalidation-context.js +1 -0
  80. package/dist/shims/error-boundary.d.ts +6 -2
  81. package/dist/shims/error-boundary.js +4 -2
  82. package/dist/shims/request-context.d.ts +3 -1
  83. package/dist/shims/url-utils.js +6 -1
  84. package/dist/utils/client-entry-manifest.d.ts +5 -0
  85. package/dist/utils/client-entry-manifest.js +1 -0
  86. package/dist/utils/client-runtime-metadata.d.ts +5 -0
  87. package/dist/utils/client-runtime-metadata.js +33 -7
  88. package/package.json +1 -1
@@ -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,7 +515,11 @@ async function writeMetadataRouteMiss(options, route, rendered, cachedEntry) {
501
515
  console.error("[vinext] Metadata route ISR cache write error:", error);
502
516
  }
503
517
  }
504
- async function handleMetadataRouteRequest(options) {
518
+ async function handleMetadataRouteRequest(requestOptions) {
519
+ const options = requestOptions.routePathname === requestOptions.cleanPathname ? {
520
+ ...requestOptions,
521
+ cleanPathname: normalizePathnameForRouteMatch(requestOptions.cleanPathname)
522
+ } : requestOptions;
505
523
  let urlParts;
506
524
  const getUrlParts = () => urlParts ??= options.cleanPathname.split("/").filter(Boolean);
507
525
  const routePathname = options.routePathname ?? options.cleanPathname;
@@ -5,6 +5,7 @@ export type MatcherConfig = string | Array<string | MiddlewareMatcherObject>;
5
5
  export type MiddlewareLocaleMatchContext = {
6
6
  kind: "defaulted";
7
7
  defaultLocale: string;
8
+ restoreRootTrailingSlash?: boolean;
8
9
  } | {
9
10
  kind: "internal" | "literal";
10
11
  } | {
@@ -60,7 +60,7 @@ function matchMatcherPattern(pathname, pattern, i18nConfig, localeContext) {
60
60
  function matchObjectMatcher(pathname, matcher, i18nConfig, localeContext) {
61
61
  if (localeContext?.kind === "next-data") return matchNextDataPattern(pathname, matcher.source, i18nConfig, matcher.locale !== false);
62
62
  if (matcher.locale !== false) return matchMatcherPattern(pathname, matcher.source, i18nConfig, localeContext);
63
- return matchRequestPattern(localeContext?.kind === "defaulted" ? `/${localeContext.defaultLocale}${pathname === "/" ? "" : pathname}` : pathname, matcher.source);
63
+ return matchRequestPattern(localeContext?.kind === "defaulted" ? `/${localeContext.defaultLocale}${pathname !== "/" ? pathname : localeContext.restoreRootTrailingSlash ? "/" : ""}` : pathname, matcher.source);
64
64
  }
65
65
  const APP_ROUTE_TRANSPORT_SUFFIX = "\\.rsc|\\.segments/.+\\.segment\\.rsc";
66
66
  const APP_ROUTE_TRANSPORT_SUFFIX_RE = /(?:\.rsc|\.segments\/.+\.segment\.rsc)$/;
@@ -61,6 +61,11 @@ type ExecuteMiddlewareOptions = {
61
61
  * second tee whose preserved side would never be consumed.
62
62
  */
63
63
  requestBodyAlreadyIsolated?: boolean;
64
+ /**
65
+ * `shouldRestoreLocaleRootTrailingSlash` for the raw request pathname. Next.js
66
+ * then tests the as-requested root as `/en/` after default-locale insertion.
67
+ */
68
+ restoreLocaleRootTrailingSlash?: boolean;
64
69
  request: Request;
65
70
  /**
66
71
  * The user's `trailingSlash` config. Plumbed into the NextRequest's NextURL
@@ -69,9 +69,26 @@ function middlewareMatcher(mod) {
69
69
  if (!isMiddlewareConfigExport(config)) return void 0;
70
70
  return config.matcher;
71
71
  }
72
+ const MIDDLEWARE_MESSAGE_HEADER_NAMES = [
73
+ "accept-encoding",
74
+ "connection",
75
+ "content-encoding",
76
+ "content-length",
77
+ "expect",
78
+ "keep-alive",
79
+ "keepalive",
80
+ "transfer-encoding"
81
+ ];
82
+ function deleteMiddlewareMessageHeaders(headers) {
83
+ for (const name of MIDDLEWARE_MESSAGE_HEADER_NAMES) headers.delete(name);
84
+ }
85
+ function processMiddlewareResponseHeaders(headers) {
86
+ processMiddlewareHeaders(headers);
87
+ deleteMiddlewareMessageHeaders(headers);
88
+ }
72
89
  function stripMiddlewareHeadersFromResponse(response) {
73
90
  const headers = new Headers(response.headers);
74
- processMiddlewareHeaders(headers);
91
+ processMiddlewareResponseHeaders(headers);
75
92
  return new Response(response.body, {
76
93
  status: response.status,
77
94
  statusText: response.statusText,
@@ -108,7 +125,7 @@ function relativizeLocation(location, requestUrl) {
108
125
  */
109
126
  function dataRedirectResponse(target, originalResponse) {
110
127
  const headers = new Headers(originalResponse.headers);
111
- processMiddlewareHeaders(headers);
128
+ processMiddlewareResponseHeaders(headers);
112
129
  headers.delete("Location");
113
130
  headers.set("x-nextjs-redirect", target);
114
131
  return new Response(null, {
@@ -120,6 +137,7 @@ function collectMiddlewareHeaders(response) {
120
137
  const responseHeaders = new Headers();
121
138
  for (const [key, value] of response.headers) if (!key.startsWith("x-middleware-") || shouldKeepMiddlewareHeader(key)) responseHeaders.append(key, value);
122
139
  dropEmptyMiddlewareSetCookie(responseHeaders);
140
+ deleteMiddlewareMessageHeaders(responseHeaders);
123
141
  return responseHeaders;
124
142
  }
125
143
  function drainFetchEvent(fetchEvent) {
@@ -209,12 +227,19 @@ async function executeMiddlewareModule(options) {
209
227
  };
210
228
  const isStaleDataRequest = options.isDataRequest === true && options.isStaleDataRequest === true;
211
229
  const toPageMatcherPathname = (candidate) => candidate !== null && options.isDataRequest && !isStaleDataRequest ? nextDataMiddlewareMatchPathname(candidate) ?? candidate : candidate;
212
- const encodedMatchPathname = toPageMatcherPathname(prepareMatcherPathname(encodedRequestPathname));
213
- let decodedMatchPathname = encodedMatchPathname;
214
- try {
215
- if (encodedMatchPathname !== null) decodedMatchPathname = decodeURIComponent(encodedMatchPathname);
216
- else if (!options.i18nConfig) decodedMatchPathname = toPageMatcherPathname(prepareMatcherPathname(decodeURIComponent(encodedRequestPathname)));
217
- } catch {}
230
+ const toMatchPathnames = (candidate) => {
231
+ const encoded = toPageMatcherPathname(prepareMatcherPathname(candidate));
232
+ let decoded = encoded;
233
+ try {
234
+ if (encoded !== null) decoded = decodeURIComponent(encoded);
235
+ else if (!options.i18nConfig) decoded = toPageMatcherPathname(prepareMatcherPathname(decodeURIComponent(candidate)));
236
+ } catch {}
237
+ return {
238
+ decoded,
239
+ encoded
240
+ };
241
+ };
242
+ const { decoded: decodedMatchPathname, encoded: encodedMatchPathname } = toMatchPathnames(encodedRequestPathname);
218
243
  let localeContext;
219
244
  if (isStaleDataRequest) localeContext = { kind: "next-data" };
220
245
  else if (options.i18nConfig && encodedMatchPathname !== null) {
@@ -226,12 +251,20 @@ async function executeMiddlewareModule(options) {
226
251
  kind: "defaulted"
227
252
  };
228
253
  }
229
- const encodedPathMatches = encodedMatchPathname !== null && matchesMiddlewarePathname(encodedMatchPathname, matcher, options.i18nConfig, localeContext);
230
- const decodedPathMatches = !encodedPathMatches && decodedMatchPathname !== null && decodedMatchPathname !== encodedMatchPathname && matchesMiddlewarePathname(decodedMatchPathname, matcher, options.i18nConfig, localeContext);
231
- if (encodedPathMatches || decodedPathMatches) options.onPathMatch?.();
232
- const encodedMatches = encodedMatchPathname !== null && matchesMiddleware(encodedMatchPathname, matcher, options.request, options.i18nConfig, localeContext);
233
- const decodedMatches = !encodedMatches && decodedMatchPathname !== null && decodedMatchPathname !== encodedMatchPathname && matchesMiddleware(decodedMatchPathname, matcher, options.request, options.i18nConfig, localeContext);
234
- if (!encodedMatches && !decodedMatches) return { continue: true };
254
+ const slashedRequestPathname = normalizePath(requestPathname);
255
+ const requestedMatchPathnames = slashedRequestPathname !== encodedRequestPathname ? toMatchPathnames(slashedRequestPathname) : {
256
+ decoded: decodedMatchPathname,
257
+ encoded: encodedMatchPathname
258
+ };
259
+ const candidatePathnames = [];
260
+ if (requestedMatchPathnames.encoded !== null) candidatePathnames.push(requestedMatchPathnames.encoded);
261
+ if (requestedMatchPathnames.decoded !== null && requestedMatchPathnames.decoded !== requestedMatchPathnames.encoded) candidatePathnames.push(requestedMatchPathnames.decoded);
262
+ const requestedLocaleContext = options.restoreLocaleRootTrailingSlash && localeContext?.kind === "defaulted" ? {
263
+ ...localeContext,
264
+ restoreRootTrailingSlash: true
265
+ } : localeContext;
266
+ if (candidatePathnames.some((candidate) => matchesMiddlewarePathname(candidate, matcher, options.i18nConfig, requestedLocaleContext))) options.onPathMatch?.();
267
+ if (!candidatePathnames.some((candidate) => matchesMiddleware(candidate, matcher, options.request, options.i18nConfig, requestedLocaleContext))) return { continue: true };
235
268
  options.onMatch?.();
236
269
  const middlewareUrl = withOrigin(requestUrl, getRequestExecutionContext()?.middlewareOrigin ?? requestUrl.origin);
237
270
  const nextRequest = createNextRequest(options.request, middlewareUrl, options.i18nConfig, options.basePath, options.trailingSlash, hadBasePath, options.requestBodyAlreadyIsolated, options.skipTrailingSlashRedirect);
@@ -305,7 +338,7 @@ async function executeMiddlewareModule(options) {
305
338
  }
306
339
  const responseHeaders = new Headers(response.headers);
307
340
  responseHeaders.delete("location");
308
- processMiddlewareHeaders(responseHeaders);
341
+ processMiddlewareResponseHeaders(responseHeaders);
309
342
  const relativizedResponseHeaders = new Headers(response.headers);
310
343
  relativizedResponseHeaders.set("Location", normalizedLocation);
311
344
  const relativizedResponse = new Response(response.body, {
@@ -44,6 +44,6 @@ export declare function findMiddlewareFile(root: string, fileMatcher: ValidFileM
44
44
  * @param request - The incoming Request object
45
45
  * @returns Middleware result describing what action to take
46
46
  */
47
- export declare function runMiddleware(runner: ModuleRunner, middlewarePath: string, request: Request, i18nConfig?: NextI18nConfig | null, basePath?: string, trailingSlash?: boolean, isDataRequest?: boolean, normalizedPathname?: string, isStaleDataRequest?: boolean, skipTrailingSlashRedirect?: boolean): Promise<MiddlewareResult>;
47
+ export declare function runMiddleware(runner: ModuleRunner, middlewarePath: string, request: Request, i18nConfig?: NextI18nConfig | null, basePath?: string, trailingSlash?: boolean, isDataRequest?: boolean, normalizedPathname?: string, isStaleDataRequest?: boolean, skipTrailingSlashRedirect?: boolean, restoreLocaleRootTrailingSlash?: boolean): Promise<MiddlewareResult>;
48
48
  //#endregion
49
49
  export { matchPattern, matchesMiddleware };
@@ -77,7 +77,7 @@ function isMiddlewareModule(value) {
77
77
  * @param request - The incoming Request object
78
78
  * @returns Middleware result describing what action to take
79
79
  */
80
- async function runMiddleware(runner, middlewarePath, request, i18nConfig, basePath, trailingSlash, isDataRequest, normalizedPathname, isStaleDataRequest, skipTrailingSlashRedirect) {
80
+ async function runMiddleware(runner, middlewarePath, request, i18nConfig, basePath, trailingSlash, isDataRequest, normalizedPathname, isStaleDataRequest, skipTrailingSlashRedirect, restoreLocaleRootTrailingSlash) {
81
81
  const mod = await runner.import(middlewarePath);
82
82
  if (!isMiddlewareModule(mod)) throw new Error(`Middleware module "${middlewarePath}" did not evaluate to an object.`);
83
83
  return runGeneratedMiddleware({
@@ -92,6 +92,7 @@ async function runMiddleware(runner, middlewarePath, request, i18nConfig, basePa
92
92
  module: mod,
93
93
  normalizedPathname,
94
94
  request,
95
+ restoreLocaleRootTrailingSlash,
95
96
  skipTrailingSlashRedirect,
96
97
  trailingSlash
97
98
  });
@@ -0,0 +1,17 @@
1
+ //#region src/server/nitro-request-target-plugin.d.ts
2
+ /**
3
+ * Nitro runtime plugin that records each request's raw target before h3 sees
4
+ * it. srvx 0.11 writes the pathname h3 decodes (one with a `%`) back into the
5
+ * Node request's `url`, dropping the authority of an absolute-form target,
6
+ * so vinext's request-based entries read the target this plugin saved.
7
+ *
8
+ * This module runs in Nitro's server bundle, separate from vinext's request
9
+ * handlers, so it imports nothing and shares state through `Symbol.for`.
10
+ */
11
+ export declare const NITRO_REQUEST_TARGET_KEY = "vinext.nitro.requestTarget";
12
+ type NitroApp = {
13
+ fetch: (request: Request) => Response | Promise<Response>;
14
+ };
15
+ export default function nitroRequestTargetPlugin(nitroApp: NitroApp): void;
16
+ //#endregion
17
+ export {};
@@ -0,0 +1,22 @@
1
+ //#region src/server/nitro-request-target-plugin.ts
2
+ /**
3
+ * Nitro runtime plugin that records each request's raw target before h3 sees
4
+ * it. srvx 0.11 writes the pathname h3 decodes (one with a `%`) back into the
5
+ * Node request's `url`, dropping the authority of an absolute-form target,
6
+ * so vinext's request-based entries read the target this plugin saved.
7
+ *
8
+ * This module runs in Nitro's server bundle, separate from vinext's request
9
+ * handlers, so it imports nothing and shares state through `Symbol.for`.
10
+ */
11
+ const NITRO_REQUEST_TARGET_KEY = "vinext.nitro.requestTarget";
12
+ function nitroRequestTargetPlugin(nitroApp) {
13
+ const appFetch = nitroApp.fetch;
14
+ const key = Symbol.for(NITRO_REQUEST_TARGET_KEY);
15
+ nitroApp.fetch = (request) => {
16
+ const req = request.runtime?.node?.req;
17
+ if (req && !Reflect.has(req, key)) Reflect.set(req, key, req.url);
18
+ return appFetch(request);
19
+ };
20
+ }
21
+ //#endregion
22
+ export { NITRO_REQUEST_TARGET_KEY, nitroRequestTargetPlugin as default };
@@ -43,7 +43,8 @@ export type RepeatedSlashRedirect = {
43
43
  * `//` after the scheme trips the same check) and 500s `OPTIONS *`. Routing
44
44
  * them would let the request line choose the origin of `request.url` that
45
45
  * middleware and handlers see, bypassing the Host trust policy. Request-based
46
- * entries always pass a pathname.
46
+ * entries pass a pathname. Where srvx builds `request.url` from the raw target
47
+ * (Nitro's Node server), `isSrvxAbsoluteFormTarget` 404s these instead.
47
48
  *
48
49
  * @param rawUrl - The raw request target: pathname plus optional `?query`
49
50
  */
@@ -44,7 +44,8 @@ const REDIRECT_LOCATION_BASE = "http://vinext.invalid";
44
44
  * `//` after the scheme trips the same check) and 500s `OPTIONS *`. Routing
45
45
  * them would let the request line choose the origin of `request.url` that
46
46
  * middleware and handlers see, bypassing the Host trust policy. Request-based
47
- * entries always pass a pathname.
47
+ * entries pass a pathname. Where srvx builds `request.url` from the raw target
48
+ * (Nitro's Node server), `isSrvxAbsoluteFormTarget` 404s these instead.
48
49
  *
49
50
  * @param rawUrl - The raw request target: pathname plus optional `?query`
50
51
  */
@@ -4,6 +4,7 @@ export type AssetCrossOrigin = "" | "anonymous" | "use-credentials";
4
4
  export type PagesClientAssets = {
5
5
  clientEntry?: string;
6
6
  appBootstrapPreinitModules?: string[];
7
+ appBootstrapPreloadModules?: string[];
7
8
  appRouteManifestModule?: string;
8
9
  /** What each App Router client reference loads in the browser, by URL. */
9
10
  appClientReferenceChunks?: ClientReferenceChunks;
@@ -24,6 +24,13 @@ type PagesI18nRequestInfo = {
24
24
  domainLocale?: DomainLocale;
25
25
  redirectUrl?: string;
26
26
  };
27
+ /**
28
+ * Mark a locale-detection redirect. Next.js answers it before next.config
29
+ * headers are staged, so it goes out without them. The global symbol crosses
30
+ * the App Router / Pages Router module graph boundary.
31
+ */
32
+ export declare function markLocaleRedirect(response: Response): Response;
33
+ export declare function isLocaleRedirect(response: Response): boolean;
27
34
  /**
28
35
  * Return the configured locale that the first pathname segment names, compared
29
36
  * case-insensitively (`/EN/about` -> `"en"`), or `undefined` when there is none.
@@ -8,6 +8,19 @@ function readHeader(headers, name) {
8
8
  return direct;
9
9
  }
10
10
  const normalizeHostname = normalizeDomainHostname;
11
+ const LOCALE_REDIRECT = Symbol.for("vinext.pages-locale-redirect");
12
+ /**
13
+ * Mark a locale-detection redirect. Next.js answers it before next.config
14
+ * headers are staged, so it goes out without them. The global symbol crosses
15
+ * the App Router / Pages Router module graph boundary.
16
+ */
17
+ function markLocaleRedirect(response) {
18
+ if (!isLocaleRedirect(response)) Object.defineProperty(response, LOCALE_REDIRECT, { value: true });
19
+ return response;
20
+ }
21
+ function isLocaleRedirect(response) {
22
+ return LOCALE_REDIRECT in response;
23
+ }
11
24
  /**
12
25
  * Return the configured locale that the first pathname segment names, compared
13
26
  * case-insensitively (`/EN/about` -> `"en"`), or `undefined` when there is none.
@@ -259,4 +272,4 @@ function resolvePagesI18nRequest(url, i18nConfig, headers, hostname, basePath =
259
272
  };
260
273
  }
261
274
  //#endregion
262
- export { detectDomainLocale, detectLocaleFromAcceptLanguage, detectPathnameLocale, extractLocaleFromUrl, getLocaleRedirect, normalizeDefaultLocalePathname, parseCookieLocaleFromHeader, resolvePagesI18nRequest, shouldRestoreLocaleRootTrailingSlash, stripI18nLocaleForApiRoute };
275
+ export { detectDomainLocale, detectLocaleFromAcceptLanguage, detectPathnameLocale, extractLocaleFromUrl, getLocaleRedirect, isLocaleRedirect, markLocaleRedirect, normalizeDefaultLocalePathname, parseCookieLocaleFromHeader, resolvePagesI18nRequest, shouldRestoreLocaleRootTrailingSlash, stripI18nLocaleForApiRoute };
@@ -201,10 +201,14 @@ var PagesResponseStream = class extends Writable {
201
201
  return this;
202
202
  }
203
203
  json(data) {
204
- this.resHeaders["content-type"] = "application/json";
204
+ this.resHeaders["content-type"] = "application/json; charset=utf-8";
205
205
  this.end(JSON.stringify(data));
206
206
  }
207
207
  send(data) {
208
+ if (data === null || data === void 0) {
209
+ this.end();
210
+ return;
211
+ }
208
212
  if (NO_BODY_RESPONSE_STATUSES.has(this.resStatusCode)) {
209
213
  this.end();
210
214
  return;
@@ -220,8 +224,8 @@ var PagesResponseStream = class extends Writable {
220
224
  this.end(data);
221
225
  return;
222
226
  }
223
- if (typeof data === "object" && data !== null) {
224
- this.resHeaders["content-type"] = "application/json";
227
+ if (typeof data === "object" || typeof data === "number" || typeof data === "boolean") {
228
+ this.resHeaders["content-type"] = "application/json; charset=utf-8";
225
229
  this.end(JSON.stringify(data));
226
230
  return;
227
231
  }
@@ -5,7 +5,7 @@ import { NEXTJS_CACHE_HEADER, NEXTJS_DEPLOYMENT_ID_HEADER, VINEXT_CACHE_HEADER,
5
5
  import { normalizePathnameForRouteMatch } from "../routing/utils.js";
6
6
  import { patternToNextFormat } from "../routing/route-validation.js";
7
7
  import { getRequestExecutionContext } from "../shims/request-context.js";
8
- import { extractLocaleFromUrl, resolvePagesI18nRequest } from "./pages-i18n.js";
8
+ import { extractLocaleFromUrl, markLocaleRedirect, resolvePagesI18nRequest } from "./pages-i18n.js";
9
9
  import { buildNextDataNotFoundResponse, buildNextDataPropsJsonResponse, normalizePagesDataRequest, parseNextDataPathname } from "./pages-data-route.js";
10
10
  import { reportRequestError } from "./instrumentation.js";
11
11
  import { logRequestError } from "./request-error-log.js";
@@ -154,7 +154,7 @@ function createPagesPageHandler(opts) {
154
154
  const originalRequestPathAndSearch = originalRequestUrl.pathname + originalRequestUrl.search;
155
155
  let dataRequestPathname = null;
156
156
  let dataRequestSearch = "";
157
- const initialDataNorm = normalizePagesDataRequest(request, buildId, vinextConfig.basePath, hasMiddleware && vinextConfig.trailingSlash);
157
+ const initialDataNorm = normalizePagesDataRequest(request, buildId, "", hasMiddleware && vinextConfig.trailingSlash);
158
158
  if (!isDataReq) {
159
159
  if (initialDataNorm.notFoundResponse) return initialDataNorm.notFoundResponse;
160
160
  if (initialDataNorm.isDataReq) {
@@ -195,10 +195,10 @@ function createPagesPageHandler(opts) {
195
195
  const currentDefaultLocale = i18nConfig ? localeInfo.domainLocale ? localeInfo.domainLocale.defaultLocale : i18nConfig.defaultLocale : void 0;
196
196
  const domainLocales = i18nConfig ? i18nConfig.domains : void 0;
197
197
  const pageIsrCacheKey = isrCacheKeyForRequest(i18nConfig ? localeInfo.domainLocale ? JSON.stringify([String(localeInfo.domainLocale.domain).toLowerCase(), locale]) : "locale:" + String(locale) : null);
198
- if (localeInfo.redirectUrl) return new Response(null, {
198
+ if (localeInfo.redirectUrl) return markLocaleRedirect(new Response(null, {
199
199
  status: 307,
200
200
  headers: { Location: localeInfo.redirectUrl }
201
- });
201
+ }));
202
202
  let match = options && options.__forcedRoute ? {
203
203
  route: options.__forcedRoute,
204
204
  params: {}
@@ -15,6 +15,14 @@ type PageRouteMatch = {
15
15
  dataKind?: PagesRouteDataKind;
16
16
  };
17
17
  };
18
+ /**
19
+ * Whether a filesystem route may serve `requestPathname` from the client
20
+ * build directory. Like Next.js, only a public file or a build asset resolves
21
+ * there: the directory also holds build metadata and private artifacts. A
22
+ * direct GET/HEAD reaches this route only for a public file, since servers
23
+ * answer existing build assets before middleware.
24
+ */
25
+ export declare function isServableFilesystemRoute(requestPathname: string, phase: FilesystemRoutePhase, method: string, publicFiles: ReadonlySet<string>, basePath?: string, assetPathPrefix?: string, pathnameHasBasePath?: boolean): boolean;
18
26
  export declare function fetchWorkerFilesystemRoute(request: Request, requestPathname: string, phase: FilesystemRoutePhase, fetchAsset: (request: Request) => Promise<Response>, publicFiles: ReadonlySet<string>, basePath?: string, assetPathPrefix?: string, pathnameHasBasePath?: boolean): Promise<Response | false>;
19
27
  export type MiddlewareResult = {
20
28
  continue: boolean;
@@ -77,6 +85,7 @@ export type PagesPipelineDeps = {
77
85
  runMiddleware?: ((request: Request, ctx: unknown, opts: {
78
86
  isDataRequest: boolean;
79
87
  isStaleDataRequest?: boolean;
88
+ restoreLocaleRootTrailingSlash?: boolean;
80
89
  }) => Promise<MiddlewareResult>) | null;
81
90
  /** Original URL presented to middleware when URL normalization is disabled. */
82
91
  middlewareRequest?: Request;
@@ -19,6 +19,7 @@ import { setFrameworkRequestRoute } from "./request-tracing.js";
19
19
  import { withExternalRewriteResponseHeaders } from "./middleware-response-headers.js";
20
20
  import { applyConfigHeadersToHeaderRecord } from "./config-headers.js";
21
21
  import { finalizeMissingStaticAssetResponse, mergeHeaders, mergeStaticFileHeaders } from "./worker-utils.js";
22
+ import { publicFileRouteKey } from "../utils/public-file-path.js";
22
23
  //#region src/server/pages-request-pipeline.ts
23
24
  function ruleUsesUnkeyedRequestCondition(rule) {
24
25
  return [...rule.has ?? [], ...rule.missing ?? []].some((condition) => condition.type === "header" || condition.type === "cookie" || condition.type === "host");
@@ -35,24 +36,41 @@ function headersFromRecord(record) {
35
36
  else headers.set(name, value);
36
37
  return headers;
37
38
  }
38
- async function fetchWorkerFilesystemRoute(request, requestPathname, phase, fetchAsset, publicFiles, basePath = "", assetPathPrefix = "", pathnameHasBasePath = true) {
39
- const isRetrievalMethod = request.method === "GET" || request.method === "HEAD";
40
- const assetUrl = new URL(request.url);
39
+ /**
40
+ * Whether a filesystem route may serve `requestPathname` from the client
41
+ * build directory. Like Next.js, only a public file or a build asset resolves
42
+ * there: the directory also holds build metadata and private artifacts. A
43
+ * direct GET/HEAD reaches this route only for a public file, since servers
44
+ * answer existing build assets before middleware.
45
+ */
46
+ function isServableFilesystemRoute(requestPathname, phase, method, publicFiles, basePath = "", assetPathPrefix = "", pathnameHasBasePath = true) {
47
+ const assetUrl = new URL("http://localhost");
41
48
  assetUrl.pathname = requestPathname;
42
- assetUrl.search = "";
43
49
  const decodedAssetUrl = new URL(assetUrl);
44
50
  let publicFileKey;
51
+ let servedPathname;
52
+ let servedFileKey;
45
53
  try {
46
54
  decodedAssetUrl.pathname = decodeURIComponent(assetUrl.pathname);
47
55
  publicFileKey = normalizePathnameForRouteMatchStrict(assetUrl.pathname);
56
+ servedPathname = decodeURIComponent(requestPathname);
57
+ servedFileKey = publicFileRouteKey(servedPathname);
48
58
  } catch {
49
59
  return false;
50
60
  }
51
- if (!pathnameHasBasePath && !isAssetPrefixBuildAsset(decodedAssetUrl.pathname, assetPathPrefix)) return false;
52
- const isPublicFile = pathnameHasBasePath && publicFiles.has(publicFileKey);
61
+ const decodedPathname = decodedAssetUrl.pathname;
62
+ if (!pathnameHasBasePath && !isAssetPrefixBuildAsset(decodedPathname, assetPathPrefix)) return false;
63
+ const isPublicFile = pathnameHasBasePath && publicFiles.has(publicFileKey) && publicFiles.has(servedFileKey);
53
64
  if (!isPublicFile && (requestPathname === "/api" || requestPathname.startsWith("/api/"))) return false;
54
- if (phase === "direct" && isRetrievalMethod && !isPublicFile) return false;
55
- if (!isPublicFile && !isNextStaticPath(decodedAssetUrl.pathname, basePath, assetPathPrefix)) return false;
65
+ if (phase === "direct" && (method === "GET" || method === "HEAD") && !isPublicFile) return false;
66
+ return isPublicFile || isNextStaticPath(decodedPathname, basePath, assetPathPrefix) && isNextStaticPath(servedPathname, basePath, assetPathPrefix);
67
+ }
68
+ async function fetchWorkerFilesystemRoute(request, requestPathname, phase, fetchAsset, publicFiles, basePath = "", assetPathPrefix = "", pathnameHasBasePath = true) {
69
+ const isRetrievalMethod = request.method === "GET" || request.method === "HEAD";
70
+ const assetUrl = new URL(request.url);
71
+ assetUrl.pathname = requestPathname;
72
+ assetUrl.search = "";
73
+ if (!isServableFilesystemRoute(requestPathname, phase, request.method, publicFiles, basePath, assetPathPrefix, pathnameHasBasePath)) return false;
56
74
  const response = await fetchAsset(isRetrievalMethod ? new Request(assetUrl, request) : new Request(assetUrl, {
57
75
  method: "HEAD",
58
76
  headers: request.headers
@@ -179,7 +197,8 @@ async function runPagesRequest(request, deps) {
179
197
  if (!isOnDemandRevalidate && typeof deps.runMiddleware === "function") {
180
198
  const result = await deps.runMiddleware(deps.middlewareRequest ?? request, deps.ctx ?? null, {
181
199
  isDataRequest,
182
- isStaleDataRequest: isDataRequest && !!deps.dataNotFoundResponse
200
+ isStaleDataRequest: isDataRequest && !!deps.dataNotFoundResponse,
201
+ restoreLocaleRootTrailingSlash: deps.restoreLocaleRootTrailingSlash
183
202
  });
184
203
  if (deps.recordCacheability !== false && result.pathnameEligible) markRouteCacheabilityDynamic("middleware can match this pathname");
185
204
  if (result.waitUntilPromises && result.waitUntilPromises.length > 0) {
@@ -465,4 +484,4 @@ async function runPagesRequest(request, deps) {
465
484
  };
466
485
  }
467
486
  //#endregion
468
- export { fetchWorkerFilesystemRoute, runPagesRequest, wrapMiddlewareWithBasePath };
487
+ export { fetchWorkerFilesystemRoute, isServableFilesystemRoute, runPagesRequest, wrapMiddlewareWithBasePath };
@@ -73,7 +73,9 @@ const imageConfig = vinextConfig?.images ? {
73
73
  dangerouslyAllowSVG: vinextConfig.images.dangerouslyAllowSVG,
74
74
  dangerouslyAllowLocalIP: vinextConfig.images.dangerouslyAllowLocalIP,
75
75
  contentDispositionType: vinextConfig.images.contentDispositionType,
76
- contentSecurityPolicy: vinextConfig.images.contentSecurityPolicy
76
+ contentSecurityPolicy: vinextConfig.images.contentSecurityPolicy,
77
+ minimumCacheTTL: vinextConfig.images.minimumCacheTTL,
78
+ maximumResponseBody: vinextConfig.images.maximumResponseBody
77
79
  } : void 0;
78
80
  /**
79
81
  * Whether the request stage serves this request as a client build file
@@ -384,7 +386,10 @@ async function handleRequestImpl(request, env, platformCtx, dispatchResponseStag
384
386
  });
385
387
  if (sourceResult.type !== "response") return null;
386
388
  await sourceResult.response.body?.cancel().catch(() => {});
387
- if (isImageSourceBodyStatus(sourceResult.response.status)) return sourceAsset;
389
+ if (isImageSourceBodyStatus(sourceResult.response.status) && sourceAsset !== null) {
390
+ const { withStagedCacheControl } = await import("./worker-image-optimization.js");
391
+ return withStagedCacheControl(sourceAsset, sourceResult.response.headers.get("Cache-Control"));
392
+ }
388
393
  await sourceAsset?.body?.cancel().catch(() => {});
389
394
  return null;
390
395
  };
@@ -1,10 +1,12 @@
1
1
  import { VINEXT_CACHEABILITY_PROBE_HEADER, VINEXT_CACHEABILITY_PROBE_QUERY_PARAM } from "./headers.js";
2
+ import { notFoundResponse } from "./http-error-responses.js";
2
3
  import { cloneRequestWithHeaders, cloneRequestWithUrl, stripRequestUrlFragment } from "./request-pipeline.js";
3
4
  import { runWithExecutionContext } from "../shims/request-context.js";
4
5
  import { getCdnCacheAdapter } from "../shims/cdn-cache.js";
5
6
  import { validateCdnRequest } from "./cache-control.js";
6
7
  import { createWorkerRevalidationContext } from "./worker-revalidation-context.js";
7
8
  import { createWorkerPrerenderDiscoveryContext, createWorkerPrerenderReadinessResponse } from "./worker-prerender-discovery.js";
9
+ import { isSrvxAbsoluteFormTarget } from "./srvx-request-target.js";
8
10
  import { handleRequestStageLocally, isPagesClientBuildFileRequest, pagesRequestStageBuildId, pagesRequestStagePrerenderSecret } from "./pages-request-stage-entry.js";
9
11
  import { renderPagesResponse } from "./pages-response-stage-entry.js";
10
12
  import { registerConfiguredCacheAdapters } from "virtual:vinext-cache-adapters";
@@ -52,6 +54,7 @@ async function handleSingleStageRequest(request, env, platformCtx) {
52
54
  return finalize ? finalize(response, ctx) : response;
53
55
  }
54
56
  var pages_router_entry_default = { fetch(request, env, ctx) {
57
+ if (isSrvxAbsoluteFormTarget(request)) return Promise.resolve(notFoundResponse());
55
58
  const strippedRequest = stripRequestUrlFragment(request);
56
59
  const handle = () => handleSingleStageRequest(strippedRequest, env, ctx);
57
60
  return ctx && typeof ctx.waitUntil === "function" ? runWithExecutionContext(ctx, handle) : handle();
@@ -90,7 +90,7 @@ declare function mergeWebResponse(middlewareHeaders: Record<string, string | str
90
90
  * Send a compressed response if the content type is compressible and the
91
91
  * client supports compression. Otherwise send uncompressed.
92
92
  */
93
- declare function sendCompressed(req: IncomingMessage, res: ServerResponse, body: string | Buffer, contentType: string, statusCode: number, extraHeaders?: Record<string, string | string[]>, compress?: boolean, statusText?: string): void;
93
+ declare function sendCompressed(req: IncomingMessage, res: ServerResponse, body: string | Buffer, contentType: string | undefined, statusCode: number, extraHeaders?: Record<string, string | string[]>, compress?: boolean, statusText?: string): void;
94
94
  /**
95
95
  * Try to serve a static file from the client build directory.
96
96
  *