vinext 1.0.0-beta.2 → 1.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +6 -9
  2. package/dist/build/client-build-config.js +7 -1
  3. package/dist/build/precompress.js +1 -1
  4. package/dist/build/prerender.d.ts +2 -1
  5. package/dist/build/prerender.js +12 -2
  6. package/dist/check.js +4 -0
  7. package/dist/cli-args.d.ts +2 -1
  8. package/dist/cli-args.js +10 -0
  9. package/dist/cli.js +14 -8
  10. package/dist/client/navigation-runtime.d.ts +6 -0
  11. package/dist/client/navigation-runtime.js +1 -1
  12. package/dist/config/config-matchers.js +4 -1
  13. package/dist/config/dotenv.d.ts +3 -4
  14. package/dist/config/dotenv.js +30 -22
  15. package/dist/config/next-config.d.ts +16 -5
  16. package/dist/config/next-config.js +7 -0
  17. package/dist/entries/app-browser-entry.d.ts +4 -10
  18. package/dist/entries/app-browser-entry.js +20 -4
  19. package/dist/entries/app-rsc-entry.js +21 -3
  20. package/dist/entries/app-rsc-manifest.js +19 -0
  21. package/dist/entries/pages-client-entry.js +0 -1
  22. package/dist/entries/pages-server-entry.js +13 -3
  23. package/dist/index.js +122 -49
  24. package/dist/init-cloudflare.d.ts +3 -1
  25. package/dist/init-cloudflare.js +59 -5
  26. package/dist/plugins/client-reference-dedup.js +7 -5
  27. package/dist/plugins/extensionless-dynamic-import.js +23 -18
  28. package/dist/plugins/ignore-dynamic-requests.js +3 -1
  29. package/dist/plugins/import-meta-url.js +17 -6
  30. package/dist/plugins/require-context.js +20 -15
  31. package/dist/plugins/sass.js +3 -4
  32. package/dist/plugins/transform-cache.d.ts +19 -0
  33. package/dist/plugins/transform-cache.js +36 -0
  34. package/dist/routing/app-route-graph.d.ts +14 -6
  35. package/dist/routing/app-route-graph.js +106 -16
  36. package/dist/server/api-handler.d.ts +2 -0
  37. package/dist/server/api-handler.js +7 -6
  38. package/dist/server/app-bfcache-identity.js +28 -2
  39. package/dist/server/app-browser-entry.js +175 -75
  40. package/dist/server/app-browser-navigation-controller.d.ts +23 -21
  41. package/dist/server/app-browser-navigation-controller.js +30 -5
  42. package/dist/server/app-browser-prefetch-response.d.ts +9 -0
  43. package/dist/server/app-browser-prefetch-response.js +32 -0
  44. package/dist/server/app-browser-state.d.ts +8 -3
  45. package/dist/server/app-browser-state.js +9 -3
  46. package/dist/server/app-page-cache-finalizer.js +1 -1
  47. package/dist/server/app-page-cache.d.ts +1 -1
  48. package/dist/server/app-page-cache.js +16 -2
  49. package/dist/server/app-page-dispatch.d.ts +9 -1
  50. package/dist/server/app-page-dispatch.js +37 -8
  51. package/dist/server/app-page-element-builder.d.ts +4 -1
  52. package/dist/server/app-page-element-builder.js +34 -7
  53. package/dist/server/app-page-head.d.ts +3 -3
  54. package/dist/server/app-page-head.js +58 -13
  55. package/dist/server/app-page-http-access-fallback-metadata.d.ts +2 -2
  56. package/dist/server/app-page-http-access-fallback-metadata.js +9 -1
  57. package/dist/server/app-page-probe.d.ts +4 -0
  58. package/dist/server/app-page-probe.js +6 -2
  59. package/dist/server/app-page-render.d.ts +1 -1
  60. package/dist/server/app-page-render.js +13 -3
  61. package/dist/server/app-page-request.d.ts +3 -1
  62. package/dist/server/app-page-request.js +4 -13
  63. package/dist/server/app-page-response.d.ts +2 -0
  64. package/dist/server/app-page-response.js +7 -2
  65. package/dist/server/app-page-route-wiring.d.ts +10 -2
  66. package/dist/server/app-page-route-wiring.js +269 -59
  67. package/dist/server/app-pages-bridge.d.ts +2 -1
  68. package/dist/server/app-pages-bridge.js +3 -1
  69. package/dist/server/app-route-handler-cache.js +4 -0
  70. package/dist/server/app-route-handler-dispatch.js +9 -3
  71. package/dist/server/app-route-handler-response.js +1 -1
  72. package/dist/server/app-route-module-loader.d.ts +6 -0
  73. package/dist/server/app-route-module-loader.js +3 -0
  74. package/dist/server/app-router-entry.js +6 -3
  75. package/dist/server/app-rsc-cache-busting.d.ts +7 -0
  76. package/dist/server/app-rsc-cache-busting.js +6 -0
  77. package/dist/server/app-rsc-errors.js +10 -0
  78. package/dist/server/app-rsc-handler.d.ts +2 -0
  79. package/dist/server/app-rsc-handler.js +30 -13
  80. package/dist/server/app-rsc-render-mode.d.ts +3 -2
  81. package/dist/server/app-rsc-render-mode.js +4 -1
  82. package/dist/server/app-rsc-request-normalization.js +65 -4
  83. package/dist/server/app-rsc-route-matching.d.ts +9 -0
  84. package/dist/server/app-rsc-route-matching.js +6 -0
  85. package/dist/server/app-server-action-execution.d.ts +1 -1
  86. package/dist/server/app-ssr-entry.js +4 -4
  87. package/dist/server/app-visited-response-cache.d.ts +6 -1
  88. package/dist/server/app-visited-response-cache.js +49 -1
  89. package/dist/server/cache-control.d.ts +2 -2
  90. package/dist/server/cache-control.js +2 -1
  91. package/dist/server/dev-server.d.ts +3 -1
  92. package/dist/server/dev-server.js +204 -348
  93. package/dist/server/edge-api-runtime.d.ts +10 -1
  94. package/dist/server/edge-api-runtime.js +29 -1
  95. package/dist/server/headers.d.ts +4 -2
  96. package/dist/server/headers.js +6 -3
  97. package/dist/server/isr-cache.d.ts +10 -33
  98. package/dist/server/isr-cache.js +33 -43
  99. package/dist/server/isr-decision.d.ts +2 -2
  100. package/dist/server/pages-api-route.d.ts +4 -0
  101. package/dist/server/pages-api-route.js +6 -3
  102. package/dist/server/pages-asset-tags.d.ts +1 -0
  103. package/dist/server/pages-asset-tags.js +6 -4
  104. package/dist/server/pages-dev-hydration.js +0 -1
  105. package/dist/server/pages-document-asset-props.d.ts +26 -0
  106. package/dist/server/pages-document-asset-props.js +69 -0
  107. package/dist/server/pages-get-initial-props.d.ts +1 -1
  108. package/dist/server/pages-get-initial-props.js +1 -4
  109. package/dist/server/pages-i18n.js +64 -16
  110. package/dist/server/pages-node-compat.d.ts +2 -0
  111. package/dist/server/pages-node-compat.js +10 -6
  112. package/dist/server/pages-page-data.d.ts +61 -9
  113. package/dist/server/pages-page-data.js +334 -74
  114. package/dist/server/pages-page-handler.d.ts +6 -1
  115. package/dist/server/pages-page-handler.js +119 -20
  116. package/dist/server/pages-page-response.d.ts +7 -3
  117. package/dist/server/pages-page-response.js +41 -33
  118. package/dist/server/pages-request-pipeline.d.ts +7 -0
  119. package/dist/server/pages-request-pipeline.js +5 -1
  120. package/dist/server/pages-revalidate.d.ts +1 -1
  121. package/dist/server/pages-revalidate.js +54 -8
  122. package/dist/server/pages-router-entry.d.ts +1 -0
  123. package/dist/server/pages-router-entry.js +10 -4
  124. package/dist/server/prerender-manifest.d.ts +1 -0
  125. package/dist/server/prod-server.d.ts +2 -1
  126. package/dist/server/prod-server.js +38 -13
  127. package/dist/server/revalidation-host.d.ts +11 -0
  128. package/dist/server/revalidation-host.js +19 -0
  129. package/dist/server/seed-cache.js +1 -1
  130. package/dist/server/worker-revalidation-context.d.ts +13 -0
  131. package/dist/server/worker-revalidation-context.js +30 -0
  132. package/dist/shims/app-router-scroll-state.d.ts +2 -1
  133. package/dist/shims/app-router-scroll-state.js +7 -2
  134. package/dist/shims/cache-handler.d.ts +1 -1
  135. package/dist/shims/cache-handler.js +10 -7
  136. package/dist/shims/cache-runtime.d.ts +6 -0
  137. package/dist/shims/cache-runtime.js +4 -1
  138. package/dist/shims/cache.js +1 -1
  139. package/dist/shims/document.js +15 -2
  140. package/dist/shims/error.js +1 -1
  141. package/dist/shims/fetch-cache.js +1 -1
  142. package/dist/shims/font-google-base.js +17 -8
  143. package/dist/shims/font-local.js +41 -10
  144. package/dist/shims/hash-scroll.d.ts +1 -1
  145. package/dist/shims/hash-scroll.js +2 -1
  146. package/dist/shims/headers.d.ts +10 -13
  147. package/dist/shims/headers.js +85 -36
  148. package/dist/shims/internal/cookie-serialize.d.ts +10 -9
  149. package/dist/shims/internal/cookie-serialize.js +9 -8
  150. package/dist/shims/internal/interpolate-as.d.ts +15 -1
  151. package/dist/shims/internal/interpolate-as.js +27 -2
  152. package/dist/shims/link.d.ts +2 -0
  153. package/dist/shims/link.js +77 -24
  154. package/dist/shims/metadata.d.ts +18 -34
  155. package/dist/shims/metadata.js +98 -56
  156. package/dist/shims/navigation.d.ts +18 -2
  157. package/dist/shims/navigation.js +70 -21
  158. package/dist/shims/request-context.d.ts +6 -2
  159. package/dist/shims/router.js +18 -7
  160. package/dist/shims/script.js +1 -1
  161. package/dist/shims/server.d.ts +7 -6
  162. package/dist/shims/server.js +156 -57
  163. package/dist/shims/slot.d.ts +2 -1
  164. package/dist/shims/slot.js +6 -2
  165. package/dist/shims/streamed-icons.d.ts +9 -0
  166. package/dist/shims/streamed-icons.js +33 -0
  167. package/dist/shims/unified-request-context.d.ts +21 -2
  168. package/dist/shims/unified-request-context.js +82 -1
  169. package/dist/utils/cache-control-metadata.d.ts +2 -1
  170. package/dist/utils/cache-control-metadata.js +5 -1
  171. package/dist/utils/html-limited-bots.js +1 -1
  172. package/dist/utils/protocol-headers.d.ts +7 -1
  173. package/dist/utils/protocol-headers.js +7 -1
  174. package/package.json +1 -1
