vinext 1.4.0 → 1.4.2

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 (156) hide show
  1. package/README.md +21 -21
  2. package/dist/build/action-owner-manifest.js +1 -0
  3. package/dist/build/prerender.js +3 -2
  4. package/dist/check.js +4 -7
  5. package/dist/client/dev-error-overlay-store.d.ts +1 -0
  6. package/dist/client/dev-error-overlay.d.ts +2 -0
  7. package/dist/client/dev-error-overlay.js +19 -3
  8. package/dist/config/config-matchers.js +4 -2
  9. package/dist/config/destination-params.d.ts +2 -1
  10. package/dist/config/destination-params.js +55 -2
  11. package/dist/config/next-config.d.ts +22 -1
  12. package/dist/config/next-config.js +49 -4
  13. package/dist/entries/app-rsc-entry.js +41 -19
  14. package/dist/entries/app-rsc-manifest.js +4 -0
  15. package/dist/entries/pages-server-entry.js +5 -1
  16. package/dist/index.js +44 -32
  17. package/dist/init-cloudflare.js +18 -2
  18. package/dist/plugins/css-target.d.ts +18 -0
  19. package/dist/plugins/css-target.js +93 -0
  20. package/dist/plugins/strip-server-exports.js +2 -1
  21. package/dist/routing/app-route-graph.d.ts +10 -0
  22. package/dist/routing/app-route-graph.js +15 -1
  23. package/dist/server/api-handler.d.ts +2 -0
  24. package/dist/server/api-handler.js +5 -6
  25. package/dist/server/app-action-forwarding.js +3 -1
  26. package/dist/server/app-browser-entry.js +28 -7
  27. package/dist/server/app-browser-rsc-redirect.js +3 -3
  28. package/dist/server/app-browser-server-action-client.js +2 -2
  29. package/dist/server/app-browser-visible-commit.js +2 -2
  30. package/dist/server/app-elements.d.ts +3 -0
  31. package/dist/server/app-elements.js +14 -1
  32. package/dist/server/app-fallback-renderer.js +2 -2
  33. package/dist/server/app-optimistic-routing.d.ts +12 -1
  34. package/dist/server/app-optimistic-routing.js +40 -19
  35. package/dist/server/app-page-boundary-render.js +7 -12
  36. package/dist/server/app-page-cache-render.js +1 -1
  37. package/dist/server/app-page-dispatch.d.ts +10 -2
  38. package/dist/server/app-page-dispatch.js +73 -31
  39. package/dist/server/app-page-element-builder.d.ts +9 -20
  40. package/dist/server/app-page-element-builder.js +40 -52
  41. package/dist/server/app-page-execution.js +9 -6
  42. package/dist/server/app-page-intercept-segment-config.d.ts +56 -0
  43. package/dist/server/app-page-intercept-segment-config.js +90 -0
  44. package/dist/server/app-page-navigation-params.d.ts +23 -0
  45. package/dist/server/app-page-navigation-params.js +49 -0
  46. package/dist/server/app-page-render.d.ts +5 -3
  47. package/dist/server/app-page-render.js +31 -17
  48. package/dist/server/app-page-request.d.ts +24 -2
  49. package/dist/server/app-page-request.js +10 -10
  50. package/dist/server/app-page-response.js +1 -1
  51. package/dist/server/app-page-route-wiring.d.ts +10 -0
  52. package/dist/server/app-page-route-wiring.js +34 -13
  53. package/dist/server/app-page-segment-plan.js +9 -1
  54. package/dist/server/app-page-segment-state.d.ts +9 -0
  55. package/dist/server/app-page-segment-state.js +54 -1
  56. package/dist/server/app-page-stream.d.ts +10 -8
  57. package/dist/server/app-page-stream.js +2 -1
  58. package/dist/server/app-request-stage-independent-entry.js +1 -1
  59. package/dist/server/app-response-stage-entry.js +1 -0
  60. package/dist/server/app-route-handler-execution.js +3 -2
  61. package/dist/server/app-route-handler-runtime.d.ts +1 -1
  62. package/dist/server/app-route-handler-runtime.js +11 -7
  63. package/dist/server/app-route-module-loader.d.ts +2 -0
  64. package/dist/server/app-route-module-loader.js +1 -0
  65. package/dist/server/app-router-entry.js +1 -1
  66. package/dist/server/app-rsc-cache-busting.d.ts +10 -10
  67. package/dist/server/app-rsc-cache-busting.js +27 -68
  68. package/dist/server/app-rsc-error-handler.js +1 -1
  69. package/dist/server/app-rsc-errors.js +4 -1
  70. package/dist/server/app-rsc-handler.d.ts +10 -1
  71. package/dist/server/app-rsc-handler.js +126 -103
  72. package/dist/server/app-rsc-request-normalization.d.ts +10 -12
  73. package/dist/server/app-rsc-request-normalization.js +11 -14
  74. package/dist/server/app-rsc-response-finalizer.d.ts +3 -0
  75. package/dist/server/app-rsc-response-finalizer.js +11 -1
  76. package/dist/server/app-rsc-response-stage.js +4 -12
  77. package/dist/server/app-rsc-route-matching.d.ts +14 -0
  78. package/dist/server/app-rsc-route-matching.js +42 -18
  79. package/dist/server/app-server-action-execution.js +24 -14
  80. package/dist/server/app-ssr-entry.d.ts +6 -0
  81. package/dist/server/app-ssr-entry.js +13 -8
  82. package/dist/server/app-worker-stages.d.ts +12 -3
  83. package/dist/server/app-worker-stages.js +37 -4
  84. package/dist/server/bfcache-identity.d.ts +7 -0
  85. package/dist/server/bfcache-identity.js +10 -1
  86. package/dist/server/cache-control.d.ts +2 -0
  87. package/dist/server/cache-control.js +3 -1
  88. package/dist/server/cacheability-manifest.js +1 -2
  89. package/dist/server/dev-origin-check.d.ts +2 -1
  90. package/dist/server/dev-origin-check.js +2 -1
  91. package/dist/server/dev-server.js +2 -1
  92. package/dist/server/image-local-patterns.d.ts +21 -0
  93. package/dist/server/image-local-patterns.js +59 -0
  94. package/dist/server/image-optimization.d.ts +28 -22
  95. package/dist/server/image-optimization.js +17 -2
  96. package/dist/server/isr-cache.js +4 -1
  97. package/dist/server/metadata-route-response.js +25 -7
  98. package/dist/server/middleware-matcher.js +19 -6
  99. package/dist/server/middleware-runtime.js +6 -2
  100. package/dist/server/multi-stage.d.ts +7 -3
  101. package/dist/server/nitro-public-files-plugin.js +20 -8
  102. package/dist/server/pages-data-route.d.ts +5 -1
  103. package/dist/server/pages-data-route.js +14 -6
  104. package/dist/server/pages-node-compat.d.ts +1 -1
  105. package/dist/server/pages-node-compat.js +2 -0
  106. package/dist/server/pages-page-data.js +2 -1
  107. package/dist/server/pages-page-handler.js +17 -6
  108. package/dist/server/pages-page-response.d.ts +9 -0
  109. package/dist/server/pages-page-response.js +18 -4
  110. package/dist/server/pages-request-pipeline.js +6 -5
  111. package/dist/server/pages-request-stage-entry.js +25 -4
  112. package/dist/server/pages-router-entry.js +3 -2
  113. package/dist/server/prod-server.d.ts +2 -2
  114. package/dist/server/prod-server.js +59 -17
  115. package/dist/server/request-error-log.d.ts +9 -0
  116. package/dist/server/request-error-log.js +13 -0
  117. package/dist/server/request-pipeline.d.ts +14 -4
  118. package/dist/server/request-pipeline.js +33 -15
  119. package/dist/server/response-stage-cacheability.d.ts +7 -0
  120. package/dist/server/response-stage-cacheability.js +6 -5
  121. package/dist/server/rsc-completion-metadata.js +3 -0
  122. package/dist/server/static-file-cache.d.ts +9 -0
  123. package/dist/server/static-file-cache.js +13 -2
  124. package/dist/server/worker-image-optimization.d.ts +2 -2
  125. package/dist/server/worker-image-optimization.js +18 -3
  126. package/dist/server/worker-utils.d.ts +8 -0
  127. package/dist/server/worker-utils.js +19 -2
  128. package/dist/shims/cache-request-state.d.ts +6 -6
  129. package/dist/shims/cache-request-state.js +6 -36
  130. package/dist/shims/cache-runtime.d.ts +3 -1
  131. package/dist/shims/cache-runtime.js +77 -47
  132. package/dist/shims/cache.d.ts +10 -5
  133. package/dist/shims/cache.js +35 -22
  134. package/dist/shims/client-hook-error.d.ts +2 -0
  135. package/dist/shims/client-hook-error.js +16 -1
  136. package/dist/shims/fetch-cache.js +6 -2
  137. package/dist/shims/headers.d.ts +2 -1
  138. package/dist/shims/headers.js +5 -3
  139. package/dist/shims/image-props.d.ts +6 -0
  140. package/dist/shims/image-props.js +39 -3
  141. package/dist/shims/image.js +3 -3
  142. package/dist/shims/internal/cache-context-accessor.d.ts +1 -1
  143. package/dist/shims/link.js +5 -10
  144. package/dist/shims/navigation.js +2 -3
  145. package/dist/shims/request-state-types.d.ts +2 -1
  146. package/dist/shims/slot.d.ts +9 -2
  147. package/dist/shims/slot.js +76 -11
  148. package/dist/shims/unified-request-context.d.ts +6 -0
  149. package/dist/shims/unified-request-context.js +1 -0
  150. package/dist/utils/base-path.d.ts +7 -0
  151. package/dist/utils/base-path.js +11 -1
  152. package/dist/utils/cache-life-profiles.d.ts +33 -0
  153. package/dist/utils/cache-life-profiles.js +111 -0
  154. package/dist/utils/middleware-request-headers.d.ts +14 -3
  155. package/dist/utils/middleware-request-headers.js +54 -7
  156. package/package.json +1 -1
