vinext 1.4.1 → 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 (143) hide show
  1. package/README.md +5 -2
  2. package/dist/build/action-owner-manifest.js +1 -0
  3. package/dist/build/prerender-paths.d.ts +11 -1
  4. package/dist/build/prerender-paths.js +36 -10
  5. package/dist/check.js +1 -1
  6. package/dist/cli.js +1 -1
  7. package/dist/client/dev-error-overlay.d.ts +2 -1
  8. package/dist/client/dev-error-overlay.js +8 -3
  9. package/dist/config/config-matchers.js +8 -4
  10. package/dist/config/destination-params.js +5 -2
  11. package/dist/config/next-config.d.ts +25 -1
  12. package/dist/config/next-config.js +31 -1
  13. package/dist/entries/app-rsc-entry.js +65 -30
  14. package/dist/entries/app-rsc-manifest.js +4 -0
  15. package/dist/entries/pages-server-entry.js +19 -4
  16. package/dist/index.js +37 -37
  17. package/dist/init-cloudflare.js +18 -2
  18. package/dist/init-platform.js +2 -2
  19. package/dist/init.js +42 -5
  20. package/dist/plugins/css-target.d.ts +18 -0
  21. package/dist/plugins/css-target.js +93 -0
  22. package/dist/routing/app-route-graph.d.ts +10 -0
  23. package/dist/routing/app-route-graph.js +15 -1
  24. package/dist/server/api-handler.d.ts +2 -0
  25. package/dist/server/api-handler.js +10 -8
  26. package/dist/server/app-action-forwarding.js +3 -1
  27. package/dist/server/app-browser-entry.js +37 -41
  28. package/dist/server/app-browser-error.d.ts +1 -1
  29. package/dist/server/app-browser-error.js +13 -4
  30. package/dist/server/app-browser-supplemental-refresh.d.ts +13 -0
  31. package/dist/server/app-browser-supplemental-refresh.js +36 -4
  32. package/dist/server/app-elements.d.ts +3 -0
  33. package/dist/server/app-elements.js +14 -1
  34. package/dist/server/app-fallback-renderer.js +2 -2
  35. package/dist/server/app-middleware.d.ts +2 -0
  36. package/dist/server/app-middleware.js +1 -0
  37. package/dist/server/app-optimistic-routing.d.ts +12 -1
  38. package/dist/server/app-optimistic-routing.js +38 -16
  39. package/dist/server/app-page-boundary-render.js +4 -2
  40. package/dist/server/app-page-cache-render.js +1 -1
  41. package/dist/server/app-page-dispatch.d.ts +11 -1
  42. package/dist/server/app-page-dispatch.js +78 -31
  43. package/dist/server/app-page-element-builder.d.ts +11 -20
  44. package/dist/server/app-page-element-builder.js +39 -50
  45. package/dist/server/app-page-intercept-segment-config.d.ts +56 -0
  46. package/dist/server/app-page-intercept-segment-config.js +90 -0
  47. package/dist/server/app-page-navigation-params.d.ts +23 -0
  48. package/dist/server/app-page-navigation-params.js +49 -0
  49. package/dist/server/app-page-render.d.ts +5 -2
  50. package/dist/server/app-page-render.js +17 -3
  51. package/dist/server/app-page-request.d.ts +24 -2
  52. package/dist/server/app-page-request.js +10 -10
  53. package/dist/server/app-page-route-wiring.d.ts +17 -5
  54. package/dist/server/app-page-route-wiring.js +19 -13
  55. package/dist/server/app-page-segment-plan.js +9 -1
  56. package/dist/server/app-page-segment-state.d.ts +9 -0
  57. package/dist/server/app-page-segment-state.js +54 -1
  58. package/dist/server/app-pages-bridge.d.ts +6 -0
  59. package/dist/server/app-pages-bridge.js +8 -3
  60. package/dist/server/app-request-stage-independent-entry.js +2 -3
  61. package/dist/server/app-response-stage-entry.js +1 -0
  62. package/dist/server/app-route-module-loader.d.ts +2 -0
  63. package/dist/server/app-route-module-loader.js +1 -0
  64. package/dist/server/app-router-entry.js +2 -3
  65. package/dist/server/app-rsc-cache-busting.d.ts +8 -0
  66. package/dist/server/app-rsc-cache-busting.js +13 -1
  67. package/dist/server/app-rsc-combined-handler.js +2 -1
  68. package/dist/server/app-rsc-handler.d.ts +14 -1
  69. package/dist/server/app-rsc-handler.js +111 -62
  70. package/dist/server/app-rsc-response-finalizer.d.ts +18 -3
  71. package/dist/server/app-rsc-response-finalizer.js +61 -7
  72. package/dist/server/app-rsc-route-matching.d.ts +14 -0
  73. package/dist/server/app-rsc-route-matching.js +42 -18
  74. package/dist/server/app-server-action-execution.js +16 -10
  75. package/dist/server/app-ssr-entry.js +8 -0
  76. package/dist/server/cache-control.d.ts +2 -0
  77. package/dist/server/cache-control.js +3 -1
  78. package/dist/server/config-headers.d.ts +7 -0
  79. package/dist/server/config-headers.js +20 -1
  80. package/dist/server/image-animation.d.ts +9 -0
  81. package/dist/server/image-animation.js +118 -0
  82. package/dist/server/image-local-patterns.d.ts +21 -0
  83. package/dist/server/image-local-patterns.js +59 -0
  84. package/dist/server/image-optimization.d.ts +48 -27
  85. package/dist/server/image-optimization.js +316 -22
  86. package/dist/server/image-source-reader.d.ts +34 -0
  87. package/dist/server/image-source-reader.js +126 -0
  88. package/dist/server/metadata-route-response.d.ts +6 -1
  89. package/dist/server/metadata-route-response.js +46 -10
  90. package/dist/server/middleware-matcher.d.ts +1 -0
  91. package/dist/server/middleware-matcher.js +1 -1
  92. package/dist/server/middleware-runtime.d.ts +5 -0
  93. package/dist/server/middleware-runtime.js +48 -15
  94. package/dist/server/middleware.d.ts +1 -1
  95. package/dist/server/middleware.js +2 -1
  96. package/dist/server/nitro-public-files-plugin.js +20 -8
  97. package/dist/server/nitro-request-target-plugin.d.ts +17 -0
  98. package/dist/server/nitro-request-target-plugin.js +22 -0
  99. package/dist/server/open-redirect.d.ts +2 -1
  100. package/dist/server/open-redirect.js +2 -1
  101. package/dist/server/pages-client-assets.d.ts +1 -0
  102. package/dist/server/pages-data-route.d.ts +5 -1
  103. package/dist/server/pages-data-route.js +14 -6
  104. package/dist/server/pages-i18n.d.ts +7 -0
  105. package/dist/server/pages-i18n.js +14 -1
  106. package/dist/server/pages-node-compat.js +7 -3
  107. package/dist/server/pages-page-handler.js +4 -4
  108. package/dist/server/pages-request-pipeline.d.ts +9 -0
  109. package/dist/server/pages-request-pipeline.js +35 -15
  110. package/dist/server/pages-request-stage-entry.js +8 -2
  111. package/dist/server/pages-router-entry.js +3 -0
  112. package/dist/server/prod-server.d.ts +1 -1
  113. package/dist/server/prod-server.js +64 -62
  114. package/dist/server/request-pipeline.d.ts +5 -0
  115. package/dist/server/request-pipeline.js +11 -1
  116. package/dist/server/response-stage-cacheability.d.ts +7 -0
  117. package/dist/server/response-stage-cacheability.js +6 -5
  118. package/dist/server/srvx-middleware-origin.d.ts +13 -0
  119. package/dist/server/srvx-middleware-origin.js +19 -0
  120. package/dist/server/srvx-request-target.d.ts +12 -0
  121. package/dist/server/srvx-request-target.js +20 -0
  122. package/dist/server/static-file-cache.js +10 -22
  123. package/dist/server/worker-image-optimization.d.ts +10 -3
  124. package/dist/server/worker-image-optimization.js +43 -9
  125. package/dist/server/worker-revalidation-context.js +1 -0
  126. package/dist/server/worker-utils.d.ts +8 -0
  127. package/dist/server/worker-utils.js +19 -2
  128. package/dist/shims/error-boundary.d.ts +6 -2
  129. package/dist/shims/error-boundary.js +4 -2
  130. package/dist/shims/image-props.d.ts +6 -0
  131. package/dist/shims/image-props.js +39 -3
  132. package/dist/shims/image.js +3 -3
  133. package/dist/shims/request-context.d.ts +3 -1
  134. package/dist/shims/slot.d.ts +3 -0
  135. package/dist/shims/slot.js +67 -9
  136. package/dist/shims/url-utils.js +6 -1
  137. package/dist/utils/base-path.d.ts +7 -0
  138. package/dist/utils/base-path.js +11 -1
  139. package/dist/utils/client-entry-manifest.d.ts +5 -0
  140. package/dist/utils/client-entry-manifest.js +1 -0
  141. package/dist/utils/client-runtime-metadata.d.ts +5 -0
  142. package/dist/utils/client-runtime-metadata.js +33 -7
  143. package/package.json +1 -1