@@ -1,19 +1,21 @@
1
- import { createRequestContext, runWithRequestContext } from "../shims/unified-request-context.js";
1
+ import { closeAfterResponse, closeAfterResponseWithBody, createRequestContext, runWithRequestContext } from "../shims/unified-request-context.js";
2
2
  import { patternToNextFormat } from "../routing/route-validation.js";
3
3
  import { getRequestExecutionContext } from "../shims/request-context.js";
4
- import { NEXTJS_DEPLOYMENT_ID_HEADER } from "./headers.js";
4
+ import { PRERENDER_REVALIDATE_HEADER } from "../utils/protocol-headers.js";
5
+ import { NEXTJS_CACHE_HEADER, NEXTJS_DEPLOYMENT_ID_HEADER, VINEXT_CACHE_HEADER } from "./headers.js";
5
6
  import { reportRequestError } from "./instrumentation.js";
6
7
  import { BROWSER_REVALIDATE_CACHE_CONTROL, NEVER_CACHE_CONTROL, applyCdnResponseHeaders, shouldUseNextDeployCacheControl } from "./cache-control.js";
7
8
  import { buildMissIsrCacheControl } from "./isr-decision.js";
8
- import { PRERENDER_REVALIDATE_HEADER, isOnDemandRevalidateRequest, isrCacheKey, isrGet, isrSet, triggerBackgroundRegeneration } from "./isr-cache.js";
9
+ import { coalesceOnDemandRevalidation, isOnDemandRevalidateRequest, isrCacheKey, isrGet, isrSet, triggerBackgroundRegeneration } from "./isr-cache.js";
10
+ import { encodeCacheTag } from "../utils/encode-cache-tag.js";
9
11
  import { ensureFetchPatch } from "../shims/fetch-cache.js";
10
- import { appendAssetDeploymentIdQuery } from "../utils/deployment-id.js";
11
12
  import { mergeRouteParamsIntoQuery, parseQueryString } from "../utils/query.js";
12
13
  import { getScriptNonceFromHeaderSources } from "./csp.js";
13
14
  import { extractLocaleFromUrl, resolvePagesI18nRequest } from "./pages-i18n.js";
14
15
  import { buildDefaultPagesNotFoundResponse } from "./pages-default-404.js";
15
16
  import { buildPagesReadinessNextData } from "./pages-readiness.js";
16
17
  import { resolvePagesPageMethodResponse } from "./pages-page-method.js";
18
+ import { setCacheStateHeaders } from "./cache-headers.js";
17
19
  import { renderPagesPageResponse } from "./pages-page-response.js";
18
20
  import { hasPagesGetInitialProps } from "./pages-get-initial-props.js";
19
21
  import { buildNextDataNotFoundResponse, buildNextDataPropsJsonResponse, normalizePagesDataRequest, parseNextDataPathname } from "./pages-data-route.js";