@@ -53,17 +53,26 @@ function toH3Pathname(pathname) {
53
53
  }
54
54
  }
55
55
  /**
56
+ * The key Nitro's static handler looks a pathname up under, following
57
+ * `runtime/internal/static.mjs`: without one trailing slash and decoded
58
+ * except for `%2F`.
59
+ */
60
+ function toAssetId(pathname) {
61
+ const id = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
62
+ try {
63
+ return decodeURIComponent(id.replace(/%2f/gi, "%252F"));
64
+ } catch {
65
+ return id;
66
+ }
67
+ }
68
+ /**
56
69
  * Whether Nitro's static handler would answer a request for a pathname with
57
- * a public file, following its lookup in `runtime/internal/static.mjs`. It
58
- * looks h3's pathname up without one trailing slash and decoded except for
59
- * `%2F`, and then as a directory with an `index.html`, each first with the
60
- * extension of a compressed copy for every encoding the request accepts.
70
+ * a public file. It looks h3's pathname up under its key, and then as a
71
+ * directory with an `index.html`, each first with the extension of a
72
+ * compressed copy for every encoding the request accepts.
61
73
  */
62
74
  function isPublicFilePathname(pathname, extensions, publicAssets) {
63
- let id = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
64
- try {
65
- id = decodeURIComponent(id.replace(/%2f/gi, "%252F"));
66
- } catch {}
75
+ const id = toAssetId(pathname);
67
76
  const index = id.endsWith("/") ? `${id}index.html` : `${id}/index.html`;
68
77
  return extensions.some((extension) => publicAssets.has(id + extension) || publicAssets.has(index + extension));
69
78
  }
