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
@@ -199,6 +199,18 @@ function canonicalizePrewarmableRscRequestHeaders(headers) {
199
199
  return true;
200
200
  }
201
201
  /**
202
+ * Convert a search-param shell prefetch to the canonical navigation of its
203
+ * page. The shell renders the page with an empty query (app-page-dispatch.ts),
204
+ * which is what the query-free navigation renders, as Next.js's static
205
+ * prefetch of a page segment never varies on search params. The caller drops
206
+ * the query from the URL.
207
+ */
208
+ function canonicalizeDynamicShellRscRequestHeaders(headers) {
209
+ if (headers.get("X-Vinext-Rsc-Render-Mode") !== "prefetch-dynamic-shell" || headers.has("X-Vinext-Interception-Context") || headers.has("X-Vinext-Interception-Id") || headers.has("X-Vinext-Mounted-Slots")) return false;
210
+ headers.delete(VINEXT_RSC_RENDER_MODE_HEADER);
211
+ return canonicalizePrewarmableRscRequestHeaders(headers);
212
+ }
213
+ /**
202
214
  * Convert an ordinary loading-boundary prefetch to the shared loading-shell
203
215
  * variant. The payload is selected by the three retained mode headers; visible
204
216
  * router state is transport context and must not fragment the shared response.
@@ -285,4 +297,4 @@ async function resolveInvalidRscCacheBustingRequest(options) {
285
297
  });
286
298
  }
287
299
  //#endregion
288
- export { VINEXT_RSC_BUILD_ID_HEADER, VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM, VINEXT_RSC_COMPATIBILITY_ID_HEADER, VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_RENDER_MODE_HEADER, VINEXT_RSC_VARY_HEADER, applyRscCompatibilityIdHeader, applyRscDeploymentIdHeader, canonicalizeLoadingShellRscRequestHeaders, canonicalizePrewarmableRscRequestHeaders, computeRscCacheBustingSearchParam, createCanonicalLoadingShellRscRequestHeaders, createCanonicalRscRequestHeaders, createCanonicalRscRequestUrl, createRscRedirectLocation, createRscRequestHeaders, createRscRequestUrl, createServerActionRequestUrl, formatRscRequestPath, getVinextRscCompatibilityId, hasRscCacheBustingSearchParam, isRscCompatibilityIdCompatible, keepOnlyValidatedRscCacheBustingSearchParam, parseRscRequestPath, resolveHardNavigationTargetFromRscResponse, resolveInvalidRscCacheBustingRequest, resolveRscCompatibilityNavigationDecision, setRscCacheBustingSearchParam, stripRscCacheBustingSearchParam, toPathAbsolutePathname };
300
+ export { VINEXT_RSC_BUILD_ID_HEADER, VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM, VINEXT_RSC_COMPATIBILITY_ID_HEADER, VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_RENDER_MODE_HEADER, VINEXT_RSC_VARY_HEADER, applyRscCompatibilityIdHeader, applyRscDeploymentIdHeader, canonicalizeDynamicShellRscRequestHeaders, canonicalizeLoadingShellRscRequestHeaders, canonicalizePrewarmableRscRequestHeaders, computeRscCacheBustingSearchParam, createCanonicalLoadingShellRscRequestHeaders, createCanonicalRscRequestHeaders, createCanonicalRscRequestUrl, createRscRedirectLocation, createRscRequestHeaders, createRscRequestUrl, createServerActionRequestUrl, formatRscRequestPath, getVinextRscCompatibilityId, hasRscCacheBustingSearchParam, isRscCompatibilityIdCompatible, keepOnlyValidatedRscCacheBustingSearchParam, parseRscRequestPath, resolveHardNavigationTargetFromRscResponse, resolveInvalidRscCacheBustingRequest, resolveRscCompatibilityNavigationDecision, setRscCacheBustingSearchParam, stripRscCacheBustingSearchParam, toPathAbsolutePathname };
@@ -1,13 +1,14 @@
1
1
  import { getRequestExecutionContext, runWithExecutionContext } from "../shims/request-context.js";
2
2
  import { createWorkerRevalidationContext } from "./worker-revalidation-context.js";
3
3
  import { createAppRscRequestHandler } from "./app-rsc-handler.js";
4
+ import { resolveSrvxMiddlewareOrigin } from "./srvx-middleware-origin.js";
4
5
  //#region src/server/app-rsc-combined-handler.ts
5
6
  /** Combined request/response handler used by default single-stage App entries. */