@@ -25,7 +27,12 @@ import { createPagesReqRes } from "./pages-node-compat.js";
25
27
  function finalizePagesPreviewResponse(response, preview) {
26
28
  if (preview.data === false && !preview.shouldClear) return response;
27
29
  const headers = new Headers(response.headers);
28
- if (preview.data !== false) headers.set("Cache-Control", PAGES_PREVIEW_CACHE_CONTROL);
30
+ if (preview.data !== false) {
31
+ headers.set("Cache-Control", PAGES_PREVIEW_CACHE_CONTROL);
32
+ headers.delete("CDN-Cache-Control");
33
+ headers.delete("Cloudflare-CDN-Cache-Control");
34
+ headers.delete("Cache-Tag");
35
+ }
29
36
  if (preview.shouldClear) appendPagesPreviewClearCookies(headers);
30
37
  return new Response(response.body, {
31
38
  headers,
@@ -33,6 +40,41 @@ function finalizePagesPreviewResponse(response, preview) {
33
40
  statusText: response.statusText
34
41
  });
35
42
  }
43
+ function withPagesCacheState(response, state) {
44
+ const headers = new Headers(response.headers);
45
+ if (state === "REVALIDATED") {
46
+ headers.set(NEXTJS_CACHE_HEADER, state);
47
+ headers.delete(VINEXT_CACHE_HEADER);
48
+ } else setCacheStateHeaders(headers, state);
49
+ return new Response(response.body, {
50
+ headers,
51
+ status: response.status,
52
+ statusText: response.statusText
53
+ });
54
+ }
55
+ function applyPagesErrorCachePolicy(response, revalidateSeconds, expireSeconds, cacheTagPathname) {
56
+ const headers = new Headers(response.headers);
57
+ const browserPolicy = headers.get("Cache-Control");
58
+ const sharedPolicies = [headers.get("CDN-Cache-Control"), headers.get("Cloudflare-CDN-Cache-Control")];
59
+ const hasCacheableSharedPolicy = sharedPolicies.some((value) => value && /(?:^|,)\s*s-maxage\s*=/i.test(value));
60
+ if (sharedPolicies.some((value) => value && /(?:private|no-store|no-cache)/i.test(value)) || !hasCacheableSharedPolicy && browserPolicy && /(?:private|no-store|no-cache)/i.test(browserPolicy)) return response;
61
+ headers.delete("CDN-Cache-Control");
62
+ headers.delete("Cloudflare-CDN-Cache-Control");
63
+ headers.delete("Cache-Tag");
64
+ if (revalidateSeconds === void 0) applyCdnResponseHeaders(headers, { cacheControl: NEVER_CACHE_CONTROL });
65
+ else {
66
+ const stem = cacheTagPathname.endsWith("/") ? cacheTagPathname.slice(0, -1) : cacheTagPathname;
67
+ applyCdnResponseHeaders(headers, {
68
+ cacheControl: buildMissIsrCacheControl(revalidateSeconds, expireSeconds),
69
+ tags: [encodeCacheTag(`_N_T_${stem || "/"}`)]
70
+ });
71
+ }
72
+ return new Response(response.body, {
73
+ headers,
74
+ status: response.status,
75
+ statusText: response.statusText
76
+ });
77
+ }
36
78
  function shouldEmitPagesClientTraceMetadata(pageModule, appComponent) {
37
79
  if (typeof pageModule.getServerSideProps === "function") return true;
38
80
  if (typeof pageModule.getStaticProps === "function") return false;
@@ -122,6 +164,7 @@ function createPagesPageHandler(opts) {
122
164
  route: options.__forcedRoute,
123
165
  params: {}
124
166
  } : matchRoute(routeUrl, pageRoutes);
167
+ let isRouteMissErrorRender = false;
125
168
  let renderStatusCodeOverride = statusCode;
126
169
  let renderAsPath = asPath;
127
170
  if (!match) {
@@ -133,6 +176,7 @@ function createPagesPageHandler(opts) {
133
176
  route: notFoundRoute,
134
177
  params: {}
135
178
  };
179
+ isRouteMissErrorRender = true;
136
180
  renderStatusCodeOverride = 404;
137
181
  renderAsPath = routeUrl;
138
182
  } else return buildDefaultPagesNotFoundResponse();
@@ -141,16 +185,43 @@ function createPagesPageHandler(opts) {
141
185
  const pageModule = route.module;
142
186
  const isStaticPropsRoute = typeof pageModule.getStaticProps === "function";
143
187
  const isStaticPropsRender = isStaticPropsRoute && typeof pageModule.getServerSideProps !== "function";
188
+ if (!options?.__skipOnDemandCoalesce && !options?.__forcedRoute && isStaticPropsRoute && isOnDemandRevalidateRequest(request.headers.get("x-prerender-revalidate"))) {
189
+ const snapshot = await coalesceOnDemandRevalidation(pageIsrCacheKey("pages", routeUrl.split("?")[0]), async () => {
190
+ const response = await renderPage(request, url, manifest, middlewareHeaders, {
191
+ ...options,
192
+ __skipOnDemandCoalesce: true
193
+ });
194
+ return {
195
+ body: request.method === "HEAD" || response.status === 204 || response.status === 304 ? null : new Uint8Array(await response.arrayBuffer()),
196
+ headers: [...response.headers.entries()],
197
+ status: response.status,
198
+ statusText: response.statusText
199
+ };
200
+ });
201
+ return new Response(snapshot.body?.slice() ?? null, {
202
+ headers: snapshot.headers,
203
+ status: snapshot.status,
204
+ statusText: snapshot.statusText
205
+ });
206
+ }
144
207
  const renderRouteUrl = isStaticPropsRender ? routeUrl.split("?")[0] : routeUrl;
145
208
  const routerAsPathSource = isStaticPropsRender ? renderRouteUrl : renderAsPath ?? renderRouteUrl;
146
209
  const routerAsPath = i18nConfig ? extractLocaleFromUrl(routerAsPathSource, i18nConfig, locale).url : routerAsPathSource;
147
- return runWithRequestContext(createRequestContext({ executionContext: getRequestExecutionContext() }), async () => {
210
+ const uCtx = createRequestContext({ executionContext: getRequestExecutionContext() });
211
+ return closeAfterResponseWithBody(await runWithRequestContext(uCtx, async () => {
148
212
  ensureFetchPatch();
149
213
  try {
150
214
  const routePattern = patternToNextFormat(route.pattern);
151
215
  const renderStatusCode = renderStatusCodeOverride ?? (routePattern === "/404" ? 404 : void 0);
216
+ const isrCachePathname = isStaticPropsRender && (routePattern === "/404" || routePattern === "/500" || routePattern === "/_error") ? routePattern : renderRouteUrl.split("?")[0];
217
+ const isNotFoundErrorRender = routePattern === "/404" || routePattern === "/_error" && renderStatusCode === 404;
218
+ const isStatusErrorRender = isNotFoundErrorRender || routePattern === "/500" || routePattern === "/_error" && renderStatusCode === 500;
219
+ const errorPageRevalidateSeconds = isNotFoundErrorRender ? options?.__notFoundRevalidateSeconds : void 0;
220
+ const errorPageExpireSeconds = isNotFoundErrorRender ? options?.__notFoundExpireSeconds : void 0;
221
+ const errorResponseCachePathname = options?.__notFoundCachePathname ?? isrCachePathname;
152
222
  const query = mergeRouteParamsIntoQuery(parseQueryString(renderRouteUrl), params);
153
223
  const isOnDemandRevalidate = isOnDemandRevalidateRequest(request.headers.get(PRERENDER_REVALIDATE_HEADER));
224
+ const revalidateOnlyGenerated = isOnDemandRevalidate && request.headers.has("x-prerender-revalidate-if-generated");
154
225
  const preview = isStaticPropsRoute || typeof pageModule.getServerSideProps === "function" ? getPagesPreviewState(request.headers.get("cookie"), { isOnDemandRevalidate }) : {
155
226
  data: false,
156
227
  shouldClear: false
@@ -211,11 +282,12 @@ function createPagesPageHandler(opts) {
211
282
  }
212
283
  };
213
284
  const scriptNonce = getScriptNonceFromHeaderSources(request.headers, middlewareHeaders);
285
+ const shouldApplyErrorResponsePolicy = previewData === false && !scriptNonce && isStatusErrorRender && (isStaticPropsRoute || isRouteMissErrorRender || options?.__forcedRoute !== void 0);
214
286
  let fontLinkHeader = "";
215
287
  let allFontPreloads = [];
216
288
  try {
217
289
  allFontPreloads = getFontPreloads();
218
- if (allFontPreloads.length > 0) fontLinkHeader = allFontPreloads.map((p) => "<" + appendAssetDeploymentIdQuery(p.href) + ">; rel=preload; as=font; type=" + p.type + "; crossorigin").join(", ");
290
+ if (allFontPreloads.length > 0) fontLinkHeader = allFontPreloads.map((p) => "<" + p.href + ">; rel=preload; as=font; type=" + p.type + "; crossorigin").join(", ");
219
291
  } catch {}
220
292
  const pagesResolvedUrl = (new URL(routeUrl, originalRequestUrl).pathname || "/") + originalRequestUrl.search;
221
293
  const createPageReqRes = () => {
@@ -232,6 +304,7 @@ function createPagesPageHandler(opts) {
232
304
  isDataReq,
233
305
  err: err instanceof Error ? err : void 0,
234
306
  applyRequestContexts: applySSRContext,
307
+ basePath: vinextConfig.basePath,
235
308
  buildId,
236
309
  deploymentId: process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID,
237
310
  htmlLimitedBots: vinextConfig.htmlLimitedBots,
@@ -253,6 +326,7 @@ function createPagesPageHandler(opts) {
253
326
  isBuildTimePrerendering: typeof process !== "undefined" && process.env && process.env.VINEXT_PRERENDER === "1",
254
327
  validatePropsSerialization: process.env.NODE_ENV !== "production" || process.env.VINEXT_PRERENDER === "1",
255
328
  isOnDemandRevalidate,
329
+ revalidateOnlyGenerated,
256
330
  previewData,
257
331
  pageModule,
258
332
  AppComponent,
@@ -265,10 +339,16 @@ function createPagesPageHandler(opts) {
265
339
  route: { isDynamic: route.isDynamic },
266
340
  routePattern,
267
341
  routeUrl: renderRouteUrl,
342
+ isrCachePathname,
268
343
  runInFreshUnifiedContext(callback) {
269
- return runWithRequestContext(createRequestContext({ executionContext: null }), async () => {
344
+ const revalCtx = createRequestContext({ executionContext: null });
345
+ return runWithRequestContext(revalCtx, async () => {
270
346
  ensureFetchPatch();
271
- return callback();
347
+ try {
348
+ return await callback();
349
+ } finally {
350
+ await closeAfterResponse(revalCtx);
351
+ }
272
352
  });
273
353
  },
274
354
  safeJsonStringify,
@@ -284,15 +364,26 @@ function createPagesPageHandler(opts) {
284
364
  });
285
365
  if (pageDataResult.kind === "notFound") {
286
366
  const notFoundRoute = findNotFoundRoute();
287
- if (notFoundRoute && routePattern !== "/404" && routePattern !== "/_error") return finalizePagesPreviewResponse(await renderPage(request, url, manifest, middlewareHeaders, {
367
+ let notFoundResponse;
368
+ if (notFoundRoute && routePattern !== "/404" && routePattern !== "/_error") notFoundResponse = await renderPage(request, url, manifest, middlewareHeaders, {
288
369
  statusCode: 404,
289
370
  asPath: routerAsPath,
290
371
  renderErrorPageOnMiss: false,
291
- __forcedRoute: notFoundRoute
292
- }), preview);
293
- return finalizePagesPreviewResponse(buildDefaultPagesNotFoundResponse(), preview);
372
+ __forcedRoute: notFoundRoute,
373
+ __notFoundRevalidateSeconds: pageDataResult.revalidateSeconds,
374
+ __notFoundExpireSeconds: pageDataResult.expireSeconds,
375
+ __notFoundCachePathname: isrCachePathname
376
+ });
377
+ else notFoundResponse = buildDefaultPagesNotFoundResponse();
378
+ if (isOnDemandRevalidate) notFoundResponse = withPagesCacheState(notFoundResponse, "REVALIDATED");
379
+ else if (pageDataResult.cacheState) notFoundResponse = withPagesCacheState(notFoundResponse, pageDataResult.cacheState);
380
+ return finalizePagesPreviewResponse(notFoundResponse, preview);
381
+ }
382
+ if (pageDataResult.kind === "response") {
383
+ let response = isOnDemandRevalidate && pageDataResult.onDemandRevalidateSuccess !== false ? withPagesCacheState(pageDataResult.response, "REVALIDATED") : pageDataResult.response;
384
+ if (shouldApplyErrorResponsePolicy) response = applyPagesErrorCachePolicy(response, errorPageRevalidateSeconds, errorPageExpireSeconds, errorResponseCachePathname);
385
+ return finalizePagesPreviewResponse(response, preview);
294
386
  }
295
- if (pageDataResult.kind === "response") return finalizePagesPreviewResponse(pageDataResult.response, preview);
296
387
  let pageProps = pageDataResult.pageProps;
297
388
  let renderProps = pageDataResult.props;
298
389
  if (previewData !== false) renderProps = {
@@ -312,6 +403,7 @@ function createPagesPageHandler(opts) {
312
403
  const gsspRes = pageDataResult.gsspRes;
313
404
  const documentReqRes = serializedPagesNextData.autoExport === true ? null : pageDataResult.documentReqRes ?? createPageReqRes();
314
405
  const isrRevalidateSeconds = pageDataResult.isrRevalidateSeconds;
406
+ const isrExpireSeconds = pageDataResult.isrExpireSeconds;
315
407
  const isFallbackRender = pageDataResult.isFallback === true;
316
408
  if (isFallbackRender) applySSRContext({
317
409
  query: {},
@@ -337,7 +429,7 @@ function createPagesPageHandler(opts) {
337
429
  }
338
430
  if (!hasUserCacheControl) init.headers["Cache-Control"] = NEVER_CACHE_CONTROL;
339
431
  } else if (isStaticPropsRoute) {
340
- if (isrRevalidateSeconds) {
432
+ if (isrRevalidateSeconds !== null) {
341
433
  const headers = new Headers(init.headers);
342
434
  applyCdnResponseHeaders(headers, { cacheControl: buildMissIsrCacheControl(isrRevalidateSeconds, vinextConfig.expireTime) });
343
435
  for (const [key, value] of headers) init.headers[key] = value;
@@ -352,7 +444,7 @@ function createPagesPageHandler(opts) {
352
444
  const pageModuleIds = [];
353
445
  if (appAssetPath) pageModuleIds.push(appAssetPath);
354
446
  if (route.filePath) pageModuleIds.push(route.filePath);
355
- return finalizePagesPreviewResponse(await renderPagesPageResponse({
447
+ let pageResponse = await renderPagesPageResponse({
356
448
  assetTags: collectAssetTags({
357
449
  manifest,
358
450
  moduleIds: pageModuleIds,
@@ -360,7 +452,8 @@ function createPagesPageHandler(opts) {
360
452
  disableOptimizedLoading: vinextConfig.disableOptimizedLoading,
361
453
  basePath: vinextConfig.basePath,
362
454
  assetPrefix: vinextConfig.assetPrefix,
363
- deploymentId: process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID
455
+ deploymentId: process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID,
456
+ crossOrigin: vinextConfig.crossOrigin
364
457
  }),
365
458
  buildId,
366
459
  clearSsrContext() {
@@ -386,8 +479,10 @@ function createPagesPageHandler(opts) {
386
479
  documentReqRes,
387
480
  gsspRes,
388
481
  isrCacheKey: pageIsrCacheKey,
389
- expireSeconds: vinextConfig.expireTime,
482
+ isrCachePathname,
483
+ expireSeconds: isrExpireSeconds,
390
484
  isrRevalidateSeconds,
485
+ isOnDemandRevalidate,
391
486
  isStaticPropsRoute,
392
487
  isrSet,
393
488
  i18n: buildI18nRenderContext(i18nConfig, locale, currentDefaultLocale, domainLocales),
@@ -406,12 +501,16 @@ function createPagesPageHandler(opts) {
406
501
  routeUrl: renderRouteUrl,
407
502
  safeJsonStringify,
408
503
  scriptNonce,
504
+ crossOrigin: vinextConfig.crossOrigin,
505
+ disableOptimizedLoading: vinextConfig.disableOptimizedLoading,
409
506
  statusCode: renderStatusCode,
410
507
  nextData: serializedPagesNextData,
411
508
  userAgent: request.headers.get("user-agent") ?? void 0,
412
509
  ifNoneMatch: request.headers.get("if-none-match") ?? void 0,
413
510
  requestCacheControl: request.headers.get("cache-control") ?? void 0
414
- }), preview);
511
+ });
512
+ if (shouldApplyErrorResponsePolicy) pageResponse = applyPagesErrorCachePolicy(pageResponse, errorPageRevalidateSeconds, errorPageExpireSeconds, errorResponseCachePathname);
513
+ return finalizePagesPreviewResponse(pageResponse, preview);
415
514
  } catch (e) {
416
515
  console.error("[vinext] SSR error:", e);
417
516
  reportRequestError(e instanceof Error ? e : new Error(String(e)), {
@@ -445,7 +544,7 @@ function createPagesPageHandler(opts) {
445
544
  }
446
545
  return new Response("Internal Server Error", { status: 500 });
447
546
  }
448
- });
547
+ }), uCtx);
449
548
  }
450
549
  return renderPage;
451
550
  }
@@ -88,11 +88,13 @@ type RenderPagesPageResponseOptions = {
88
88
  setDocumentInitialHead?: ((head: ReactNode[]) => void) | undefined;
89
89
  documentReqRes?: PagesDocumentReqRes | null;
90
90
  gsspRes: PagesGsspResponse | null;
91
- isrCacheKey: (router: string, pathname: string) => string;
91
+ isrCacheKey: (router: string, pathname: string) => string; /** Filesystem-route identity used for ISR persistence and cache tags. */
92
+ isrCachePathname?: string;
92
93
  expireSeconds?: number;
93
- isrRevalidateSeconds: number | null;
94
+ isrRevalidateSeconds: number | false | null; /** Synchronous `res.revalidate()` render; cache persistence must finish before returning. */
95
+ isOnDemandRevalidate?: boolean;
94
96
  isStaticPropsRoute?: boolean;
95
- isrSet: (key: string, data: CachedPagesValue, revalidateSeconds: number, tags?: string[], expireSeconds?: number) => Promise<void>;
97
+ isrSet: (key: string, data: CachedPagesValue, revalidateSeconds: number | false, tags?: string[], expireSeconds?: number) => Promise<void>;
96
98
  i18n: PagesI18nRenderContext;
97
99
  /**
98
100
  * True when rendering a `getStaticPaths` fallback shell for a path that
@@ -112,6 +114,8 @@ type RenderPagesPageResponseOptions = {
112
114
  routeUrl: string;
113
115
  safeJsonStringify: (value: unknown) => string;
114
116
  scriptNonce?: string;
117
+ crossOrigin?: string;
118
+ disableOptimizedLoading: boolean;
115
119
  statusCode?: number;
116
120
  vinext?: VinextNextData["__vinext"];
117
121
  nextData?: PagesNextDataExtras;
@@ -1,6 +1,6 @@
1
1
  import { getRequestExecutionContext } from "../shims/request-context.js";
2
+ import { NEXTJS_CACHE_HEADER } from "./headers.js";
2
3
  import { reportRequestError } from "./instrumentation.js";
3
- import { setCacheStateHeaders } from "./cache-headers.js";
4
4
  import { BROWSER_REVALIDATE_CACHE_CONTROL, NEVER_CACHE_CONTROL, NO_STORE_CACHE_CONTROL, applyCdnResponseHeaders, shouldUseNextDeployCacheControl } from "./cache-control.js";
5
5
  import { buildMissIsrCacheControl } from "./isr-decision.js";
6
6
  import { fnv1a52 } from "../utils/hash.js";
@@ -8,30 +8,21 @@ import { encodeCacheTag } from "../utils/encode-cache-tag.js";
8
8
  import { appendAssetDeploymentIdQuery } from "../utils/deployment-id.js";
9
9
  import { withScriptNonce } from "../shims/script-nonce-context.js";
10
10
  import { createNonceAttribute, escapeHtmlAttr } from "./html.js";
11
+ import { applyDocumentAssetProps, extractDocumentAssetProps } from "./pages-document-asset-props.js";
11
12
  import { getClientTraceMetadataHTML } from "./client-trace-metadata.js";
13
+ import { setCacheStateHeaders } from "./cache-headers.js";
12
14
  import { readStreamAsText } from "../utils/text-stream.js";
13
15
  import { loadUserDocumentInitialProps, runDocumentRenderPage } from "./pages-document-initial-props.js";
14
16
  import { callDocumentGetInitialProps } from "./document-initial-head.js";
17
+ import { isBotUserAgent } from "../utils/html-limited-bots.js";
15
18
  import React from "react";
16
19
  //#region src/server/pages-page-response.ts
17
20
  /**
18
- * Crawlers that cannot handle streamed HTML: they read metadata only from
19
- * the first network chunk, so streaming would give them an incomplete <head>.
20
- * Pattern sourced from Next.js html-bots.ts (updated to match the canary).
21
- */
22
- const HTML_LIMITED_BOT_UA_RE = /[\w-]+-Google|Google-[\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight/i;
23
- /**
24
- * Googlebot (the main search crawler) executes JavaScript via a headless
25
- * browser, so it too cannot safely handle mid-stream HTML mutations.
26
- * Matches "Googlebot" but NOT suffixed variants like "Googlebot-Image".
27
- */
28
- const HEADLESS_BROWSER_BOT_UA_RE = /Googlebot(?!-)|Googlebot$/i;
29
- /**
30
21
  * Returns true when the User-Agent belongs to a bot or crawler that cannot
31
22
  * reliably consume a streamed HTML response.
32
23
  */
33
24
  function isPagesStreamingBot(userAgent) {
34
- return HEADLESS_BROWSER_BOT_UA_RE.test(userAgent) || HTML_LIMITED_BOT_UA_RE.test(userAgent);
25
+ return isBotUserAgent(userAgent);
35
26
  }
36
27
  function generatePagesETag(payload) {
37
28
  return "\"" + fnv1a52(payload).toString(36) + payload.length.toString(36) + "\"";
@@ -64,7 +55,7 @@ function buildPagesFontHeadHtml(fontLinks, fontPreloads, fontStyles, scriptNonce
64
55
  let html = "";
65
56
  const nonceAttr = createNonceAttribute(scriptNonce);
66
57
  for (const link of fontLinks) html += `<link rel="stylesheet"${nonceAttr} href="${escapeHtmlAttr(appendAssetDeploymentIdQuery(link))}" />\n `;
67
- for (const preload of fontPreloads) html += `<link rel="preload"${nonceAttr} href="${escapeHtmlAttr(appendAssetDeploymentIdQuery(preload.href))}" as="font" type="${escapeHtmlAttr(preload.type)}" crossorigin />\n `;
58
+ for (const preload of fontPreloads) html += `<link rel="preload"${nonceAttr} href="${escapeHtmlAttr(preload.href)}" as="font" type="${escapeHtmlAttr(preload.type)}" crossorigin="anonymous" />\n `;
68
59
  if (fontStyles.length > 0) html += `<style data-vinext-fonts${nonceAttr}>${fontStyles.join("\n")}</style>\n `;
69
60
  return html;
70
61
  }
@@ -95,19 +86,27 @@ async function buildPagesShellHtml(bodyMarker, fontHeadHTML, nextDataScript, opt
95
86
  if (options.DocumentComponent) {
96
87
  const docProps = options.resolvedDocProps ?? await loadUserDocumentInitialProps(options.DocumentComponent);
97
88
  const docElement = docProps ? React.createElement(options.DocumentComponent, docProps) : React.createElement(options.DocumentComponent);
98
- let html = await options.renderDocumentToString(docElement);
89
+ const renderedDocument = extractDocumentAssetProps(await options.renderDocumentToString(docElement));
90
+ let html = renderedDocument.html;
91
+ const generatedAssetTags = applyDocumentAssetProps(options.assetTags, renderedDocument.props, {
92
+ configuredCrossOrigin: options.crossOrigin,
93
+ scriptOwner: options.disableOptimizedLoading ? "next-script" : "head"
94
+ });
95
+ const generatedNextDataScript = applyDocumentAssetProps(nextDataScript, renderedDocument.props, { configuredCrossOrigin: options.crossOrigin });
99
96
  html = html.replace("__NEXT_MAIN__", bodyMarker);
100
- if (options.ssrHeadHTML || options.assetTags || fontHeadHTML) html = html.replace("</head>", ` ${fontHeadHTML}${options.ssrHeadHTML}\n ${options.assetTags}\n</head>`);
101
- html = html.replace("<!-- __NEXT_SCRIPTS__ -->", nextDataScript);
102
- if (!html.includes("__NEXT_DATA__")) html = html.replace("</body>", ` ${nextDataScript}\n</body>`);
97
+ if (options.ssrHeadHTML || generatedAssetTags || fontHeadHTML) html = html.replace("</head>", ` ${fontHeadHTML}${options.ssrHeadHTML}\n ${generatedAssetTags}\n</head>`);
98
+ html = html.replace("<!-- __NEXT_SCRIPTS__ -->", generatedNextDataScript);
99
+ if (!html.includes("__NEXT_DATA__")) html = html.replace("</body>", ` ${generatedNextDataScript}\n</body>`);
103
100
  return html;
104
101
  }
102
+ const generatedAssetTags = applyDocumentAssetProps(options.assetTags, {}, { configuredCrossOrigin: options.crossOrigin });
103
+ const generatedNextDataScript = applyDocumentAssetProps(nextDataScript, {}, { configuredCrossOrigin: options.crossOrigin });
105
104
  return `<!DOCTYPE html>
106
105
  <html>
107
106
  <head>
108
- ${fontHeadHTML}${options.ssrHeadHTML}\n ${options.assetTags}\n</head>
107
+ ${fontHeadHTML}${options.ssrHeadHTML}\n ${generatedAssetTags}\n</head>
109
108
  <body>
110
- <div id="__next">${bodyMarker}</div>\n ${nextDataScript}\n</body>
109
+ <div id="__next">${bodyMarker}</div>\n ${generatedNextDataScript}\n</body>
111
110
  </html>`;
112
111
  }
113
112
  async function buildPagesCompositeStream(bodyStream, shellPrefix, shellSuffix) {
@@ -142,14 +141,18 @@ async function reportPagesIsrCacheWriteError(error, cacheKey, routePattern) {
142
141
  });
143
142
  } catch {}
144
143
  }
145
- function schedulePagesIsrCacheWrite(options) {
146
- const cacheWritePromise = readStreamAsText(options.stream).then((bodyHtml) => options.setCache(options.cacheKey, {
144
+ async function writePagesIsrCache(options) {
145
+ const bodyHtml = await readStreamAsText(options.stream);
146
+ await options.setCache(options.cacheKey, {
147
147
  kind: "PAGES",
148
148
  html: options.shellPrefix + bodyHtml + options.shellSuffix,
149
149
  pageData: options.pageData,
150
150
  headers: void 0,
151
151
  status: options.status
152
- }, options.revalidateSeconds, void 0, options.expireSeconds)).catch((error) => reportPagesIsrCacheWriteError(error, options.cacheKey, options.routePattern));
152
+ }, options.revalidateSeconds, void 0, options.expireSeconds);
153
+ }
154
+ function schedulePagesIsrCacheWrite(options) {
155
+ const cacheWritePromise = writePagesIsrCache(options).catch((error) => reportPagesIsrCacheWriteError(error, options.cacheKey, options.routePattern));
153
156
  getRequestExecutionContext()?.waitUntil(cacheWritePromise);
154
157
  }
155
158
  function applyGsspHeaders(headers, gsspRes, statusCode) {
@@ -223,10 +226,12 @@ async function renderPagesPageResponse(options) {
223
226
  if (documentRenderPage.status === "rendered" && documentRenderPage.stylesHTML) ssrHeadHTML += `\n ${documentRenderPage.stylesHTML}`;
224
227
  const shellHtml = await buildPagesShellHtml(bodyMarker, fontHeadHTML, nextDataScript, {
225
228
  assetTags: options.assetTags,
229
+ disableOptimizedLoading: options.disableOptimizedLoading,
226
230
  DocumentComponent: options.DocumentComponent,
227
231
  renderDocumentToString: options.renderDocumentToString,
228
232
  ssrHeadHTML,
229
- resolvedDocProps: documentRenderPage.status === "skipped" ? null : documentRenderPage.docProps
233
+ resolvedDocProps: documentRenderPage.status === "skipped" ? null : documentRenderPage.docProps,
234
+ crossOrigin: options.crossOrigin
230
235
  });
231
236
  options.clearSsrContext();
232
237
  const markerIndex = shellHtml.indexOf(bodyMarker);
@@ -235,15 +240,15 @@ async function renderPagesPageResponse(options) {
235
240
  const responseHeaders = new Headers({ "Content-Type": "text/html; charset=utf-8" });
236
241
  const finalStatus = applyGsspHeaders(responseHeaders, options.gsspRes ?? options.documentReqRes?.res ?? null, options.statusCode);
237
242
  let responseBodyStream = bodyStream;
238
- if (!options.scriptNonce && options.isrRevalidateSeconds !== null && options.isrRevalidateSeconds > 0) {
243
+ if (!options.scriptNonce && options.isrRevalidateSeconds !== null && (options.isrRevalidateSeconds === false || options.isrRevalidateSeconds > 0)) {
239
244
  const cacheBodyStreamPair = bodyStream.tee();
240
245
  responseBodyStream = cacheBodyStreamPair[0];
241
246
  const cacheBodyStream = cacheBodyStreamPair[1];
242
- const isrPathname = options.routeUrl.split("?")[0];
243
- schedulePagesIsrCacheWrite({
247
+ const isrPathname = options.isrCachePathname ?? options.routeUrl.split("?")[0];
248
+ const cacheWriteOptions = {
244
249
  cacheKey: options.isrCacheKey("pages", isrPathname),
245
250
  expireSeconds: options.expireSeconds,
246
- pageData: options.pageProps,
251
+ pageData: options.props ?? { pageProps: options.pageProps },
247
252
  revalidateSeconds: options.isrRevalidateSeconds,
248
253
  routePattern: options.routePattern,
249
254
  setCache: options.isrSet,
@@ -251,19 +256,22 @@ async function renderPagesPageResponse(options) {
251
256
  shellSuffix,
252
257
  status: finalStatus,
253
258
  stream: cacheBodyStream
254
- });
259
+ };
260
+ if (options.isOnDemandRevalidate) await writePagesIsrCache(cacheWriteOptions);
261
+ else schedulePagesIsrCacheWrite(cacheWriteOptions);
255
262
  }
256
263
  const compositeStream = await buildPagesCompositeStream(responseBodyStream, shellPrefix, shellSuffix);
257
264
  const userSetCacheControl = responseHeaders.has("Cache-Control");
258
265
  if (options.scriptNonce) responseHeaders.set("Cache-Control", NO_STORE_CACHE_CONTROL);
259
- else if (options.isrRevalidateSeconds) {
260
- const isrPathname = options.routeUrl.split("?")[0];
266
+ else if (options.isrRevalidateSeconds !== null) {
267
+ const isrPathname = options.isrCachePathname ?? options.routeUrl.split("?")[0];
261
268
  const stem = isrPathname.endsWith("/") ? isrPathname.slice(0, -1) : isrPathname;
262
269
  applyCdnResponseHeaders(responseHeaders, {
263
270
  cacheControl: buildMissIsrCacheControl(options.isrRevalidateSeconds, options.expireSeconds),
264
271
  tags: [encodeCacheTag(`_N_T_${stem || "/"}`)]
265
272
  });
266
- setCacheStateHeaders(responseHeaders, "MISS");
273
+ if (options.isOnDemandRevalidate) responseHeaders.set(NEXTJS_CACHE_HEADER, "REVALIDATED");
274
+ else setCacheStateHeaders(responseHeaders, "MISS");
267
275
  } else if (options.isStaticPropsRoute && shouldUseNextDeployCacheControl()) responseHeaders.set("Cache-Control", BROWSER_REVALIDATE_CACHE_CONTROL);
268
276
  else if (options.gsspRes && !userSetCacheControl) responseHeaders.set("Cache-Control", NEVER_CACHE_CONTROL);
269
277
  if (options.fontLinkHeader) responseHeaders.set("Link", options.fontLinkHeader);
@@ -45,6 +45,13 @@ type PagesPipelineDeps = {
45
45
  ctx?: unknown;
46
46
  rawSearch?: string;
47
47
  configMatchPathname?: string;
48
+ /**
49
+ * Validate an on-demand revalidation credential using the runtime's
50
+ * authoritative build secret. Production adapters must inject the verifier
51
+ * exported by the generated server entry so this early middleware decision
52
+ * uses the same baked secret as the eventual page renderer.
53
+ */
54
+ authorizeOnDemandRevalidate?: (headerValue: string | null) => boolean;
48
55
  matchPageRoute?: ((pathname: string, request: Request) => PageRouteMatch | null) | null;
49
56
  runMiddleware?: ((request: Request, ctx: unknown, opts: {
50
57
  isDataRequest: boolean;
@@ -1,9 +1,11 @@
1
1
  import { addBasePathToPathname, hasBasePath } from "../utils/base-path.js";
2
2
  import { patternToNextFormat } from "../routing/route-validation.js";
3
+ import { PRERENDER_REVALIDATE_HEADER } from "../utils/protocol-headers.js";
3
4
  import { requestContextFromRequest } from "../config/request-context.js";
4
5
  import { isExternalUrl } from "../utils/external-url.js";
5
6
  import { applyMiddlewareRequestHeaders, matchRedirect, matchRewrite, preserveRedirectDestinationQuery, proxyExternalRequest, sanitizeDestination } from "../config/config-matchers.js";
6
7
  import { cloneRequestWithUrl, normalizeTrailingSlash } from "./request-pipeline.js";
8
+ import { isOnDemandRevalidateRequest } from "./isr-cache.js";
7
9
  import { mergeRewriteQuery } from "../utils/query.js";
8
10
  import { normalizeDefaultLocalePathname, stripI18nLocaleForApiRoute } from "./pages-i18n.js";
9
11
  import { buildMiddlewarePrefetchSkipResponse } from "./pages-data-route.js";
@@ -56,6 +58,8 @@ async function runPagesRequest(request, deps) {
56
58
  const url = new URL(request.url);
57
59
  let pathname = url.pathname;
58
60
  const search = url.search;
61
+ const revalidateHeader = request.headers.get(PRERENDER_REVALIDATE_HEADER);
62
+ const isOnDemandRevalidate = deps.authorizeOnDemandRevalidate ? deps.authorizeOnDemandRevalidate(revalidateHeader) : isOnDemandRevalidateRequest(revalidateHeader);
59
63
  const requestConfigPathname = deps.configMatchPathname ?? pathname;
60
64
  const basePathState = {
61
65
  basePath,
@@ -98,7 +102,7 @@ async function runPagesRequest(request, deps) {
98
102
  };
99
103
  return served ? { type: "handled" } : null;
100
104
  };
101
- if (typeof deps.runMiddleware === "function") {
105
+ if (!isOnDemandRevalidate && typeof deps.runMiddleware === "function") {
102
106
  const result = await deps.runMiddleware(request, deps.ctx ?? null, { isDataRequest });
103
107
  if (result.waitUntilPromises && result.waitUntilPromises.length > 0) {
104
108
  const ctx = deps.ctx;
@@ -10,6 +10,6 @@ type RevalidateOptions = {
10
10
  */
11
11
  unstable_onlyGenerated?: boolean;
12
12
  };
13
- declare function performOnDemandRevalidate(source: IncomingMessage | Headers, urlPath: string, opts?: RevalidateOptions): Promise<void>;
13
+ declare function performOnDemandRevalidate(source: IncomingMessage | Headers, urlPath: string, opts?: RevalidateOptions, trustedOrigin?: string, allowedRevalidateHeaderKeys?: readonly string[], dev?: boolean): Promise<void>;
14
14
  //#endregion
15
15
  export { RevalidateOptions, performOnDemandRevalidate };
@@ -1,19 +1,65 @@
1
+ import { getRequestExecutionContext } from "../shims/request-context.js";
2
+ import { PRERENDER_REVALIDATE_HEADER, PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER, VINEXT_REVALIDATE_HOST_HEADER } from "../utils/protocol-headers.js";
1
3
  import "./headers.js";
2
- import { PRERENDER_REVALIDATE_HEADER, PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER, getRevalidateSecret } from "./isr-cache.js";
4
+ import { getRevalidateSecret, isOnDemandRevalidateRequest } from "./isr-cache.js";
5
+ import { normalizeDomainHostname } from "../utils/domain-locale.js";
3
6
  import { resolveRequestHost, resolveRequestProtocol } from "./proxy-trust.js";
4
7
  //#region src/server/pages-revalidate.ts
5
- async function performOnDemandRevalidate(source, urlPath, opts = {}) {
8
+ async function performOnDemandRevalidate(source, urlPath, opts = {}, trustedOrigin, allowedRevalidateHeaderKeys = [], dev = false) {
6
9
  if (typeof urlPath !== "string" || !urlPath.startsWith("/")) throw new Error(`Invalid urlPath provided to revalidate(), must be a path e.g. /blog/post-1, received ${urlPath}`);
7
- const proto = resolveRequestProtocol(source);
8
- const host = resolveRequestHost(source, "localhost");
9
- const target = new URL(urlPath, `${proto}://${host}`);
10
+ if (isSourceRevalidationRequest(source)) throw new Error(`Cannot revalidate ${urlPath} from an internal revalidation request`);
11
+ const executionContext = getRequestExecutionContext();
12
+ const dispatchRevalidate = executionContext?.dispatchPagesRevalidate;
13
+ const target = createRevalidateTarget(source, urlPath, trustedOrigin);
10
14
  const headers = { [PRERENDER_REVALIDATE_HEADER]: getRevalidateSecret() };
15
+ if (trustedOrigin && !dispatchRevalidate) {
16
+ const logicalHostname = normalizeDomainHostname(resolveRequestHost(source, "localhost"));
17
+ if (logicalHostname) headers[VINEXT_REVALIDATE_HOST_HEADER] = logicalHostname;
18
+ }
11
19
  if (opts.unstable_onlyGenerated) headers[PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER] = "1";
12
- const res = await fetch(target, {
20
+ const allowedHeaders = new Set(allowedRevalidateHeaderKeys.map((key) => key.toLowerCase()));
21
+ if (dev) allowedHeaders.add("cookie");
22
+ for (const key of allowedHeaders) {
23
+ const value = readSourceHeader(source, key);
24
+ if (value !== void 0) headers[key] = value;
25
+ }
26
+ if (dispatchRevalidate && executionContext.isInternalPagesRevalidation) throw new Error(`Cannot revalidate ${urlPath} from an internal revalidation request`);
27
+ const res = await fetchRevalidateTarget(target, headers, dispatchRevalidate);
28
+ if (!((res.headers.get("x-vercel-cache") ?? res.headers.get("x-nextjs-cache"))?.toUpperCase() === "REVALIDATED" || res.status === 200 || res.status === 404 && opts.unstable_onlyGenerated === true)) throw new Error(`Failed to revalidate ${urlPath}: ${res.status}`);
29
+ }
30
+ function readSourceHeader(source, key) {
31
+ if (isWebHeaders(source)) return source.get(key) ?? void 0;
32
+ const value = source.headers[key];
33
+ return Array.isArray(value) ? value.join(", ") : value;
34
+ }
35
+ function isSourceRevalidationRequest(source) {
36
+ return isOnDemandRevalidateRequest(isWebHeaders(source) ? source.get(PRERENDER_REVALIDATE_HEADER) : source.headers[PRERENDER_REVALIDATE_HEADER]);
37
+ }
38
+ function isWebHeaders(source) {
39
+ return typeof source.get === "function";
40
+ }
41
+ async function fetchRevalidateTarget(initialTarget, headers, dispatchRevalidate) {
42
+ const request = new Request(initialTarget, {
13
43
  method: "HEAD",
14
- headers
44
+ headers,
45
+ redirect: "manual"
15
46
  });
16
- if (!(res.headers.get("x-nextjs-cache")?.toUpperCase() === "REVALIDATED" || res.status === 200 || res.status === 404 && opts.unstable_onlyGenerated === true)) throw new Error(`Failed to revalidate ${urlPath}: ${res.status}`);
47
+ return dispatchRevalidate ? await dispatchRevalidate(request) : await fetch(request);
48
+ }
49
+ function createRevalidateTarget(source, urlPath, trustedOrigin) {
50
+ const origin = resolveRevalidateOrigin(source, trustedOrigin);
51
+ const target = new URL(`${origin}${urlPath}`);
52
+ if (target.origin !== origin) throw new Error(`Invalid urlPath provided to revalidate(), resolved outside application origin`);
53
+ return target;
54
+ }
55
+ function resolveRevalidateOrigin(source, trustedOrigin) {
56
+ if (trustedOrigin) return normalizeRevalidateOrigin(trustedOrigin);
57
+ return normalizeRevalidateOrigin(`${resolveRequestProtocol(source)}://${resolveRequestHost(source, "localhost")}`);
58
+ }
59
+ function normalizeRevalidateOrigin(origin) {
60
+ const parsed = new URL(origin);
61
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`Invalid revalidate origin protocol: ${parsed.protocol}`);
62
+ return parsed.origin;
17
63
  }
18
64
  //#endregion
19
65
  export { performOnDemandRevalidate };
@@ -22,6 +22,7 @@ type PagesWorkerEnv = {
22
22
  type PagesWorkerExecutionContext = {
23
23
  waitUntil?(promise: Promise<unknown>): void;
24
24
  passThroughOnException?(): void;
25
+ cache?: unknown;
25
26
  };
26
27
  declare const _default: {
27
28
  fetch(request: Request, env?: PagesWorkerEnv, ctx?: PagesWorkerExecutionContext): Promise<Response>;