@@ -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");
@@ -25,6 +28,48 @@ function markAppRscResponseConfigHeadersApplied(response) {
25
28
  configHeadersAlreadyApplied.add(response);
26
29
  return response;
27
30
  }
31
+ const streamedActionRedirects = /* @__PURE__ */ new WeakSet();
32
+ /** Mark a Server Action redirect that streams its target's render, and so keeps its policy. */
33
+ function markStreamedActionRedirectResponse(response) {
34
+ streamedActionRedirects.add(response);
35
+ return response;
36
+ }
37
+ function isStreamedActionRedirectResponse(response) {
38
+ return streamedActionRedirects.has(response);
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
+ }
28
73
  /** Apply only the matching next.config headers for an App Router request. */
29
74
  async function applyAppRscConfigHeaders(headers, request, options) {
30
75
  if (!HAS_CONFIG_HEADERS || !options.configHeaders.length) return;
@@ -48,6 +93,7 @@ async function applyAppRscConfigHeaders(headers, request, options) {
48
93
  appendToPostConfigLink: hasPostConfigLinkHeaders(headers),
49
94
  middlewareHeaders: options.middlewareHeaders,
50
95
  recordCacheability: options.recordCacheability,
96
+ routeHandlerRuntime: options.routeHandlerRuntime,
51
97
  overwriteExisting: options.overwriteExisting ?? isCdnResponsePolicyHeader
52
98
  });