6
7
  function createAppRscHandler(options) {
7
8
  const appRscRequestHandler = createAppRscRequestHandler(options);
8
9
  const appRscHandler = (request, ctx, ...rest) => {
9
10
  if (ctx != null || getRequestExecutionContext()) return appRscRequestHandler(request, ctx, ...rest);
10
- const executionContext = createWorkerRevalidationContext(void 0, (internalRequest, internalCtx) => runWithExecutionContext(internalCtx, () => appRscRequestHandler(internalRequest, internalCtx)), "node");
11
+ const executionContext = createWorkerRevalidationContext({ middlewareOrigin: resolveSrvxMiddlewareOrigin(request) }, (internalRequest, internalCtx) => runWithExecutionContext(internalCtx, () => appRscRequestHandler(internalRequest, internalCtx)), "node");
11
12
  return runWithExecutionContext(executionContext, () => appRscRequestHandler(request, ctx, ...rest));
12
13
  };
13
14
  return Object.assign(appRscHandler, { handleResponseStage(request, ctx, props, stageOptions) {
@@ -26,6 +26,8 @@ type RunAppMiddlewareOptions = {
26
26
  isDataRequest: boolean;
27
27
  middlewareRequest?: Request;
28
28
  request: Request;
29
+ /** `shouldRestoreLocaleRootTrailingSlash` for the raw request pathname. */
30
+ restoreLocaleRootTrailingSlash?: boolean;
29
31
  validateExternalRewriteRequest: () => Promise<Response | null>;
30
32
  };
31
33
  export type AppRscHandlerRoute = {
@@ -186,6 +188,8 @@ type RenderPagesFallbackOptions = {
186
188
  pathname?: string;
187
189
  pagesDataRequest?: Request | null;
188
190
  request: Request;
191
+ /** The basePath `request.url` still carries, removed before the page renders. */
192
+ requestBasePath?: string;
189
193
  /** The shared cache entry answers every host; a request-time render is not stored. */
190
194
  sharedAcrossHosts?: boolean;
191
195
  url: URL;
@@ -15,12 +15,13 @@ import { patternToNextFormat } from "../routing/route-validation.js";
15
15
  import { getRequestExecutionContext } from "../shims/request-context.js";
16
16
  import { runOutsideWorkStoreScope } from "../shims/internal/work-store-scope.js";
17
17
  import { isInterceptionMatchedUrlPath, normalizePath } from "./normalize-path.js";
18
- import { normalizeDefaultLocalePathname, shouldRestoreLocaleRootTrailingSlash } from "./pages-i18n.js";
18
+ import { isLocaleRedirect, normalizeDefaultLocalePathname, shouldRestoreLocaleRootTrailingSlash } from "./pages-i18n.js";
19
19
  import { buildNextDataNotFoundResponse, normalizePagesDataRequest } from "./pages-data-route.js";
20
20
  import { markRouteCacheabilityDynamic, preserveRouteCacheabilityResponsePolicy } from "../shims/cacheability-classification.js";
21
21
  import { NEVER_CACHE_CONTROL, SERVER_ACTION_CACHE_CONTROL, applyCdnResponseHeaders, captureCdnResponsePolicyOverrides, isCdnResponsePolicyHeader, reconcileCdnResponseHeadersAfterOuterPolicy } from "./cache-control.js";
22
22
  import { isOnDemandRevalidateRequest } from "./revalidation-request.js";
23
23
  import { getDraftModeCookieHeader, getEffectiveRequestCookieHeader, getHeadersContext, hasEffectiveRequestCookieChanges, headersContextFromRequest, isDraftModeEnabled, isDraftModeRequest, runWithHeadersContext } from "../shims/headers.js";
24
+ import { normalizePathTrailingSlash } from "../shims/url-utils.js";
24
25
  import { mergeRewriteQuery } from "../utils/query.js";
25
26
  import { getScriptNonceFromHeaderSources } from "./csp.js";
26
27
  import { traceFindPageComponents } from "./pages-execution-tracing.js";
@@ -31,9 +32,9 @@ import { openForwardedMiddlewareContext } from "./forwarded-middleware-context.j
31
32
  import { consumeResponseStageLinkProvenance, copyLinkHeaderProvenance } from "./app-response-header-provenance.js";
32
33
  import { setFrameworkRequestRoute, traceFrameworkRequest } from "./request-tracing.js";
33
34
  import { mergeMiddlewareResponseHeaders, mergeMiddlewareResponseHeadersOntoResponse, withExternalRewriteResponseHeaders } from "./middleware-response-headers.js";
34
- import { VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM, canonicalizeLoadingShellRscRequestHeaders, canonicalizePrewarmableRscRequestHeaders, createCanonicalRscRequestUrl, createRscRedirectLocation, createRscRequestUrl, hasRscCacheBustingSearchParam, resolveInvalidRscCacheBustingRequest, stripRscCacheBustingSearchParam } from "./app-rsc-cache-busting.js";
35
+ import { VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM, canonicalizeDynamicShellRscRequestHeaders, canonicalizeLoadingShellRscRequestHeaders, canonicalizePrewarmableRscRequestHeaders, createCanonicalRscRequestUrl, createRscRedirectLocation, createRscRequestUrl, hasRscCacheBustingSearchParam, resolveInvalidRscCacheBustingRequest, stripRscCacheBustingSearchParam } from "./app-rsc-cache-busting.js";
35
36
  import { matchPrerenderRouteParamsPayload, readTrustedPrerenderRouteParams, serializePrerenderRouteParamsHeader } from "./prerender-route-params.js";
36
- import { applyAppRscConfigHeaders, finalizeAppRscResponse, isStreamedActionRedirectResponse, markAppRscResponseConfigHeadersApplied, markStreamedActionRedirectResponse } from "./app-rsc-response-finalizer.js";
37
+ import { applyAppRscConfigHeaders, finalizeAppRscResponse, isStreamedActionRedirectResponse, markAppRscApplicationResponse, markAppRscResponseConfigHeadersApplied, markAppRscRouteHandlerResponse, markStreamedActionRedirectResponse } from "./app-rsc-response-finalizer.js";
37
38
  import { createServerActionNotFoundResponse, getServerActionNotFoundMessage } from "./server-action-not-found.js";
38
39
  import { isPossibleAppRouteActionRequest } from "./app-action-request.js";
39
40
  import "./app-page-response.js";
@@ -442,6 +443,7 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
442
443
  isDataRequest: isMiddlewareDataRequest,
443
444
  middlewareRequest: isolatedMiddlewareRequest,
444
445
  request: userlandRequest,
446
+ restoreLocaleRootTrailingSlash,
445
447
  validateExternalRewriteRequest: () => validateClaimedOutsideBasePathRsc(true)
446
448
  });
447
449
  if (!dispatchResponseStage && middlewareResult.pathnameEligible) markRouteCacheabilityDynamic(middlewareResult.matched ? "middleware matched this request" : "middleware is eligible for this pathname");
@@ -499,19 +501,28 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
499
501
  const responseStagePolicy = await loadResponseStagePolicy();
500
502
  const cache = responseStageProbeMode || isOnDemandRevalidate || (props.kind === "app-page" || props.kind === "app-route-handler") && (props.bypassInterceptionContextCache || props.forceDynamic === true && responseStagePolicy === null) ? "bypass" : canUseSharedWorkerResponseStage ? "shared" : "bypass";
501
503
  let dispatchRequest = props.kind === "app-page" ? prepareSharedAppPageDispatch(stageRequest, cache) : stageRequest;
504
+ let dispatchProps = props;
502
505
  let canonicalRsc = null;
503
506
  if (cache === "shared" && props.kind === "app-page" && props.isRscRequest && (props.matchKind === "request" || props.matchKind === "resolved") && props.interceptionContext === null && props.interceptionId === null && props.mountedSlotsHeader === null) {
504
507
  const headers = new Headers(dispatchRequest.headers);
505
- if (props.renderMode === "navigation" ? canonicalizePrewarmableRscRequestHeaders(headers) : props.renderMode === "prefetch-loading-shell" && props.canUseCanonicalLoadingShell ? canonicalizeLoadingShellRscRequestHeaders(headers) : false) {
508
+ if (props.renderMode === "navigation" ? canonicalizePrewarmableRscRequestHeaders(headers) : props.renderMode === "prefetch-loading-shell" && props.canUseCanonicalLoadingShell ? canonicalizeLoadingShellRscRequestHeaders(headers) : props.renderMode === "prefetch-dynamic-shell" ? canonicalizeDynamicShellRscRequestHeaders(headers) : false) {
506
509
  canonicalRsc = {
507
510
  headers,
508
- navigation: props.renderMode === "navigation"
511
+ navigation: props.renderMode !== "prefetch-loading-shell"
509
512
  };
510
513
  dispatchRequest = cloneRequestWithHeaders(dispatchRequest, headers);
514
+ if (props.renderMode === "prefetch-dynamic-shell") {
515
+ const queryFree = withoutAppPageDispatchQuery(dispatchRequest.url, {
516
+ ...props,
517
+ renderMode: "navigation"
518
+ });
519
+ dispatchRequest = cloneRequestWithUrl(dispatchRequest, queryFree.url);
520
+ dispatchProps = queryFree.props;
521
+ }
511
522
  }
512
523
  }
513
524
  let stageProps = {
514
- ...props,
525
+ ...dispatchProps,
515
526
  cacheability: {
516
527
  ...props.cacheability,
517
528
  policyHeaders: responseStagePolicy
@@ -579,7 +590,7 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
579
590
  }
580
591
  return options.handleMetadataRouteRequest?.(cleanPathname, metadataRoutePathname) ?? null;
581
592
  };
582
- const applyConfigHeadersToResponseStage = async (response, preserveExistingPolicy = false) => {
593
+ const applyConfigHeadersToResponseStage = async (response, preserveExistingPolicy = false, routeHandlerRuntime) => {
583
594
  const headers = new Headers(response.headers);
584
595
  copyLinkHeaderProvenance(response.headers, headers);
585
596
  await applyAppRscConfigHeaders(headers, request, {
@@ -590,7 +601,8 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
590
601
  middlewareHeaders: middlewareContext.headers,
591
602
  overwriteExisting: preserveExistingPolicy ? /* @__PURE__ */ new Set() : isCdnResponsePolicyHeader,
592
603
  recordCacheability: dispatchResponseStage === void 0,
593
- requestContext: preMiddlewareRequestContext
604
+ requestContext: preMiddlewareRequestContext,
605
+ routeHandlerRuntime
594
606
  });
595
607
  return preserveFullyBufferedBodyMetadata(response, new Response(response.body, {
596
608
  headers,
@@ -616,10 +628,10 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
616
628
  })();
617
629
  let outerResponsePolicyPromise;
618
630
  const loadOuterResponsePolicy = () => outerResponsePolicyPromise ??= Promise.all([loadPreHandlerResponseHeaders(), loadResponseStagePolicy()]).then(([headers, responseStagePolicy]) => captureCdnResponsePolicyOverrides(headers, new Headers(responseStagePolicy ?? [])));
619
- const composeResponseStageResponse = async (response) => {
631
+ const composeResponseStageResponse = async (response, routeHandlerRuntime) => {
620
632
  const transportedLink = consumeResponseStageLinkProvenance(response);
621
633
  response = transportedLink.response;
622
- response = await applyConfigHeadersToResponseStage(response, Boolean(dispatchResponseStage || options.renderResponseStageLocally));
634
+ response = await applyConfigHeadersToResponseStage(response, Boolean(dispatchResponseStage || options.renderResponseStageLocally), routeHandlerRuntime);
623
635
  response = applyMiddlewareContextToResponse(response, middlewareContext, transportedLink.appendToPostConfigLink);
624
636
  reconcileCdnResponseHeadersAfterOuterPolicy(response.headers, await loadOuterResponsePolicy());
625
637
  return markAppRscResponseConfigHeadersApplied(response);
@@ -770,6 +782,7 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
770
782
  hadBasePath: resolvedPathnameHasBasePath,
771
783
  isDataRequest: isMiddlewareDataRequest,
772
784
  request: sourceMiddlewareRequest,
785
+ restoreLocaleRootTrailingSlash: shouldRestoreLocaleRootTrailingSlash(normalizePathTrailingSlash(sourceUrlPathname, options.trailingSlash ?? false), options),
773
786
  validateExternalRewriteRequest: () => validateClaimedOutsideBasePathRsc(true)
774
787
  }));
775
788
  } finally {
@@ -778,6 +791,7 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
778
791
  }
779
792
  if (!dispatchResponseStage && sourceMiddlewareResult.pathnameEligible) markRouteCacheabilityDynamic(sourceMiddlewareResult.matched ? "middleware matched this request" : "middleware is eligible for this pathname");
780
793
  if (sourceMiddlewareResult.kind === "response") {
794
+ if (request.body && !request.body.locked) request.body.cancel().catch(() => {});
781
795
  options.clearRequestContext();
782
796
  return sourceMiddlewareResult.response;
783
797
  }
@@ -913,7 +927,7 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
913
927
  request,
914
928
  routeMatch
915
929
  });
916
- if (progressiveActionResult instanceof Response) return markServerActionResponse(progressiveActionResult);
930
+ if (progressiveActionResult instanceof Response) return markServerActionResponse(markAppRscApplicationResponse(progressiveActionResult));
917
931
  if (progressiveActionResult?.kind !== "form-state") return null;
918
932
  if ("actionError" in progressiveActionResult && middlewareContext.status === null && progressiveActionHttpFallbackStatus(progressiveActionResult.actionError) === null) middlewareContext.status = 500;
919
933
  return progressiveActionResult;
@@ -998,23 +1012,24 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
998
1012
  pathname: pagesRouteUrl,
999
1013
  pagesDataRequest,
1000
1014
  request,
1015
+ requestBasePath: hadBasePath ? options.basePath : "",
1001
1016
  url
1002
1017
  })) ?? null : null;
1003
1018
  if (response) preserveRouteCacheabilityResponsePolicy();
1004
1019
  if (!response) return null;
1005
1020
  if (sharedOuterPolicyNeedsReconciliation) reconcileCdnResponseHeadersAfterOuterPolicy(response.headers, await loadOuterResponsePolicy());
1021
+ const markPagesResponse = (pagesResponse) => dispatchPagesResponseStage ? markAppRscResponseConfigHeadersApplied(pagesResponse) : isLocaleRedirect(response) ? pagesResponse : markAppRscApplicationResponse(pagesResponse);
1006
1022
  const isRewritten = resolvedUrl !== originalResolvedUrl;
1007
1023
  const isPrerender = typeof process !== "undefined" && process.env?.VINEXT_PRERENDER === "1";
1008
- if ((!pagesDataRequest || !isRewritten) && !isPrerender) return dispatchPagesResponseStage ? markAppRscResponseConfigHeadersApplied(response) : response;
1024
+ if ((!pagesDataRequest || !isRewritten) && !isPrerender) return markPagesResponse(response);
1009
1025
  const headers = new Headers(response.headers);
1010
1026
  if (pagesDataRequest && isRewritten) headers.set("x-nextjs-rewrite", resolvedUrl);
1011
1027
  if (isPrerender) headers.set(VINEXT_PRERENDER_REWRITTEN_HEADER, isRewritten ? "1" : "0");
1012
- const markedResponse = new Response(response.body, {
1028
+ return markPagesResponse(new Response(response.body, {
1013
1029
  headers,
1014
1030
  status: response.status,
1015
1031
  statusText: response.statusText
1016
- });
1017
- return dispatchPagesResponseStage ? markAppRscResponseConfigHeadersApplied(markedResponse) : markedResponse;
1032
+ }));
1018
1033
  };