@@ -86,6 +95,9 @@ function createNitroPublicFilesPlugin(options) {
86
95
  const token = crypto.randomUUID();
87
96
  Reflect.set(globalThis, Symbol.for(NITRO_ASSETS_KEY), {
88
97
  async fetch(request) {
98
+ const { pathname } = new URL(request.url);
99
+ const h3Pathname = toH3Pathname(pathname);
100
+ if (h3Pathname === void 0 || toAssetId(h3Pathname) !== toAssetId(pathname)) return new Response(null, { status: 404 });
89
101
  const headers = new Headers(request.headers);
90
102
  headers.set(ASSET_REQUEST_HEADER, token);
91
103
  return appFetch(new Request(request, { headers }));
@@ -59,8 +59,12 @@ export declare function encodeUrlParserIgnoredCharacters(pathname: string): stri
59
59
  * `/_next/data/<buildId>/en.json` → `/en`
60
60
  * `/_next/data/<wrong-id>/about.json` → null
61
61
  * `/_next/data/<buildId>/about` → null (missing .json suffix)
62
+ * `/_next/data/<buildId>/blog/...json` → null (strips to a dot segment)
63
+ *
64
+ * `decoded` marks a percent-decoded copy of a pathname whose raw form already
65
+ * parsed: a `%2e` left in it was `%252e` on the wire, an ordinary character.
62
66
  */
63
- export declare function parseNextDataPathname(pathname: string, buildId: string): NextDataMatch | null;
67
+ export declare function parseNextDataPathname(pathname: string, buildId: string, decoded?: boolean): NextDataMatch | null;
64
68
  /**
65
69
  * The page pathname a `_next/data` URL addresses, whatever its buildId. Next.js
66
70
  * compiles every middleware matcher with an optional `/_next/data/<buildId>`
@@ -1,7 +1,7 @@
1
1
  import { MIDDLEWARE_SKIP_HEADER } from "../utils/protocol-headers.js";
2
- import { addBasePathToPathname, hasBasePath, stripBasePath } from "../utils/base-path.js";
2
+ import { hasBasePath, restoreBasePath, stripBasePath } from "../utils/base-path.js";
3
3
  import { NEXTJS_DEPLOYMENT_ID_HEADER } from "./headers.js";
4
- import { cloneRequestWithUrl } from "./request-pipeline.js";
4
+ import { cloneRequestWithUrl, hasUrlParserDotSegment } from "./request-pipeline.js";
5
5
  //#region src/server/pages-data-route.ts
6
6
  /**
7
7
  * Helpers for the Pages Router `/_next/data/{buildId}/{...page}.json` endpoint.
@@ -62,13 +62,19 @@ function encodeUrlParserIgnoredCharacters(pathname) {
62
62
  * `/_next/data/<buildId>/en.json` → `/en`
63
63
  * `/_next/data/<wrong-id>/about.json` → null
64
64
  * `/_next/data/<buildId>/about` → null (missing .json suffix)
65
+ * `/_next/data/<buildId>/blog/...json` → null (strips to a dot segment)
66
+ *
67
+ * `decoded` marks a percent-decoded copy of a pathname whose raw form already
68
+ * parsed: a `%2e` left in it was `%252e` on the wire, an ordinary character.
65
69
  */
66
- function parseNextDataPathname(pathname, buildId) {
70
+ function parseNextDataPathname(pathname, buildId, decoded = false) {
67
71
  if (!buildId) return null;
68
72
  if (!isNextDataPathname(pathname)) return null;
69
73
  const expectedPrefix = `${NEXT_DATA_PREFIX}${buildId}/`;
70
74
  if (!pathname.startsWith(expectedPrefix)) return null;
71
- return pageFromNextDataRest(pathname.slice(expectedPrefix.length, -5));
75
+ const rest = pathname.slice(expectedPrefix.length, -5);
76
+ if (!decoded && hasUrlParserDotSegment(rest)) return null;
77
+ return pageFromNextDataRest(rest);
72
78
  }
73
79
  /**
74
80
  * The page pathname a `_next/data` URL addresses, whatever its buildId. Next.js
@@ -83,7 +89,9 @@ function nextDataMiddlewareMatchPathname(pathname) {
83
89
  if (!isNextDataPathname(pathname)) return null;
84
90
  const buildIdEnd = pathname.indexOf("/", 12);
85
91
  if (buildIdEnd <= 12) return null;
86
- return pageFromNextDataRest(pathname.slice(buildIdEnd + 1, -5))?.pagePathname ?? null;
92
+ const rest = pathname.slice(buildIdEnd + 1, -5);
93
+ if (hasUrlParserDotSegment(rest)) return null;
94
+ return pageFromNextDataRest(rest)?.pagePathname ?? null;
87
95
  }
88
96
  function pageFromNextDataRest(rest) {
89
97
  if (rest.length === 0) return null;
@@ -189,7 +197,7 @@ function normalizePagesDataRequest(request, buildId, basePath = "", trailingSlas
189
197
  };
190
198
  const pagePathname = normalizeNextDataPagePathname(dataMatch.pagePathname, trailingSlash);
191
199
  const normalizedUrl = new URL(reqUrl);
192
- normalizedUrl.pathname = hadBasePath ? addBasePathToPathname(pagePathname, basePath) : pagePathname;
200
+ normalizedUrl.pathname = hadBasePath ? restoreBasePath(pagePathname, basePath) : pagePathname;
193
201
  return {
194
202
  isDataReq: true,
195
203
  request: cloneRequestWithUrl(request, normalizedUrl.href),
@@ -7,7 +7,7 @@ export type PagesRequestQuery = Record<string, string | string[]>;
7
7
  export type PagesReqResRequest = Readable & {
8
8
  method: string;
9
9
  url: string;
10
- headers: Record<string, string>;
10
+ headers: Record<string, string | string[] | undefined>;
11
11
  query: PagesRequestQuery;
12
12
  body: unknown;
13
13
  cookies: Record<string, string>;
@@ -367,6 +367,8 @@ function isVinextStreamedApiResponse(response) {
367
367
  function createPagesReqRes(options) {
368
368
  const headersObj = {};
369
369
  for (const [key, value] of options.request.headers) headersObj[key.toLowerCase()] = value;
370
+ const setCookies = options.request.headers.getSetCookie();
371
+ if (setCookies.length > 1) headersObj["set-cookie"] = setCookies;
370
372
  const req = Object.assign(createRequestReadable(options.request), {
371
373
  method: options.request.method,
372
374
  url: options.url,
@@ -13,7 +13,7 @@ import { normalizeStaticPathname } from "../routing/route-pattern.js";
13
13
  import { buildCacheStateHeaders } from "./cache-headers.js";
14
14
  import { pagesPathCacheTag } from "./pages-cache-tag.js";
15
15
  import { isPageNotModified } from "./http-conditional.js";
16
- import { generatePagesETag, isPagesStreamingBot, rendersPagesNotModified } from "./pages-page-response.js";
16
+ import { applyPagesPrerenderRevalidateHeader, generatePagesETag, isPagesStreamingBot, rendersPagesNotModified } from "./pages-page-response.js";
17
17
  import { createPagesGetInitialPropsRouter, hasPagesGetInitialProps, isResponseSent, loadPagesGetInitialProps } from "./pages-get-initial-props.js";
18
18
  import { isSerializableProps } from "./pages-serializable-props.js";
19
19
  import { tracePagesData, tracePagesDocument } from "./pages-execution-tracing.js";
@@ -130,6 +130,7 @@ function applyPagesTerminalMissHeaders(response, revalidateSeconds, isrCachePath
130
130
  tags: [pagesPathCacheTag(isrCachePathname)]
131
131
  });
132
132
  for (const [name, value] of Object.entries(buildCacheStateHeaders("MISS"))) response.headers.set(name, value);
133
+ applyPagesPrerenderRevalidateHeader(response.headers, revalidateSeconds);
133
134
  return response;
134
135
  }
135
136
  function applyCachedPagesRepresentationHeaders(response, cacheState, entry, options) {
@@ -8,6 +8,7 @@ import { getRequestExecutionContext } from "../shims/request-context.js";
8
8
  import { extractLocaleFromUrl, 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
+ import { logRequestError } from "./request-error-log.js";
11
12
  import { getCdnCacheAdapter } from "../shims/cdn-cache.js";
12
13
  import { beginRouteCacheability, isRouteCacheabilityIdentityProbe, isRouteCacheabilityProbe, markRouteCacheabilityExplicitResponsePolicy, markRouteCacheabilityFinalResponseUncacheable, recordRouteCacheability } from "../shims/cacheability-classification.js";
13
14
  import { BROWSER_REVALIDATE_CACHE_CONTROL, NEVER_CACHE_CONTROL, STATIC_CACHE_CONTROL, applyCdnResponseHeaders, hasCdnResponsePolicy, hasExplicitNonCacheableResponsePolicy, isCdnResponsePolicyHeader, shouldUseNextDeployCacheControl } from "./cache-control.js";
@@ -22,7 +23,7 @@ import { buildPagesReadinessNextData } from "./pages-readiness.js";
22
23
  import { resolvePagesPageMethodResponse } from "./pages-page-method.js";
23
24
  import { setCacheStateHeaders } from "./cache-headers.js";
24
25
  import { pagesPathCacheTag } from "./pages-cache-tag.js";
25
- import { renderPagesPageResponse } from "./pages-page-response.js";
26
+ import { applyPagesPrerenderRevalidateHeader, renderPagesPageResponse } from "./pages-page-response.js";
26
27
  import { hasPagesGetInitialProps } from "./pages-get-initial-props.js";
27
28
  import { traceFindPageComponents, tracePagesDocumentStream } from "./pages-execution-tracing.js";
28
29
  import { mergePagesNotFoundSourceHeaders, resolvePagesPageData, resolvePagesStatusPageRenderPath, setPagesResponseHeader } from "./pages-page-data.js";
@@ -244,10 +245,12 @@ function createPagesPageHandler(opts) {
244
245
  hasRewrites
245
246
  });
246
247
  const isCacheabilityProbe = isRouteCacheabilityProbe();
247
- if (!isRouteMissErrorRender && options?.__forcedRoute === void 0) {
248
+ const isTopLevelPageRoute = !isRouteMissErrorRender && options?.__forcedRoute === void 0;
249
+ const isInitialPropsRender = isTopLevelPageRoute && !isStaticPropsRoute && pagesReadiness.gssp !== true && (pagesReadiness.gip === true || pagesReadiness.appGip === true);
250
+ if (isTopLevelPageRoute) {
248
251
  beginRouteCacheability("pages-page", route.pattern);
249
252
  if (isRouteCacheabilityIdentityProbe()) return new Response(null, { status: 204 });
250
- if (pagesReadiness.gssp !== true && pagesReadiness.gip !== true && !(pagesReadiness.appGip === true && !isStaticPropsRoute) && !isStaticPropsRoute) recordRouteCacheability({
253
+ if (pagesReadiness.gssp !== true && !isInitialPropsRender && !isStaticPropsRoute) recordRouteCacheability({
251
254
  cacheable: true,
252
255
  cacheControl: STATIC_CACHE_CONTROL
253
256
  });
@@ -282,7 +285,7 @@ function createPagesPageHandler(opts) {
282
285
  resolveSentResponse = resolve;
283
286
  });
284
287
  const reportSsrError = async (e) => {
285
- console.error("[vinext] SSR error:", e);
288
+ logRequestError("[vinext] SSR error:", e);
286
289
  await reportRequestError(e, {
287
290
  path: url,
288
291
  method: request.method,
@@ -435,6 +438,7 @@ function createPagesPageHandler(opts) {
435
438
  clientTraceMetadata: shouldEmitPagesClientTraceMetadata(pageModule, AppComponent) ? vinextConfig.clientTraceMetadata : void 0,
436
439
  documentReqRes: render.documentReqRes,
437
440
  gsspRes: render.gsspRes,
441
+ isInitialPropsRender,
438
442
  isrCacheKey: pageIsrCacheKey,
439
443
  isrCachePathname,
440
444
  expireSeconds: render.expireSeconds,
@@ -585,6 +589,7 @@ function createPagesPageHandler(opts) {
585
589
  notFoundResponse = stripPagesNotFoundFramingHeaders(notFoundResponse);
586
590
  if (isOnDemandRevalidate) notFoundResponse = withPagesCacheState(notFoundResponse, "REVALIDATED", isrCachePathname);
587
591
  else if (pageDataResult.cacheState) notFoundResponse = withPagesCacheState(notFoundResponse, pageDataResult.cacheState);
592
+ if (pageDataResult.revalidateSeconds !== void 0) applyPagesPrerenderRevalidateHeader(notFoundResponse.headers, pageDataResult.revalidateSeconds);
588
593
  return finalizePagesPreviewResponse(withBrowserPolicy(notFoundResponse), preview);
589
594
  }
590
595
  if (pageDataResult.kind === "response") {
@@ -614,6 +619,7 @@ function createPagesPageHandler(opts) {
614
619
  });
615
620
  if (isDataReq) {
616
621
  const headers = new Headers();
622
+ let resHasCacheControl = false;
617
623
  if (gsspRes && typeof gsspRes.getHeaders === "function") {
618
624
  const gsspHeaders = gsspRes.getHeaders();
619
625
  for (const k of Object.keys(gsspHeaders)) {
@@ -622,10 +628,15 @@ function createPagesPageHandler(opts) {
622
628
  if (k.toLowerCase() === "set-cookie" && Array.isArray(v)) for (const cookie of v) headers.append(k, String(cookie));
623
629
  else headers.set(k, Array.isArray(v) ? v.join(", ") : String(v));
624
630
  }
631
+ resHasCacheControl = headers.has("Cache-Control");
632
+ } else if (isInitialPropsRender && documentReqRes) {
633
+ const resHeaders = documentReqRes.res.getHeaders();
634
+ for (const [name, value] of Object.entries(resHeaders)) if (isCdnResponsePolicyHeader(name)) setPagesResponseHeader(headers, name, value);
635
+ resHasCacheControl = Boolean(resHeaders["cache-control"]);
625
636
  }
626
637
  if (hasCdnResponsePolicy(headers)) markRouteCacheabilityExplicitResponsePolicy();
627
- if (gsspRes) {
628
- if (!headers.has("Cache-Control")) headers.set("Cache-Control", NEVER_CACHE_CONTROL);
638
+ if (gsspRes || isInitialPropsRender) {
639
+ if (!resHasCacheControl) headers.set("Cache-Control", NEVER_CACHE_CONTROL);
629
640
  } else if (isStaticPropsRoute) {
630
641
  if (isrRevalidateSeconds !== null) applyCdnResponseHeaders(headers, {
631
642
  cacheControl: buildMissIsrCacheControl(isrRevalidateSeconds, vinextConfig.expireTime),
@@ -17,6 +17,13 @@ export declare function generatePagesETag(payload: string): string;
17
17
  * A page whose status carries no body has nothing to store.
18
18
  */
19
19
  export declare function rendersPagesNotModified(status: number): boolean;
20
+ /**
21
+ * Hand the revalidate getStaticProps returned to build/prerender.ts, which
22
+ * records it in vinext-prerender.json like Next.js's `initialRevalidateSeconds`.
23
+ * The CDN adapter may rewrite Cache-Control, so the build cannot read it there.
24
+ * Only build-time renders carry this header.
25
+ */
26
+ export declare function applyPagesPrerenderRevalidateHeader(headers: Headers, revalidateSeconds: number | false): void;
20
27
  type PagesFontPreload = {
21
28
  href: string;
22
29
  type: string;
@@ -79,6 +86,8 @@ type RenderPagesPageResponseOptions = {
79
86
  setDocumentInitialHead?: ((head: ReactNode[]) => void) | undefined;
80
87
  documentReqRes?: PagesDocumentReqRes | null;
81
88
  gsspRes: PagesGsspResponse | null;
89
+ /** A page or custom `_app` getInitialProps render; rendered per request like gSSP. */
90
+ isInitialPropsRender?: boolean;
82
91
  isrCacheKey: (router: string, pathname: string) => string;
83
92
  /** Filesystem-route identity used for ISR persistence and cache tags. */
84
93
  isrCachePathname?: string;
@@ -1,7 +1,8 @@
1
1
  import { isBotUserAgent } from "../utils/html-limited-bots.js";
2
- import { NEXTJS_CACHE_HEADER, VINEXT_REVALIDATED_CACHE_TAG_HEADER } from "./headers.js";
2
+ import { NEXTJS_CACHE_HEADER, VINEXT_PRERENDER_CACHE_LIFE_HEADER, VINEXT_REVALIDATED_CACHE_TAG_HEADER } from "./headers.js";
3
3
  import { getRequestExecutionContext } from "../shims/request-context.js";
4
4
  import { reportRequestError } from "./instrumentation.js";
5
+ import { logRequestError } from "./request-error-log.js";
5
6
  import { getCdnCacheAdapter } from "../shims/cdn-cache.js";
6
7
  import { markRouteCacheabilityExplicitResponsePolicy } from "../shims/cacheability-classification.js";
7
8
  import { BROWSER_REVALIDATE_CACHE_CONTROL, NEVER_CACHE_CONTROL, NO_STORE_CACHE_CONTROL, applyCdnResponseHeaders, hasCdnResponsePolicy, shouldUseNextDeployCacheControl } from "./cache-control.js";
@@ -17,6 +18,7 @@ import { spliceDocumentHeadClose, spliceDocumentHeadOpen, spliceDocumentScripts,
17
18
  import { getClientTraceMetadataHTML } from "./client-trace-metadata.js";
18
19
  import { setCacheStateHeaders } from "./cache-headers.js";
19
20
  import { pagesPathCacheTag } from "./pages-cache-tag.js";
21
+ import { applyPrerenderCacheLifeHeader } from "./prerender-cache-life-header.js";
20
22
  import { readStreamAsText } from "../utils/text-stream.js";
21
23
  import { loadUserDocumentInitialProps, runDocumentRenderPage } from "./pages-document-initial-props.js";
22
24
  import { callDocumentGetInitialProps } from "./document-initial-head.js";
@@ -42,6 +44,17 @@ function generatePagesETag(payload) {
42
44
  function rendersPagesNotModified(status) {
43
45
  return status === 204 || status === 205 || status === 304 || getCdnCacheAdapter().deferNotModifiedResponse?.() !== true;
44
46
  }
47
+ /**
48
+ * Hand the revalidate getStaticProps returned to build/prerender.ts, which
49
+ * records it in vinext-prerender.json like Next.js's `initialRevalidateSeconds`.
50
+ * The CDN adapter may rewrite Cache-Control, so the build cannot read it there.
51
+ * Only build-time renders carry this header.
52
+ */
53
+ function applyPagesPrerenderRevalidateHeader(headers, revalidateSeconds) {
54
+ if (typeof process === "undefined" || process.env?.VINEXT_PRERENDER !== "1") return;
55
+ headers.delete(VINEXT_PRERENDER_CACHE_LIFE_HEADER);
56
+ if (typeof revalidateSeconds === "number") applyPrerenderCacheLifeHeader(headers, { revalidate: revalidateSeconds });
57
+ }
45
58
  function buildPagesFontHeadHtml(fontLinks, fontPreloads, fontStyles, scriptNonce) {
46
59
  let html = "";
47
60
  const nonceAttr = createNonceAttribute(scriptNonce);
@@ -121,7 +134,7 @@ async function buildPagesCompositeStream(bodyStream, shellPrefix, shellSuffix) {
121
134
  } });
122
135
  }
123
136
  async function reportPagesIsrCacheWriteError(error, cacheKey, routePattern) {
124
- console.error(`[vinext] Pages ISR cache write failed for ${cacheKey}:`, error);
137
+ logRequestError(`[vinext] Pages ISR cache write failed for ${cacheKey}:`, error);
125
138
  try {
126
139
  await reportRequestError(error, {
127
140
  path: cacheKey,
@@ -284,12 +297,13 @@ async function renderPagesPageResponse(options) {
284
297
  cacheControl: buildMissIsrCacheControl(options.isrRevalidateSeconds, options.expireSeconds),
285
298
  tags: [pathTag]
286
299
  });
300
+ applyPagesPrerenderRevalidateHeader(responseHeaders, options.isrRevalidateSeconds);
287
301
  if (options.isOnDemandRevalidate) {
288
302
  responseHeaders.set(NEXTJS_CACHE_HEADER, "REVALIDATED");
289
303
  responseHeaders.set(VINEXT_REVALIDATED_CACHE_TAG_HEADER, encodeURIComponent(pathTag));
290
304
  } else setCacheStateHeaders(responseHeaders, "MISS");
291
305
  } else if (options.isStaticPropsRoute && shouldUseNextDeployCacheControl()) responseHeaders.set("Cache-Control", BROWSER_REVALIDATE_CACHE_CONTROL);
292
- else if (options.gsspRes && !userSetCacheControl) responseHeaders.set("Cache-Control", NEVER_CACHE_CONTROL);
306
+ else if ((options.gsspRes || options.isInitialPropsRender || options.isFallback) && !userSetCacheControl) responseHeaders.set("Cache-Control", NEVER_CACHE_CONTROL);
293
307
  if (options.fontLinkHeader) responseHeaders.set("Link", options.fontLinkHeader);
294
308
  if (options.userAgent && isPagesStreamingBot(options.userAgent)) {
295
309
  const fullHtml = await readStreamAsText(compositeStream);
@@ -314,4 +328,4 @@ async function renderPagesPageResponse(options) {
314
328
  }), { __vinextStreamedHtmlResponse: true });
315
329
  }
316
330
  //#endregion
317
- export { buildPagesNextDataScript, generatePagesETag, isPagesStreamingBot, renderPagesPageResponse, rendersPagesNotModified };
331
+ export { applyPagesPrerenderRevalidateHeader, buildPagesNextDataScript, generatePagesETag, isPagesStreamingBot, renderPagesPageResponse, rendersPagesNotModified };
@@ -1,7 +1,7 @@
1
1
  import { PRERENDER_REVALIDATE_HEADER } from "../utils/protocol-headers.js";
2
2
  import { requestContextFromRequest } from "../config/request-context.js";
3
3
  import { isExternalUrl } from "../utils/external-url.js";
4
- import { addBasePathToPathname } from "../utils/base-path.js";
4
+ import { restoreBasePath } from "../utils/base-path.js";
5
5
  import { VINEXT_PRERENDER_REWRITTEN_HEADER } from "./headers.js";
6
6
  import { methodNotAllowedResponse, notFoundStaticAssetResponse, sanitizeMethodNotAllowedHeaders } from "./http-error-responses.js";
7
7
  import { cloneRequestWithUrl, normalizeTrailingSlash } from "./request-pipeline.js";
@@ -14,10 +14,11 @@ import { buildMiddlewarePrefetchSkipResponse } from "./pages-data-route.js";
14
14
  import { markRouteCacheabilityDynamic } from "../shims/cacheability-classification.js";
15
15
  import { isOnDemandRevalidateRequest } from "./revalidation-request.js";
16
16
  import { mergeRewriteQuery } from "../utils/query.js";
17
+ import { isImageOptimizationPath } from "./image-optimization.js";
17
18
  import { setFrameworkRequestRoute } from "./request-tracing.js";
18
19
  import { withExternalRewriteResponseHeaders } from "./middleware-response-headers.js";
19
20
  import { applyConfigHeadersToHeaderRecord } from "./config-headers.js";
20
- import { finalizeMissingStaticAssetResponse, mergeHeaders } from "./worker-utils.js";
21
+ import { finalizeMissingStaticAssetResponse, mergeHeaders, mergeStaticFileHeaders } from "./worker-utils.js";
21
22
  //#region src/server/pages-request-pipeline.ts
22
23
  function ruleUsesUnkeyedRequestCondition(rule) {
23
24
  return [...rule.has ?? [], ...rule.missing ?? []].some((condition) => condition.type === "header" || condition.type === "cookie" || condition.type === "host");
@@ -80,7 +81,7 @@ function wrapMiddlewareWithBasePath(runMiddleware, basePath, hadBasePath) {
80
81
  if (!hadBasePath || !basePath) return runMiddleware;
81
82
  return (request, ctx, opts) => {
82
83
  const mwUrl = new URL(request.url);
83
- mwUrl.pathname = addBasePathToPathname(mwUrl.pathname, basePath);
84
+ mwUrl.pathname = restoreBasePath(mwUrl.pathname, basePath);
84
85
  return runMiddleware(cloneRequestWithUrl(request, mwUrl.toString()), ctx, opts);
85
86
  };
86
87
  }
@@ -166,7 +167,7 @@ async function runPagesRequest(request, deps) {
166
167
  const served = await deps.serveFilesystemRoute(requestPathname, middlewareHeaders, phase, resolvedUrl, request, resolvedPathnameHasBasePath);
167
168
  if (served instanceof Response) {
168
169
  const isStaticMethodNotAllowed = served.status === 405 && served.headers.get("allow") === "GET, HEAD";
169
- const response = mergeHeaders(served, middlewareHeaders, isStaticMethodNotAllowed || !served.ok || served.status === 206 ? void 0 : middlewareStatus);
170
+ const response = (resolvedPathnameHasBasePath && isImageOptimizationPath(requestPathname) ? mergeHeaders : mergeStaticFileHeaders)(served, middlewareHeaders, isStaticMethodNotAllowed || !served.ok || served.status === 206 ? void 0 : middlewareStatus);
170
171
  if (isStaticMethodNotAllowed) sanitizeMethodNotAllowedHeaders(response.headers, "GET, HEAD");
171
172
  return {
172
173
  type: "response",
@@ -306,7 +307,7 @@ async function runPagesRequest(request, deps) {
306
307
  let apiRequest = request;
307
308
  if (basePath && hadBasePath) {
308
309
  const apiRequestUrl = new URL(request.url);
309
- apiRequestUrl.pathname = addBasePathToPathname(apiRequestUrl.pathname, basePath);
310
+ apiRequestUrl.pathname = restoreBasePath(apiRequestUrl.pathname, basePath);
310
311
  apiRequest = cloneRequestWithUrl(request, apiRequestUrl.toString());
311
312
  }
312
313
  const response = await deps.handleApi(apiRequest, apiLookupUrl, deps.ctx ?? null, headersFromRecord(middlewareHeaders));
@@ -1,9 +1,10 @@
1
1
  import { VINEXT_PRERENDER_SECRET_HEADER, VINEXT_REVALIDATE_HOST_HEADER } from "../utils/protocol-headers.js";
2
+ import { applyMiddlewareResponseHeadersToRequest } from "../utils/middleware-request-headers.js";
2
3
  import { requestContextFromRequest } from "../config/request-context.js";
3
4
  import { hasBasePath, stripBasePath } from "../utils/base-path.js";
4
5
  import { VINEXT_CACHEABILITY_PROBE_QUERY_PARAM, VINEXT_EXPECTED_WORKER_VERSION_HEADER } from "./headers.js";
5
6
  import { notFoundStaticAssetResponse } from "./http-error-responses.js";
6
- import { attachRequestCfMetadata, cloneRequestWithUrl, filterInternalHeaders, guardProtocolRelativeUrl, haveSameHeaders, withRequestHeaders } from "./request-pipeline.js";
7
+ import { attachRequestCfMetadata, cloneRequestWithUrl, filterInternalHeaders, guardProtocolRelativeUrl, haveSameHeaders, stripRequestUrlFragment, withRequestHeaders } from "./request-pipeline.js";
7
8
  import { assetPrefixPathname, isNextStaticPath } from "../utils/asset-prefix.js";
8
9
  import { normalizePathnameForRouteMatchStrict } from "../routing/utils.js";
9
10
  import { runWithExecutionContext } from "../shims/request-context.js";
@@ -35,6 +36,15 @@ import * as pagesEntry from "virtual:vinext-pages-request-entry";
35
36
  * The public pages-router-entry delegates here. Multi-stage hosts import the
36
37
  * named request handler directly and provide their response-stage dispatcher.
37
38
  */
39
+ /**
40
+ * Whether middleware's request-header overrides changed the request. Next.js
41
+ * also copies ordinary middleware response headers onto the request; like a
42
+ * client's own headers, those do not opt the render out of the shared stage.
43
+ */
44
+ function hasRequestHeaderOverrides(originalHeaders, requestHeaders, middlewareResponseHeaders) {
45
+ const expectedHeaders = middlewareResponseHeaders ? applyMiddlewareResponseHeadersToRequest(originalHeaders, middlewareResponseHeaders) : originalHeaders;
46
+ return !haveSameHeaders(expectedHeaders, requestHeaders);
47
+ }
38
48
  function captureOuterResponsePolicyHeaders(stagedHeaders, transportedPolicyHeaders) {
39
49
  return captureCdnResponsePolicyOverrides(stagedHeaders, new Headers(transportedPolicyHeaders ?? []));
40
50
  }
@@ -59,6 +69,7 @@ const configRewrites = vinextConfig?.rewrites ?? {
59
69
  const configHeaders = vinextConfig?.headers ?? [];
60
70
  const imageConfig = vinextConfig?.images ? {
61
71
  qualities: vinextConfig.images.qualities,
72
+ localPatterns: vinextConfig.images.localPatterns,
62
73
  dangerouslyAllowSVG: vinextConfig.images.dangerouslyAllowSVG,
63
74
  dangerouslyAllowLocalIP: vinextConfig.images.dangerouslyAllowLocalIP,
64
75
  contentDispositionType: vinextConfig.images.contentDispositionType,
@@ -86,6 +97,7 @@ function handleRequestStageLocally(request, env, ctx, dispatchResponseStage) {
86
97
  return handleRequest(request, env, ctx, (stageRequest, props, _options, stageEnv, stageCtx) => dispatchResponseStage(stageRequest, stageEnv, stageCtx, props), true, "worker", ctx?.assets).then((response) => applyCdnResponseIdentityHeaders(response, originalRequest));
87
98
  }
88
99
  function handleRequest(request, env, platformCtx, dispatchResponseStage, forceCacheBypass, defaultHostRuntime, assets) {
100
+ request = stripRequestUrlFragment(request);
89
101
  const url = new URL(request.url);
90
102
  const trace = async () => {
91
103
  await pagesEntry.__ensureInstrumentation?.(platformCtx);
@@ -208,6 +220,7 @@ async function handleRequestImpl(request, env, platformCtx, dispatchResponseStag
208
220
  requestContext: requestContextFromRequest(request)
209
221
  });
210
222
  let speculativeRenderRequest = null;
223
+ let middlewareResponseHeaders;
211
224
  const deps = {
212
225
  assetPrefix: vinextConfig?.assetPrefix,
213
226
  basePath,
@@ -229,7 +242,15 @@ async function handleRequestImpl(request, env, platformCtx, dispatchResponseStag
229
242
  authorizeOnDemandRevalidate: typeof authorizeOnDemandRevalidate === "function" ? authorizeOnDemandRevalidate : void 0,
230
243
  matchApiRoute: typeof matchApiRoute === "function" ? matchApiRoute : null,
231
244
  matchPageRoute: typeof matchPageRoute === "function" ? matchPageRoute : null,
232
- runMiddleware: typeof runMiddleware === "function" ? wrapMiddlewareWithBasePath(runMiddleware, basePath, hadBasePath) : null,
245
+ runMiddleware: typeof runMiddleware === "function" ? async (middlewareInput, middlewareCtx, middlewareOptions) => {
246
+ const result = await wrapMiddlewareWithBasePath(runMiddleware, basePath, hadBasePath)(middlewareInput, middlewareCtx, middlewareOptions);
247
+ middlewareResponseHeaders = void 0;
248
+ if (result.responseHeaders) {
249
+ middlewareResponseHeaders = new Headers();
250
+ for (const [key, value] of result.responseHeaders) middlewareResponseHeaders.append(key, value);
251
+ }
252
+ return result;
253
+ } : null,
233
254
  renderPage: (req, resolvedUrl, options, stagedHeaders) => {
234
255
  if (options?.renderErrorPageOnMiss === false && req.body !== null && !req.bodyUsed) speculativeRenderRequest = req;
235
256
  const stageRequest = speculativeRenderRequest === req && req.body !== null && !req.bodyUsed ? attachRequestCfMetadata(req.clone(), req) : req;
@@ -238,7 +259,7 @@ async function handleRequestImpl(request, env, platformCtx, dispatchResponseStag
238
259
  authorizeOnDemandRevalidate: typeof authorizeOnDemandRevalidate === "function" ? authorizeOnDemandRevalidate : void 0,
239
260
  hasRequestAwareDocument: hasRequestAwareDocument === true,
240
261
  request: req,
241
- requestHeadersChanged: !haveSameHeaders(filteredHeaders, req.headers),
262
+ requestHeadersChanged: hasRequestHeaderOverrides(filteredHeaders, req.headers, middlewareResponseHeaders),
242
263
  routeDataKind: matchedPage?.route.dataKind,
243
264
  stagedHeaders
244
265
  });
@@ -306,7 +327,7 @@ async function handleRequestImpl(request, env, platformCtx, dispatchResponseStag
306
327
  const cache = forceCacheBypass || probeMode ? "bypass" : getPagesResponseStageCacheDisposition({
307
328
  authorizeOnDemandRevalidate: typeof authorizeOnDemandRevalidate === "function" ? authorizeOnDemandRevalidate : void 0,
308
329
  request: req,
309
- requestHeadersChanged: !haveSameHeaders(filteredHeaders, req.headers),
330
+ requestHeadersChanged: hasRequestHeaderOverrides(filteredHeaders, req.headers, middlewareResponseHeaders),
310
331
  stagedHeaders
311
332
  });
312
333
  const dispatched = trackedDispatchResponseStage(req, responseStageProps(), { cache }, env, ctx);
@@ -1,5 +1,5 @@
1
1
  import { VINEXT_CACHEABILITY_PROBE_HEADER, VINEXT_CACHEABILITY_PROBE_QUERY_PARAM } from "./headers.js";
2
- import { cloneRequestWithHeaders, cloneRequestWithUrl } from "./request-pipeline.js";
2
+ import { cloneRequestWithHeaders, cloneRequestWithUrl, stripRequestUrlFragment } from "./request-pipeline.js";
3
3
  import { runWithExecutionContext } from "../shims/request-context.js";
4
4
  import { getCdnCacheAdapter } from "../shims/cdn-cache.js";
5
5
  import { validateCdnRequest } from "./cache-control.js";
@@ -52,7 +52,8 @@ async function handleSingleStageRequest(request, env, platformCtx) {
52
52
  return finalize ? finalize(response, ctx) : response;
53
53
  }
54
54
  var pages_router_entry_default = { fetch(request, env, ctx) {
55
- const handle = () => handleSingleStageRequest(request, env, ctx);
55
+ const strippedRequest = stripRequestUrlFragment(request);
56
+ const handle = () => handleSingleStageRequest(strippedRequest, env, ctx);
56
57
  return ctx && typeof ctx.waitUntil === "function" ? runWithExecutionContext(ctx, handle) : handle();
57
58
  } };
58
59
  //#endregion
@@ -102,11 +102,11 @@ declare function sendCompressed(req: IncomingMessage, res: ServerResponse, body:
102
102
  * Without a cache, falls back to async filesystem probing (still non-blocking,
103
103
  * unlike the old sync existsSync/statSync approach).
104
104
  *
105
- * `extraHeaders` go on every response for the file. `representationHeaders` go
105
+ * `stagedHeaders` go on every response for the file. `representationHeaders` go
106
106
  * only on responses that carry the file's bytes (200 and 206), never on a 304,
107
107
  * 412, 416 or 405.
108
108
  */
109
- declare function tryServeStatic(req: IncomingMessage, res: ServerResponse, clientDir: string, pathname: string, compress: boolean, cache?: StaticFileCache, extraHeaders?: Record<string, string | string[]>, statusCode?: number, representationHeaders?: Record<string, string>): Promise<boolean>;
109
+ declare function tryServeStatic(req: IncomingMessage, res: ServerResponse, clientDir: string, pathname: string, compress: boolean, cache?: StaticFileCache, stagedHeaders?: Record<string, string | string[]>, statusCode?: number, representationHeaders?: Record<string, string>): Promise<boolean>;
110
110
  /**
111
111
  * Convert a Node.js IncomingMessage to a Web Request object.
112
112
  *