53
99
  }
@@ -59,12 +105,16 @@ async function applyAppRscConfigHeaders(headers, request, options) {
59
105
  * handling, so that every response path (page, route handler, server action,
60
106
  * metadata, not-found) gets headers applied consistently.
61
107
  *
62
- * Skips 3xx redirect responses. Response.redirect() creates immutable
63
- * headers that throw on mutation, and Next.js does not apply config headers
64
- * 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.
65
111
  */
66
112
  async function finalizeAppRscResponse(response, request, options) {
67
- 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
+ }
68
118
  if (!isStaticFileSignal(response)) {
69
119
  const varyHeader = response.headers.get("Vary");
70
120
  if (varyHeader === null) response.headers.set("Vary", VINEXT_RSC_VARY_HEADER);
@@ -73,15 +123,19 @@ async function finalizeAppRscResponse(response, request, options) {
73
123
  if (!response.headers.has("Cache-Control")) {
74
124
  applyCdnResponseHeaders(response.headers, { cacheControl: "" });
75
125
  captureRouteCacheabilityResponsePolicy(captureCdnResponsePolicyHeaders(response.headers));
126
+ if (isStaticFileSignal(response)) response.headers.delete("Cache-Control");
76
127
  }
77
128
  if (configHeadersAlreadyApplied.has(response)) {
78
129
  normalizeExplicitNonCacheablePolicy(response.headers);
79
130
  return response;
80
131
  }
81
- await applyAppRscConfigHeaders(response.headers, request, options);
132
+ await applyAppRscConfigHeaders(response.headers, request, {
133
+ ...options,
134
+ routeHandlerRuntime
135
+ });
82
136
  normalizeExplicitNonCacheablePolicy(response.headers);
83
137
  if (response.status === 405 && response.headers.get("Allow") === "GET, HEAD") sanitizeMethodNotAllowedHeaders(response.headers, "GET, HEAD");
84
138
  return response;
85
139
  }
86
140
  //#endregion
87
- export { applyAppRscConfigHeaders, finalizeAppRscResponse, markAppRscResponseConfigHeadersApplied };
141
+ export { applyAppRscConfigHeaders, finalizeAppRscResponse, isStreamedActionRedirectResponse, markAppRscApplicationResponse, markAppRscResponseConfigHeadersApplied, markAppRscRouteHandlerResponse, markStreamedActionRedirectResponse };
@@ -101,6 +101,7 @@ type AppRscRouteForMatching = {
101
101
  pattern: string;
102
102
  patternParts: string[];
103
103
  routeHandler?: unknown;
104
+ routeSegments?: readonly string[] | null;
104
105
  slots?: Record<string, AppRscSlotForMatching>;
105
106
  siblingIntercepts?: AppRscSiblingInterceptForMatching[];
106
107
  };
@@ -108,6 +109,14 @@ type AppRscInterceptMatch = AppRscInterceptLookupEntry & {
108
109
  matchedParams: AppRscRouteParams;
109
110
  sourceRouteIsConcrete: boolean;
110
111
  sourceMatchedParams: AppRscRouteParams;
112
+ /**
113
+ * The source route and params the request's interception context names,
114
+ * which the request handler authorizes. They differ from the rendered
115
+ * `sourceRouteIndex` only for a sibling intercept opened from a deeper
116
+ * source, which renders the intercepting route itself.
117
+ */
118
+ authorizationSourceRouteIndex: number;
119
+ authorizationSourceParams: AppRscRouteParams;
111
120
  };
112
121
  type AppRscInterceptLoadState = {
113
122
  page: unknown;
@@ -120,6 +129,11 @@ type AppRscInterceptLookupEntry = {
120
129
  interceptionId: string | null;
121
130
  interceptionGraphId: string | null;
122
131
  sourceRouteIndex: number;
132
+ /**
133
+ * A sibling intercept whose owner route's page sits in the directory of the
134
+ * interception marker, so the owner is the intercepting route itself.
135
+ */
136
+ ownerIsInterceptingRoute: boolean;
123
137
  slotKey: string;
124
138
  targetPattern: string;
125
139
  targetPatternParts: string[];
@@ -1,7 +1,8 @@
1
- import { decodeMatchedParams, decodeRouteSegment, splitPathSegments, splitPathnameForRouteMatch } from "../routing/utils.js";
1
+ import { decodeMatchedParams, decodeRouteSegment, splitPathSegments } from "../routing/utils.js";
2
2
  import { createAppRouteGraphInterceptionId } from "../routing/app-route-ids.js";
3
3
  import { buildRouteTrie, trieMatchRaw } from "../routing/route-trie.js";
4
4
  import { extractRawRoutePatternParams, matchRoutePatternPrefix, matchRoutePatternRaw } from "../routing/route-pattern.js";
5
+ import { getRenderedConcreteUrlPathsForRoute } from "./pregenerated-concrete-paths.js";
5
6
  import { canonicalizeAppPageParams } from "./app-page-segment-state.js";
6
7
  //#region src/server/app-rsc-route-matching.ts
7
8
  /**
@@ -13,10 +14,19 @@ const SIBLING_PAGE_INTERCEPT_SLOT_KEY = "__vinext_page_intercept";
13
14
  function createRouteParams() {
14
15
  return Object.create(null);
15
16
  }
16
- function appRscPathnameParts(pathname, isNormalized = false) {
17
+ /**
18
+ * Whether each encoded raw segment is exactly the spelling the URL parser, as
19
+ * a browser runs it, gives its decoded form: `caf%C3%A9` for `café`, but `q&a`
20
+ * for `q&a`. `%61dmin`, `caf%c3%a9`, `%63af%C3%A9` and `q%26a` are other
21
+ * spellings of a page, which middleware never compared as its pathname.
22
+ */
23
+ function isCanonicalPathEncoding(rawParts, decodedParts) {
24
+ return rawParts.every((rawPart, index) => !rawPart.includes("%") || rawPart === new URL(`/${decodedParts[index]}`, "http://n").pathname.slice(1));
25
+ }
26
+ function appRscPathnameParts(pathname) {
17
27
  const pathOnly = pathname.split("?")[0];
18
28
  const normalizedPathname = pathOnly === "/" ? "/" : pathOnly.replace(/\/$/, "");
19
- return isNormalized ? splitPathSegments(normalizedPathname) : splitPathnameForRouteMatch(normalizedPathname);
29
+ return splitPathSegments(normalizedPathname);
20
30
  }
21
31
  function isAppRouteHandlerRoute(route) {
22
32
  return route.routeHandler != null || typeof route.__loadRouteHandler === "function";
@@ -49,8 +59,10 @@ function createAppRscRouteMatcher(routes, decodedPathnamePatterns = []) {
49
59
  const result = matchRawRequestParts(rawParts);
50
60
  if (!result) return null;
51
61
  if (decodedPathnameRoutes.size > 0 && result.route.patternParts.some((part) => part.startsWith(":")) && !isAppRouteHandlerRoute(result.route) && rawParts.some((part) => part.includes("%"))) {
52
- const route = decodedPathnameRoutes.get(rawParts.map(decodeRouteSegment).join("/"));
53
- if (route && route !== result.route) return {
62
+ const decodedParts = rawParts.map(decodeRouteSegment);
63
+ const decodedPathname = decodedParts.join("/");
64
+ const route = decodedPathnameRoutes.get(decodedPathname);
65
+ if (route && route !== result.route && (isCanonicalPathEncoding(rawParts, decodedParts) || getRenderedConcreteUrlPathsForRoute(route.pattern)?.has(`/${decodedPathname}`) === true)) return {
54
66
  route,
55
67
  params: createRouteParams()
56
68
  };
@@ -62,23 +74,18 @@ function createAppRscRouteMatcher(routes, decodedPathnamePatterns = []) {
62
74
  return interceptionIds.has(interceptionId);
63
75
  },
64
76
  matchRoute(url) {
65
- const rawParts = appRscPathnameParts(url, true);
66
- const result = trieMatchRaw(routeTrie, appRscPathnameParts(url, false));
67
- if (!result) return null;
68
- result.params = extractRawRoutePatternParams(result.route.patternParts, rawParts);
69
- normalizeMatchedParamsForRoute(result);
70
- return result;
77
+ return matchRequestParts(appRscPathnameParts(url));
71
78
  },
72
79
  matchRequestRoute(url) {
73
- return matchRequestParts(appRscPathnameParts(url, true));
80
+ return matchRequestParts(appRscPathnameParts(url));
74
81
  },
75
82
  matchRawRequestRoute(url) {
76
- return matchRawRequestParts(appRscPathnameParts(url, true));
83
+ return matchRawRequestParts(appRscPathnameParts(url));
77
84
  },
78
85
  findIntercept(pathname, sourcePathname = null, interceptionId = null) {
79
86
  if (sourcePathname === null) return null;
80
- const urlParts = appRscPathnameParts(pathname, true);
81
- const sourceParts = appRscPathnameParts(sourcePathname, true);
87
+ const urlParts = appRscPathnameParts(pathname);
88
+ const sourceParts = appRscPathnameParts(sourcePathname);
82
89
  const matchedSourceRoute = matchRawRequestParts(sourceParts);
83
90
  for (const entry of interceptLookup) {
84
91
  if (interceptionId !== null && entry.interceptionId !== interceptionId) continue;
@@ -92,15 +99,21 @@ function createAppRscRouteMatcher(routes, decodedPathnamePatterns = []) {
92
99
  if (sourceRoute && isAppRouteHandlerRoute(sourceRoute)) continue;
93
100
  const matchedSourceParams = concreteSourceRoute ? concreteSourceRoute.params : sourceRoute ? matchSlotOwnerSourceParams(sourceParts, sourceRoute.patternParts, entry.sourceMatchPatternParts !== null) : null;
94
101
  if (!concreteSourceRoute && matchedSourceParams) canonicalizeAppPageParams(matchedSourceParams);
102
+ const ownerRoute = routes[entry.sourceRouteIndex];
103
+ const rendersOwnerRoute = entry.ownerIsInterceptingRoute && concreteSourceRouteIndex !== entry.sourceRouteIndex && ownerRoute !== void 0;
104
+ const renderedSourceParams = rendersOwnerRoute ? matchSlotOwnerSourceParams(sourceParts, ownerRoute.patternParts, true) : matchedSourceParams;
105
+ if (rendersOwnerRoute && renderedSourceParams) canonicalizeAppPageParams(renderedSourceParams);
95
106
  if (matchedSourceParams === null && entry.sourceMatchPatternParts === null) continue;
96
- const sourceParams = matchedSourceParams && entry.sourceMatchPatternParts !== null ? pickPatternParams(matchedSourceParams, entry.sourceMatchPatternParts) : matchedSourceParams ?? createRouteParams();
107
+ const sourceParams = renderedSourceParams && entry.sourceMatchPatternParts !== null ? pickPatternParams(renderedSourceParams, entry.sourceMatchPatternParts) : renderedSourceParams ?? createRouteParams();
97
108
  return {
98
109
  ...entry,
99
110
  page: entry.__loadState.page,
100
111
  sourceRouteIsConcrete: concreteSourceRoute !== null,
101
- sourceRouteIndex: concreteSourceRouteIndex,
112
+ sourceRouteIndex: rendersOwnerRoute ? entry.sourceRouteIndex : concreteSourceRouteIndex,
102
113
  matchedParams: mergeMatchedParams(sourceParams, params),
103
- sourceMatchedParams: matchedSourceParams ?? createRouteParams()
114
+ sourceMatchedParams: renderedSourceParams ?? createRouteParams(),
115
+ authorizationSourceRouteIndex: concreteSourceRouteIndex,
116
+ authorizationSourceParams: matchedSourceParams ?? createRouteParams()
104
117
  };
105
118
  }
106
119
  return null;
@@ -108,6 +121,15 @@ function createAppRscRouteMatcher(routes, decodedPathnamePatterns = []) {
108
121
  };
109
122
  }
110
123
  /**
124
+ * Whether a sibling intercept's owner route is the intercepting route: its
125
+ * page lives in the directory that holds the interception marker. An owner
126
+ * found elsewhere stands in for a marker directory without a page.
127
+ */
128
+ function isInterceptingRoute(routeSegments, sourcePageSegments) {
129
+ if (!routeSegments || !sourcePageSegments) return false;
130
+ return sourcePageSegments.findIndex((segment) => /^\(\.+\)/.test(segment)) === routeSegments.length && routeSegments.every((segment, index) => segment === sourcePageSegments[index]);
131
+ }
132
+ /**
111
133
  * Params for the slot owner when interception falls back to it instead of a
112
134
  * concrete descendant source route. The owner is what renders, and
113
135
  * `matchInterceptRoute` reads the promoted route's params solely from these,
@@ -195,6 +217,7 @@ function createInterceptLookup(routes) {
195
217
  interceptionId: typeof slotModule.id === "string" && sourceMatchPattern !== null ? createAppRouteGraphInterceptionId(slotModule.id, sourceMatchPattern, intercept.targetPattern) : null,
196
218
  interceptionGraphId: null,
197
219
  sourceRouteIndex: ownerRouteIndex,
220
+ ownerIsInterceptingRoute: false,
198
221
  slotKey,
199
222
  slotId: typeof slotModule.id === "string" ? slotModule.id : null,
200
223
  targetRouteGraphId: resolveTargetRouteGraphId(intercept.targetPattern),
@@ -246,6 +269,7 @@ function createInterceptLookup(routes) {
246
269
  interceptionId: typeof intercept.id === "string" ? intercept.id : null,
247
270
  interceptionGraphId: typeof intercept.id === "string" ? intercept.id : null,
248
271
  sourceRouteIndex: routeIndex,
272
+ ownerIsInterceptingRoute: !isAppRouteHandlerRoute(route) && isInterceptingRoute(route.routeSegments, intercept.sourcePageSegments),
249
273
  slotKey: SIBLING_PAGE_INTERCEPT_SLOT_KEY,
250
274
  slotId: typeof intercept.slotId === "string" ? intercept.slotId : null,
251
275
  targetRouteGraphId: resolveTargetRouteGraphId(intercept.targetPattern),
@@ -12,6 +12,7 @@ import { attachRequestCfMetadata, validateCsrfOrigin, validateServerActionPayloa
12
12
  import { splitPathSegments } from "../routing/utils.js";
13
13
  import { getRequestExecutionContext } from "../shims/request-context.js";
14
14
  import { logRequestError } from "./request-error-log.js";
15
+ import { SERVER_ACTION_CACHE_CONTROL, applyCdnResponseHeaders } from "./cache-control.js";
15
16
  import { shareIsolatePromise } from "./isolate-shared-promise.js";
16
17
  import { APP_RSC_RENDER_MODE_NAVIGATION } from "./app-rsc-render-mode.js";
17
18
  import { isDraftModeRequest, setHeadersContext } from "../shims/headers.js";
@@ -21,17 +22,18 @@ import { readStreamAsTextWithLimit } from "../utils/text-stream.js";
21
22
  import { mergeMiddlewareResponseHeaders } from "./middleware-response-headers.js";
22
23
  import { VINEXT_RSC_CONTENT_TYPE, applyRscCompatibilityIdHeader, createRscRequestUrl, stripRscCacheBustingSearchParam } from "./app-rsc-cache-busting.js";
23
24
  import { getSetCookieName } from "./cookie-utils.js";
25
+ import { markAppRscResponseConfigHeadersApplied, markStreamedActionRedirectResponse } from "./app-rsc-response-finalizer.js";
24
26
  import { createServerActionNotFoundResponse, getServerActionNotFoundMessage, isServerActionNotFoundError } from "./server-action-not-found.js";
25
27
  import { applyEdgeRuntimeHeader } from "./app-page-response.js";
26
28
  import { getNextErrorDigest, parseNextHttpErrorDigest, parseNextRedirectDigest } from "./next-error-digest.js";
27
- import { hasDigest } from "./app-rsc-errors.js";
28
- import { resolveAppPageNavigationParams } from "./app-page-element-builder.js";
29
+ import { resolveAppPageNavigationParams } from "./app-page-navigation-params.js";
29
30
  import "./app-page-stream.js";
31
+ import { hasDigest } from "./app-rsc-errors.js";
30
32
  import { buildAppPageTags } from "./implicit-tags.js";
33
+ import { resolveAppPageInterceptSegmentConfig } from "./app-page-intercept-segment-config.js";
31
34
  import { resolveAppPageActionRerenderTarget } from "./app-page-request.js";
32
35
  import { createRootParamsUsageController, runWithRootParamsUsage } from "../shims/root-params.js";
33
36
  import { createStaticGenerationHeadersContext } from "./app-static-generation.js";
34
- import { markAppRscResponseConfigHeadersApplied } from "./app-rsc-response-finalizer.js";
35
37
  //#region src/server/app-server-action-execution.ts
36
38
  /**
37
39
  * Run a server-reference load or decode outside every request scope.
@@ -659,7 +661,7 @@ async function handleProgressiveServerActionRequest(options) {
659
661
  options.clearRequestContext();
660
662
  const headers = new Headers();
661
663
  mergeMiddlewareResponseHeaders(headers, options.middlewareHeaders);
662
- headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
664
+ headers.set("Cache-Control", SERVER_ACTION_CACHE_CONTROL);
663
665
  for (const cookie of actionPendingCookies) headers.append("Set-Cookie", cookie);
664
666
  if (actionDraftCookie) headers.append("Set-Cookie", actionDraftCookie);
665
667
  return internalServerErrorResponse(process.env.NODE_ENV === "production" ? void 0 : "Server action failed: " + getServerActionFailureMessage(actionError), { headers });
@@ -879,6 +881,7 @@ async function handleServerActionRscRequest(options) {
879
881
  applyEdgeRuntimeHeader(redirectHeaders, options.isEdgeRuntime);
880
882
  mergeActionRedirectSourceHeaders(redirectHeaders, options.sourceConfigHeaders ?? null);
881
883
  mergeActionRedirectSourceHeaders(redirectHeaders, options.middlewareHeaders);
884
+ applyCdnResponseHeaders(redirectHeaders, { cacheControl: SERVER_ACTION_CACHE_CONTROL });
882
885
  applyRscCompatibilityIdHeader(redirectHeaders);
883
886
  const actionRedirectUrl = applyActionRedirectBasePath(actionRedirect.url, options.basePath ?? "");
884
887
  redirectHeaders.set(ACTION_REDIRECT_HEADER, actionRedirectUrl);
@@ -938,10 +941,10 @@ async function handleServerActionRscRequest(options) {
938
941
  targetPathname,
939
942
  targetRoute: targetMatch?.route ?? null
940
943
  }) ? 200 : 303;
941
- return markAppRscResponseConfigHeadersApplied(createServerActionRscResponse(targetResponse.body, {
944
+ return markStreamedActionRedirectResponse(markAppRscResponseConfigHeadersApplied(createServerActionRscResponse(targetResponse.body, {
942
945
  status: redirectResponseStatus,
943
946
  headers: redirectHeaders
944
- }, options.clearRequestContext));
947
+ }, options.clearRequestContext)));
945
948
  }
946
949
  const actionPendingCookies = dedupePendingCookies(options.getAndClearPendingCookies());
947
950
  const actionDraftCookie = options.getDraftModeCookieHeader();
@@ -976,6 +979,7 @@ async function handleServerActionRscRequest(options) {
976
979
  let element;
977
980
  let errorPattern = match ? match.route.pattern : options.cleanPathname;
978
981
  let renderedPathAndSearch = null;
982
+ let actionRerenderIsEdgeRuntime = options.isEdgeRuntime;
979
983
  const actionRerenderIsRscRequest = true;
980
984
  if (match) {
981
985
  const { route: actionRoute, params: actionParams } = match;
@@ -991,7 +995,9 @@ async function handleServerActionRscRequest(options) {
991
995
  });
992
996
  const resolvedActionNavigationParams = resolveAppPageNavigationParams(actionRerenderTarget.route, actionRerenderTarget.navigationParams, options.cleanPathname, actionRerenderTarget.interceptOpts);
993
997
  await options.ensureRouteLoaded?.(actionRerenderTarget.route);
994
- const actionRerenderDynamicConfig = options.resolveRouteDynamicConfig?.(actionRerenderTarget.route);
998
+ const actionInterceptConfig = actionRerenderTarget.interceptOpts ? resolveAppPageInterceptSegmentConfig(actionRerenderTarget.route, actionRerenderTarget.interceptOpts) : null;
999
+ if (actionInterceptConfig) actionRerenderIsEdgeRuntime = actionInterceptConfig.isEdgeRuntime;
1000
+ const actionRerenderDynamicConfig = actionInterceptConfig ? actionInterceptConfig.dynamicConfig : options.resolveRouteDynamicConfig?.(actionRerenderTarget.route);
995
1001
  const actionRerenderSearchParams = prepareActionPageRerenderContext({
996
1002
  draftModeCookie: actionDraftCookie,
997
1003
  draftModeSecret: options.draftModeSecret,
@@ -1007,8 +1013,8 @@ async function handleServerActionRscRequest(options) {
1007
1013
  });
1008
1014
  const renderedSearch = options.searchParams.toString();
1009
1015
  renderedPathAndSearch = renderedSearch ? `${options.cleanPathname}?${renderedSearch}` : options.cleanPathname;
1010
- setCurrentFetchCacheMode(options.resolveRouteFetchCacheMode?.(actionRerenderTarget.route) ?? null);
1011
- setCurrentFetchRevalidate(options.resolveRouteRevalidateSeconds?.(actionRerenderTarget.route) ?? null);
1016
+ setCurrentFetchCacheMode(actionInterceptConfig ? actionInterceptConfig.fetchCacheMode : options.resolveRouteFetchCacheMode?.(actionRerenderTarget.route) ?? null);
1017
+ setCurrentFetchRevalidate(actionInterceptConfig ? actionInterceptConfig.revalidateSeconds : options.resolveRouteRevalidateSeconds?.(actionRerenderTarget.route) ?? null);
1012
1018
  setCurrentForceDynamicFetchDefault(actionRerenderDynamicConfig === "force-dynamic");
1013
1019
  setCurrentFetchSoftTags(buildServerActionPageTags(actionRerenderTarget.route, options.cleanPathname));
1014
1020
  const buildActionRerenderElement = () => options.buildPageElement({
@@ -1047,7 +1053,7 @@ async function handleServerActionRscRequest(options) {
1047
1053
  "Content-Type": VINEXT_RSC_CONTENT_TYPE,
1048
1054
  Vary: VINEXT_RSC_VARY_HEADER
1049
1055
  });
1050
- applyEdgeRuntimeHeader(actionHeaders, options.isEdgeRuntime);
1056
+ applyEdgeRuntimeHeader(actionHeaders, actionRerenderIsEdgeRuntime);
1051
1057
  mergeMiddlewareResponseHeaders(actionHeaders, options.middlewareHeaders);
1052
1058
  applyRscCompatibilityIdHeader(actionHeaders);
1053
1059
  setActionRevalidatedHeader(actionHeaders, actionRevalidationKind);
@@ -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;
@@ -1,6 +1,8 @@
1
1
  import { CdnCacheableHeaderInput, isNonCacheableCacheControl } from "../shims/cdn-cache.js";
2
2
  //#region src/server/cache-control.d.ts
3
3
  export declare const NEVER_CACHE_CONTROL = "private, no-cache, no-store, max-age=0, must-revalidate";
4
+ /** The policy Next.js sends on every Server Action response. */
5
+ export declare const SERVER_ACTION_CACHE_CONTROL = "no-cache, no-store, max-age=0, must-revalidate";
4
6
  export declare const BROWSER_REVALIDATE_CACHE_CONTROL = "public, max-age=0, must-revalidate";
5
7
  export declare const STATIC_CACHE_CONTROL = "s-maxage=31536000, stale-while-revalidate";
6
8
  export declare const NO_STORE_CACHE_CONTROL = "no-store, must-revalidate";
@@ -2,6 +2,8 @@ import { getCdnCacheAdapter, isNonCacheableCacheControl } from "../shims/cdn-cac
2
2
  import { recordRouteCacheabilityCdnTags } from "../shims/cacheability-classification.js";
3
3
  //#region src/server/cache-control.ts
4
4
  const NEVER_CACHE_CONTROL = "private, no-cache, no-store, max-age=0, must-revalidate";
5
+ /** The policy Next.js sends on every Server Action response. */
6
+ const SERVER_ACTION_CACHE_CONTROL = "no-cache, no-store, max-age=0, must-revalidate";
5
7
  const BROWSER_REVALIDATE_CACHE_CONTROL = "public, max-age=0, must-revalidate";
6
8
  const STATIC_CACHE_CONTROL = "s-maxage=31536000, stale-while-revalidate";
7
9
  const STALE_REVALIDATE_CACHE_CONTROL = "s-maxage=0, stale-while-revalidate";
@@ -167,4 +169,4 @@ function buildCachedRevalidateCacheControl(cacheState, revalidateSeconds, expire
167
169
  return buildRevalidateCacheControl(revalidateSeconds, expireSeconds);
168
170
  }
169
171
  //#endregion
170
- export { BROWSER_REVALIDATE_CACHE_CONTROL, NEVER_CACHE_CONTROL, NO_STORE_CACHE_CONTROL, STATIC_CACHE_CONTROL, applyCdnResponseBuildIdentityHeaders, applyCdnResponseHeaders, applyCdnResponseIdentityHeaders, buildCachedRevalidateCacheControl, buildRevalidateCacheControl, captureCdnResponsePolicyHeaders, captureCdnResponsePolicyOverrides, hasCdnResponsePolicy, hasExplicitNonCacheableResponsePolicy, isCdnResponsePolicyHeader, isNonCacheableCacheControl, isNonCacheableCdnResponsePolicy, readCdnResponseCacheControl, readCdnResponsePolicyHeaderName, reconcileCdnResponseHeadersAfterOuterPolicy, shouldUseNextDeployCacheControl, validateCdnRequest };
172
+ export { BROWSER_REVALIDATE_CACHE_CONTROL, NEVER_CACHE_CONTROL, NO_STORE_CACHE_CONTROL, SERVER_ACTION_CACHE_CONTROL, STATIC_CACHE_CONTROL, applyCdnResponseBuildIdentityHeaders, applyCdnResponseHeaders, applyCdnResponseIdentityHeaders, buildCachedRevalidateCacheControl, buildRevalidateCacheControl, captureCdnResponsePolicyHeaders, captureCdnResponsePolicyOverrides, hasCdnResponsePolicy, hasExplicitNonCacheableResponsePolicy, isCdnResponsePolicyHeader, isNonCacheableCacheControl, isNonCacheableCdnResponsePolicy, readCdnResponseCacheControl, readCdnResponsePolicyHeaderName, reconcileCdnResponseHeadersAfterOuterPolicy, shouldUseNextDeployCacheControl, validateCdnRequest };
@@ -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 };