1019
1034
  const staticPagesFallbackResponse = await renderPagesForMatchKind("static");
1020
1035
  if (staticPagesFallbackResponse) {
@@ -1198,7 +1213,7 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
1198
1213
  route: null,
1199
1214
  scriptNonce
1200
1215
  }));
1201
- if (renderedNotFoundResponse) return withUnmatchedRouteCacheControl(renderedNotFoundResponse);
1216
+ if (renderedNotFoundResponse) return markAppRscApplicationResponse(withUnmatchedRouteCacheControl(renderedNotFoundResponse));
1202
1217
  options.clearRequestContext();
1203
1218
  const headers = new Headers();
1204
1219
  mergeMiddlewareResponseHeaders(headers, middlewareContext.headers);
@@ -1249,9 +1264,10 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
1249
1264
  const rootParams = pickRootParams(renderParams, route.rootParamNames);
1250
1265
  setRootParams(rootParams);
1251
1266
  if (route.routeHandler) {
1252
- const routeHandlerRequest = isEdgeRouteHandler(route.routeHandler) ? userlandRequest : request;
1267
+ const routeHandlerRuntime = isEdgeRouteHandler(route.routeHandler) ? "edge" : "node";
1268
+ const routeHandlerRequest = routeHandlerRuntime === "edge" ? userlandRequest : request;
1253
1269
  const routeHandlerUrl = new URL(routeHandlerRequest.url);
1254
- const internalRscValues = isEdgeRouteHandler(route.routeHandler) ? [] : routeHandlerUrl.searchParams.getAll(VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM);
1270
+ const internalRscValues = routeHandlerRuntime === "edge" ? [] : routeHandlerUrl.searchParams.getAll(VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM);
1255
1271
  routeHandlerUrl.search = resolvedSearchParams.toString();
1256
1272
  for (const internalRscValue of internalRscValues) routeHandlerUrl.searchParams.append(VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM, internalRscValue);
1257
1273
  if (matchedResponseStage) return composeResponseStageResponse(await matchedResponseStage(responseStageRequest(), {
@@ -1279,10 +1295,10 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
1279
1295
  routePattern: route.pattern,
1280
1296
  routePathname: responseStageRoutePathname,
1281
1297
  scriptNonce: scriptNonce ?? null
1282
- }));
1298
+ }), routeHandlerRuntime);
1283
1299
  const { setCurrentFetchSoftTags } = await import("../shims/fetch-cache.js");
1284
1300
  setCurrentFetchSoftTags(buildPageCacheTags(cleanPathname, [], [...route.routeSegments], "route"));
1285
- return options.dispatchMatchedRouteHandler({
1301
+ const routeHandlerResponse = await options.dispatchMatchedRouteHandler({
1286
1302
  bypassInterceptionContextCache,
1287
1303
  cachePathname,
1288
1304
  cleanPathname,
@@ -1292,6 +1308,7 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
1292
1308
  route,
1293
1309
  searchParams: resolvedSearchParams
1294
1310
  });
1311
+ return markAppRscRouteHandlerResponse(routeHandlerResponse, routeHandlerRuntime);
1295
1312
  }
1296
1313
  const failedProgressiveActionResult = progressiveActionFormState && "actionError" in progressiveActionFormState ? progressiveActionFormState : null;
1297
1314
  const actionHttpFallbackStatus = failedProgressiveActionResult ? progressiveActionHttpFallbackStatus(failedProgressiveActionResult.actionError) : null;
@@ -1354,8 +1371,8 @@ async function handleAppRscRequest(options, request, preMiddlewareRequestContext
1354
1371
  searchParams: resolvedSearchParams,
1355
1372
  renderMode
1356
1373
  }), isRscRequest && request.headers.get("Next-Router-Prefetch") === "1" && request.headers.has("Next-Router-Segment-Prefetch"));
1357
- if (progressiveActionFormState) return markServerActionResponse(applyProgressiveActionSideEffects(pageResponse, progressiveActionFormState));
1358
- return pageResponse;
1374
+ if (progressiveActionFormState) return markServerActionResponse(markAppRscApplicationResponse(applyProgressiveActionSideEffects(pageResponse, progressiveActionFormState)));
1375
+ return markAppRscApplicationResponse(pageResponse);
1359
1376
  }
1360
1377
  function progressiveActionHttpFallbackStatus(actionError) {
1361
1378
  const actionErrorDigest = actionError && typeof actionError === "object" && "digest" in actionError ? String(actionError.digest) : null;
@@ -1,5 +1,6 @@
1
1
  import { NextHeader, NextI18nConfig } from "../config/next-config.js";
2
2
  import { RequestContext } from "../config/request-context.js";
3
+ import { RouteHandlerRuntime } from "./config-headers.js";
3
4
  //#region src/server/app-rsc-response-finalizer.d.ts
4
5
  type FinalizeAppRscResponseOptions = {
5
6
  basePath: string;
@@ -29,12 +30,23 @@ type FinalizeAppRscResponseOptions = {
29
30
  middlewareHeaders?: Headers | null;
30
31
  /** Whether config matching should update the active cacheability classification. */
31
32
  recordCacheability?: boolean;
33
+ /** The runtime of the Route Handler that produced the response, if one did. */
34
+ routeHandlerRuntime?: RouteHandlerRuntime;
32
35
  };
33
36
  /** Mark a response whose final target pipeline has already applied config headers. */
34
37
  export declare function markAppRscResponseConfigHeadersApplied(response: Response): Response;
35
38
  /** Mark a Server Action redirect that streams its target's render, and so keeps its policy. */
36
39
  export declare function markStreamedActionRedirectResponse(response: Response): Response;
37
40
  export declare function isStreamedActionRedirectResponse(response: Response): boolean;
41
+ /**
42
+ * Mark a response that a page, Route Handler, Server Action or Pages route
43
+ * produced. Next.js stages next.config headers before application code runs,
44
+ * so the redirects these return carry them. Redirects that routing answers
45
+ * itself (next.config redirects, trailing slash, middleware) go out without.
46
+ */
47
+ export declare function markAppRscApplicationResponse(response: Response): Response;
48
+ /** Mark a response a Route Handler produced, so its own headers meet config as in Next.js. */
49
+ export declare function markAppRscRouteHandlerResponse(response: Response, runtime: RouteHandlerRuntime): Response;
38
50
  /** Apply only the matching next.config headers for an App Router request. */
39
51
  export declare function applyAppRscConfigHeaders(headers: Headers, request: Request, options: FinalizeAppRscResponseOptions): Promise<void>;
40
52
  /**
@@ -45,9 +57,9 @@ export declare function applyAppRscConfigHeaders(headers: Headers, request: Requ
45
57
  * handling, so that every response path (page, route handler, server action,
46
58
  * metadata, not-found) gets headers applied consistently.
47
59
  *
48
- * Skips 3xx redirect responses. Response.redirect() creates immutable
49
- * headers that throw on mutation, and Next.js does not apply config headers
50
- * to redirects regardless.
60
+ * Skips 3xx responses that routing answered itself, as Next.js does not
61
+ * apply config headers to those. A 3xx that application code returned is
62
+ * finalized like any other response.
51
63
  */
52
64
  export declare function finalizeAppRscResponse(response: Response, request: Request, options: FinalizeAppRscResponseOptions): Promise<Response>;
53
65
  //#endregion
@@ -1,3 +1,4 @@
1
+ import { preserveFullyBufferedBodyMetadata } from "./fully-buffered-response.js";
1
2
  import { hasBasePath, stripBasePath } from "../utils/base-path.js";
2
3
  import { VINEXT_RSC_VARY_HEADER } from "./app-rsc-vary.js";
3
4
  import { sanitizeMethodNotAllowedHeaders } from "./http-error-responses.js";
@@ -6,12 +7,14 @@ import { normalizeDefaultLocalePathname } from "./pages-i18n.js";
6
7
  import { isNonCacheableCacheControl } from "../shims/cdn-cache.js";
7
8
  import { captureRouteCacheabilityResponsePolicy } from "../shims/cacheability-classification.js";
8
9
  import { NO_STORE_CACHE_CONTROL, applyCdnResponseHeaders, captureCdnResponsePolicyHeaders, hasExplicitNonCacheableResponsePolicy, isCdnResponsePolicyHeader } from "./cache-control.js";
9
- import { hasPostConfigLinkHeaders } from "./app-response-header-provenance.js";
10
+ import { copyLinkHeaderProvenance, hasPostConfigLinkHeaders } from "./app-response-header-provenance.js";
10
11
  import { mergeVaryHeader } from "./middleware-response-headers.js";
11
12
  import "./app-rsc-cache-busting.js";
12
13
  //#region src/server/app-rsc-response-finalizer.ts
13
14
  const HAS_CONFIG_HEADERS = process.env.__VINEXT_HAS_CONFIG_HEADERS !== "false";
14
15
  const configHeadersAlreadyApplied = /* @__PURE__ */ new WeakSet();
16
+ const applicationResponses = /* @__PURE__ */ new WeakSet();
17
+ const routeHandlerResponses = /* @__PURE__ */ new WeakMap();
15
18
  function normalizeExplicitNonCacheablePolicy(headers) {
16
19
  if (!hasExplicitNonCacheableResponsePolicy(headers)) return;
17
20
  const cacheControl = headers.get("Cache-Control");
@@ -34,6 +37,39 @@ function markStreamedActionRedirectResponse(response) {
34
37
  function isStreamedActionRedirectResponse(response) {
35
38
  return streamedActionRedirects.has(response);
36
39
  }
40
+ /**
41
+ * Mark a response that a page, Route Handler, Server Action or Pages route
42
+ * produced. Next.js stages next.config headers before application code runs,
43
+ * so the redirects these return carry them. Redirects that routing answers
44
+ * itself (next.config redirects, trailing slash, middleware) go out without.
45
+ */
46
+ function markAppRscApplicationResponse(response) {
47
+ applicationResponses.add(response);
48
+ return response;
49
+ }
50
+ /** Mark a response a Route Handler produced, so its own headers meet config as in Next.js. */
51
+ function markAppRscRouteHandlerResponse(response, runtime) {
52
+ routeHandlerResponses.set(response, runtime);
53
+ return markAppRscApplicationResponse(response);
54
+ }
55
+ /** Response.redirect() creates immutable headers; copy them only when needed. */
56
+ function withMutableHeaders(response) {
57
+ const probe = "x-vinext-mutable-headers-probe";
58
+ const probeValue = response.headers.get(probe);
59
+ try {
60
+ if (probeValue === null) response.headers.delete(probe);
61
+ else response.headers.set(probe, probeValue);
62
+ return response;
63
+ } catch {
64
+ const headers = new Headers(response.headers);
65
+ copyLinkHeaderProvenance(response.headers, headers);
66
+ return preserveFullyBufferedBodyMetadata(response, new Response(response.body, {
67
+ headers,
68
+ status: response.status,
69
+ statusText: response.statusText
70
+ }));
71
+ }
72
+ }
37
73
  /** Apply only the matching next.config headers for an App Router request. */
38
74
  async function applyAppRscConfigHeaders(headers, request, options) {
39
75
  if (!HAS_CONFIG_HEADERS || !options.configHeaders.length) return;
@@ -57,6 +93,7 @@ async function applyAppRscConfigHeaders(headers, request, options) {
57
93
  appendToPostConfigLink: hasPostConfigLinkHeaders(headers),
58
94
  middlewareHeaders: options.middlewareHeaders,
59
95
  recordCacheability: options.recordCacheability,
96
+ routeHandlerRuntime: options.routeHandlerRuntime,
60
97
  overwriteExisting: options.overwriteExisting ?? isCdnResponsePolicyHeader
61
98
  });
62
99
  }
@@ -68,12 +105,16 @@ async function applyAppRscConfigHeaders(headers, request, options) {
68
105
  * handling, so that every response path (page, route handler, server action,
69
106
  * metadata, not-found) gets headers applied consistently.
70
107
  *
71
- * Skips 3xx redirect responses. Response.redirect() creates immutable
72
- * headers that throw on mutation, and Next.js does not apply config headers
73
- * to redirects regardless.
108
+ * Skips 3xx responses that routing answered itself, as Next.js does not
109
+ * apply config headers to those. A 3xx that application code returned is
110
+ * finalized like any other response.
74
111
  */
75
112
  async function finalizeAppRscResponse(response, request, options) {
76
- if (response.status >= 300 && response.status < 400) return response;
113
+ const routeHandlerRuntime = routeHandlerResponses.get(response);
114
+ if (response.status >= 300 && response.status < 400) {
115
+ if (!applicationResponses.has(response) || configHeadersAlreadyApplied.has(response)) return response;
116
+ response = withMutableHeaders(response);
117
+ }
77
118
  if (!isStaticFileSignal(response)) {
78
119
  const varyHeader = response.headers.get("Vary");
79
120
  if (varyHeader === null) response.headers.set("Vary", VINEXT_RSC_VARY_HEADER);
@@ -88,10 +129,13 @@ async function finalizeAppRscResponse(response, request, options) {
88
129
  normalizeExplicitNonCacheablePolicy(response.headers);
89
130
  return response;
90
131
  }
91
- await applyAppRscConfigHeaders(response.headers, request, options);
132
+ await applyAppRscConfigHeaders(response.headers, request, {
133
+ ...options,
134
+ routeHandlerRuntime
135
+ });
92
136
  normalizeExplicitNonCacheablePolicy(response.headers);
93
137
  if (response.status === 405 && response.headers.get("Allow") === "GET, HEAD") sanitizeMethodNotAllowedHeaders(response.headers, "GET, HEAD");
94
138
  return response;
95
139
  }
96
140
  //#endregion
97
- export { applyAppRscConfigHeaders, finalizeAppRscResponse, isStreamedActionRedirectResponse, markAppRscResponseConfigHeadersApplied, markStreamedActionRedirectResponse };
141
+ export { applyAppRscConfigHeaders, finalizeAppRscResponse, isStreamedActionRedirectResponse, markAppRscApplicationResponse, markAppRscResponseConfigHeadersApplied, markAppRscRouteHandlerResponse, markStreamedActionRedirectResponse };
@@ -32,6 +32,7 @@ import { createRenderStreamCancellation, isAppRenderAbortError } from "./app-ren
32
32
  import { runWithRootParamsScope } from "../shims/root-params.js";
33
33
  import { runWithNavigationContext } from "../shims/navigation-state.js";
34
34
  import { startCandidateSearchParamsGate } from "./app-ssr-search-params-gate.js";
35
+ import { isSrvxAbsoluteFormTarget } from "./srvx-request-target.js";
35
36
  import { withAppRouterTree } from "../shims/app-router-tree-context.js";
36
37
  import { BeforeInteractiveContext } from "../shims/before-interactive-context.js";
37
38
  import { renderBeforeInteractiveInlineScripts } from "./before-interactive-head.js";
@@ -230,6 +231,12 @@ async function handleSsr(rscStream, navContext, fontData, options) {
230
231
  fetchPriority: "low",
231
232
  nonce: options?.scriptNonce
232
233
  });
234
+ for (const moduleUrl of pagesClientAssets.appBootstrapPreloadModules ?? []) preloadModule(moduleUrl, {
235
+ as: "script",
236
+ crossOrigin: assetCrossOrigin,
237
+ fetchPriority: "low",
238
+ nonce: options?.scriptNonce
239
+ });
233
240
  if (pagesClientAssets.appRouteManifestModule) preloadModule(pagesClientAssets.appRouteManifestModule, {
234
241
  as: "script",
235
242
  crossOrigin: assetCrossOrigin,
@@ -420,6 +427,7 @@ async function handleSsr(rscStream, navContext, fontData, options) {
420
427
  });
421
428
  }
422
429
  var app_ssr_entry_default = { async fetch(request) {
430
+ if (isSrvxAbsoluteFormTarget(request)) return notFoundResponse();
423
431
  const url = new URL(request.url);
424
432
  const slashRedirect = repeatedSlashRedirectResponse(url.pathname + url.search);
425
433
  if (slashRedirect) return slashRedirect;
@@ -3,6 +3,7 @@ import { RequestContext } from "../config/request-context.js";
3
3
  import { BasePathMatchState } from "../config/config-matchers.js";
4
4
  import { HeaderRecord } from "./request-pipeline.js";
5
5
  //#region src/server/config-headers.d.ts
6
+ export type RouteHandlerRuntime = "node" | "edge";
6
7
  export type ResponseStageCachePolicyOptions = {
7
8
  basePathState?: BasePathMatchState;
8
9
  configHeaders: NextHeader[];
@@ -34,6 +35,12 @@ type ApplyConfigHeadersOptions = {
34
35
  middlewareHeaders?: Headers | null;
35
36
  /** Whether this composition participates in response-cache admission. */
36
37
  recordCacheability?: boolean;
38
+ /**
39
+ * Set when a Route Handler produced the response. Next.js copies a Node
40
+ * handler's headers only where config left none, and appends an edge
41
+ * handler's after config. Cache policy headers keep their own rules.
42
+ */
43
+ routeHandlerRuntime?: RouteHandlerRuntime;
37
44
  };
38
45
  /** Apply matched next.config.js headers to a Web Headers object. */
39
46
  export declare function applyConfigHeadersToResponse(responseHeaders: Headers, options: ApplyConfigHeadersOptions): void;
@@ -5,6 +5,15 @@ import { isCdnResponsePolicyHeader, isNonCacheableCdnResponsePolicy } from "./ca
5
5
  import { mergeVaryHeader } from "./middleware-response-headers.js";
6
6
  //#region src/server/config-headers.ts
7
7
  const ADDITIVE_CONFIG_HEADER_NAMES = /* @__PURE__ */ new Set(["set-cookie", "vary"]);
8
+ const ROUTE_HANDLER_APPENDED_HEADER_NAMES = /* @__PURE__ */ new Set(["www-authenticate", "proxy-authenticate"]);
9
+ /**
10
+ * Next.js sets x-nextjs-cache over config only on the responses its cache
11
+ * manages, which vinext marks with x-vinext-cache too. A Route Handler's own
12
+ * x-nextjs-cache meets config like any other header.
13
+ */
14
+ function isFrameworkCacheStateHeader(headers, lowerName) {
15
+ return lowerName === "X-Vinext-Cache".toLowerCase() || lowerName === "x-nextjs-cache" && headers.has("X-Vinext-Cache");
16
+ }
8
17
  /**
9
18
  * Resolve positive config cache policy that must accompany an inner artifact.
10
19
  * The request stage evaluates every condition. Shared stage transports must key
@@ -77,6 +86,7 @@ function applyConfigHeadersToResponse(responseHeaders, options) {
77
86
  const matched = retainLastSingularConfigValues(matchHeaders(options.pathname, options.configHeaders, options.requestContext, options.basePathState, options.recordCacheability === false ? void 0 : markConditionalConfigHeaderCacheability));
78
87
  if (options.recordCacheability !== false) markExplicitConfigResponseVeto(matched);
79
88
  const configCookies = [];
89
+ const configVary = [];
80
90
  for (const header of matched) {
81
91
  const lowerName = header.key.toLowerCase();
82
92
  if (lowerName === "x-vinext-special-error-status") continue;
@@ -86,9 +96,18 @@ function applyConfigHeadersToResponse(responseHeaders, options) {
86
96
  const postConfigLink = options.appendToPostConfigLink ? responseHeaders.get(lowerName) : null;
87
97
  responseHeaders.set(header.key, postConfigLink ? `${header.value}, ${postConfigLink}` : header.value);
88
98
  } else if (!ADDITIVE_CONFIG_HEADER_NAMES.has(lowerName) && options.middlewareHeaders?.has(lowerName)) continue;
89
- else if (lowerName === "vary") mergeVaryHeader(responseHeaders, header.value);
99
+ else if (lowerName === "vary") configVary.push(header.value);
90
100
  else if (ADDITIVE_CONFIG_HEADER_NAMES.has(lowerName)) responseHeaders.append(header.key, header.value);
91
101
  else if ((typeof options.overwriteExisting === "function" ? options.overwriteExisting(lowerName) : options.overwriteExisting?.has(lowerName)) || !responseHeaders.has(lowerName)) responseHeaders.set(header.key, header.value);
102
+ else if (options.routeHandlerRuntime && !isFrameworkCacheStateHeader(responseHeaders, lowerName) && !isCdnResponsePolicyHeader(lowerName)) {
103
+ const handlerValue = responseHeaders.get(lowerName);
104
+ responseHeaders.set(header.key, handlerValue !== header.value && (options.routeHandlerRuntime === "edge" || ROUTE_HANDLER_APPENDED_HEADER_NAMES.has(lowerName)) ? `${header.value}, ${handlerValue}` : header.value);
105
+ }
106
+ }
107
+ if (configVary.length > 0) {
108
+ const routeHandlerVary = options.routeHandlerRuntime ? responseHeaders.get("vary") : null;
109
+ if (routeHandlerVary) responseHeaders.delete("vary");
110
+ for (const value of [...configVary, ...routeHandlerVary ? [routeHandlerVary] : []]) mergeVaryHeader(responseHeaders, value);
92
111
  }
93
112
  if (configCookies.length > 0 && !options.middlewareHeaders?.get("set-cookie")) {
94
113
  const responseCookies = responseHeaders.getSetCookie();
@@ -0,0 +1,9 @@
1
+ import { ImageSourceReader } from "./image-source-reader.js";
2
+ //#region src/server/image-animation.d.ts
3
+ /**
4
+ * Whether a source is an animated GIF, PNG or WebP, judged by its signature.
5
+ * The source is parsed as it streams in, so only the bytes a format's check
6
+ * needs are read, and none are kept beyond what the reader holds to replay.
7
+ */
8
+ export declare function isAnimatedImage(source: ImageSourceReader): Promise<boolean>;
9
+ //#endregion
@@ -0,0 +1,118 @@
1
+ //#region src/server/image-animation.ts
2
+ const PNG_SIGNATURE = [
3
+ 137,
4
+ 80,
5
+ 78,
6
+ 71,
7
+ 13,
8
+ 10,
9
+ 26,
10
+ 10
11
+ ];
12
+ const WEBP_FOURCC = [
13
+ 87,
14
+ 69,
15
+ 66,
16
+ 80
17
+ ];
18
+ const WEBP_ANIM_CHUNK = [
19
+ 65,
20
+ 78,
21
+ 73,
22
+ 77
23
+ ];
24
+ function bytesAt(bytes, offset, expected) {
25
+ return expected.every((byte, index) => bytes[offset + index] === byte);
26
+ }
27
+ function ascii(bytes, start, end) {
28
+ return String.fromCharCode(...bytes.subarray(start, end));
29
+ }
30
+ /** Length of a run of GIF data sub-blocks, including its zero terminator. */
31
+ async function gifDataBlocksLength(source, offset) {
32
+ let length = 0;
33
+ for (;;) {
34
+ const size = await source.byte(offset + length) ?? 0;
35
+ if (size === 0) return length + 1;
36
+ length += size + 1;
37
+ }
38
+ }
39
+ async function isAnimatedGif(source, head) {
40
+ let hasColorTable = head[10] & 128;
41
+ let colorTableSize = head[10] & 7;
42
+ let offset = 13 + (hasColorTable ? 3 * 2 ** (colorTableSize + 1) : 0);
43
+ let frames = 0;
44
+ while (frames < 2) switch (await source.byte(offset) ?? 59) {
45
+ case 44: {
46
+ frames += 1;
47
+ const flags = await source.byte(offset + 9) ?? 0;
48
+ hasColorTable = flags & 128;
49
+ colorTableSize = flags & 7;
50
+ offset += 10 + (hasColorTable ? 3 * 2 ** (colorTableSize + 1) : 0);
51
+ offset += await gifDataBlocksLength(source, offset + 1) + 1;
52
+ break;
53
+ }
54
+ case 33:
55
+ offset += 2;
56
+ offset += await gifDataBlocksLength(source, offset);
57
+ break;
58
+ default: return false;
59
+ }
60
+ return true;
61
+ }
62
+ /** An APNG has an `acTL` chunk before its first `IDAT`, and `fdAT` frames after. */
63
+ async function isAnimatedPng(source) {
64
+ let hasAnimationControl = false;
65
+ let hasImageData = false;
66
+ let hasFrameData = false;
67
+ let previousChunk = null;
68
+ let offset = 8;
69
+ for (;;) {
70
+ const header = await source.bytes(offset, 8);
71
+ if (header.byteLength < 8) break;
72
+ const length = new DataView(header.buffer).getUint32(0);
73
+ const chunk = ascii(header, 4, 8);
74
+ switch (chunk) {
75
+ case "acTL":
76
+ hasAnimationControl = true;
77
+ break;
78
+ case "IDAT":
79
+ if (!hasAnimationControl) return false;
80
+ if (previousChunk !== "fcTL" && previousChunk !== "IDAT") return false;
81
+ hasImageData = true;
82
+ break;
83
+ case "fdAT":
84
+ if (!hasImageData) return false;
85
+ if (previousChunk !== "fcTL" && previousChunk !== "fdAT") return false;
86
+ hasFrameData = true;
87
+ }
88
+ previousChunk = chunk;
89
+ offset += 12 + length;
90
+ }
91
+ return hasAnimationControl && hasImageData && hasFrameData;
92
+ }
93
+ /** Bytes of a WebP source searched per read. */
94
+ const WEBP_SCAN_STEP = 65536;
95
+ /** Like is-animated, any `ANIM` in a WebP source marks it animated. */
96
+ async function isAnimatedWebp(source) {
97
+ const first = WEBP_ANIM_CHUNK[0];
98
+ const readLength = WEBP_SCAN_STEP + WEBP_ANIM_CHUNK.length - 1;
99
+ for (let start = 0;; start += WEBP_SCAN_STEP) {
100
+ const bytes = await source.bytes(start, readLength);
101
+ for (let offset = 0; offset < bytes.length; offset++) if (bytes[offset] === first && bytesAt(bytes, offset, WEBP_ANIM_CHUNK)) return true;
102
+ if (bytes.length < readLength) return false;
103
+ }
104
+ }
105
+ /**
106
+ * Whether a source is an animated GIF, PNG or WebP, judged by its signature.
107
+ * The source is parsed as it streams in, so only the bytes a format's check
108
+ * needs are read, and none are kept beyond what the reader holds to replay.
109
+ */
110
+ async function isAnimatedImage(source) {
111
+ const head = await source.bytes(0, 12);
112
+ if (ascii(head, 0, 3) === "GIF") return isAnimatedGif(source, head);
113
+ if (bytesAt(head, 0, PNG_SIGNATURE)) return isAnimatedPng(source);
114
+ if (bytesAt(head, 8, WEBP_FOURCC)) return isAnimatedWebp(source);
115
+ return false;
116
+ }
117
+ //#endregion
118
+ export { isAnimatedImage };