@thomas.f/prnext 0.1.0-alpha.1

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 (211) hide show
  1. package/README.md +54 -0
  2. package/build/action-exports.mjs +107 -0
  3. package/build/actions.mjs +214 -0
  4. package/build/app-config.mjs +138 -0
  5. package/build/app-routing.mjs +119 -0
  6. package/build/app-static.mjs +158 -0
  7. package/build/app.mjs +536 -0
  8. package/build/cache-components.mjs +91 -0
  9. package/build/cache-handlers.mjs +54 -0
  10. package/build/check.mjs +40 -0
  11. package/build/compiler.mjs +222 -0
  12. package/build/config.mjs +193 -0
  13. package/build/css-modules.mjs +114 -0
  14. package/build/custom-routes.mjs +363 -0
  15. package/build/dev-worker.mjs +18 -0
  16. package/build/dev.mjs +76 -0
  17. package/build/directory-cache.mjs +29 -0
  18. package/build/dynamic.mjs +125 -0
  19. package/build/edge.mjs +141 -0
  20. package/build/font-data.LICENSE.txt +25 -0
  21. package/build/font-data.json +1 -0
  22. package/build/font-files.mjs +221 -0
  23. package/build/fonts.mjs +131 -0
  24. package/build/framework-imports.mjs +17 -0
  25. package/build/i18n.mjs +39 -0
  26. package/build/images-config.mjs +62 -0
  27. package/build/images.mjs +86 -0
  28. package/build/incremental.mjs +76 -0
  29. package/build/index.mjs +356 -0
  30. package/build/loader-cache.mjs +69 -0
  31. package/build/metadata-images.mjs +72 -0
  32. package/build/metadata-routes.mjs +77 -0
  33. package/build/middleware.mjs +231 -0
  34. package/build/migrate.mjs +289 -0
  35. package/build/module-resolution.mjs +78 -0
  36. package/build/native-discovery.mjs +134 -0
  37. package/build/native-packages.mjs +105 -0
  38. package/build/npm-packages.mjs +110 -0
  39. package/build/og.mjs +14 -0
  40. package/build/page-extensions.mjs +12 -0
  41. package/build/passthrough-loader.cjs +4 -0
  42. package/build/precompress.mjs +122 -0
  43. package/build/scan.mjs +178 -0
  44. package/build/script-workers.mjs +25 -0
  45. package/build/server-actions-config.mjs +27 -0
  46. package/build/source-maps.mjs +34 -0
  47. package/build/standalone-config.mjs +19 -0
  48. package/build/standalone-runner.mjs +21 -0
  49. package/build/standalone-worker.mjs +5 -0
  50. package/build/standalone.mjs +240 -0
  51. package/build/static-export.mjs +93 -0
  52. package/build/styles.mjs +174 -0
  53. package/build/transform-cache.mjs +50 -0
  54. package/build/transform.mjs +155 -0
  55. package/build/turbopack-conditions.mjs +59 -0
  56. package/build/webpack-bridge.cjs +3 -0
  57. package/build/webpack-graph.mjs +256 -0
  58. package/build/webpack-loader.cjs +9 -0
  59. package/cli.mjs +210 -0
  60. package/compat/app-client-page.cjs +32 -0
  61. package/compat/app-context.cjs +40 -0
  62. package/compat/app-context.d.cts +20 -0
  63. package/compat/app-dynamic.cjs +34 -0
  64. package/compat/app-error-boundary.cjs +53 -0
  65. package/compat/app-global-error-boundary.cjs +34 -0
  66. package/compat/app-layout-context.cjs +27 -0
  67. package/compat/app-navigation-boundary.cjs +47 -0
  68. package/compat/app.cjs +33 -0
  69. package/compat/app.d.cts +9 -0
  70. package/compat/cache-handlers.cjs +223 -0
  71. package/compat/cache-life.cjs +34 -0
  72. package/compat/cache.cjs +99 -0
  73. package/compat/cache.d.cts +36 -0
  74. package/compat/compat/router.d.cts +2 -0
  75. package/compat/compat-router.cjs +7 -0
  76. package/compat/constants.cjs +8 -0
  77. package/compat/constants.d.cts +7 -0
  78. package/compat/cookies.cjs +129 -0
  79. package/compat/data-cache.cjs +389 -0
  80. package/compat/dist/shared/lib/app-router-context.shared-runtime.d.cts +3 -0
  81. package/compat/dist/shared/lib/router-context.d.cts +1 -0
  82. package/compat/dist/shared/lib/router-context.shared-runtime.d.cts +3 -0
  83. package/compat/document.cjs +73 -0
  84. package/compat/document.d.cts +41 -0
  85. package/compat/draft.cjs +28 -0
  86. package/compat/dynamic-bailout.cjs +13 -0
  87. package/compat/dynamic.cjs +119 -0
  88. package/compat/dynamic.d.cts +32 -0
  89. package/compat/error.cjs +36 -0
  90. package/compat/error.d.cts +15 -0
  91. package/compat/fetch.d.cts +10 -0
  92. package/compat/font/google.d.cts +1 -0
  93. package/compat/font/local.d.cts +2 -0
  94. package/compat/font-google.cjs +2 -0
  95. package/compat/font-google.d.cts +1944 -0
  96. package/compat/font-local.cjs +5 -0
  97. package/compat/font-local.d.cts +3 -0
  98. package/compat/font-types.d.cts +25 -0
  99. package/compat/head.cjs +180 -0
  100. package/compat/head.d.cts +3 -0
  101. package/compat/headers.cjs +89 -0
  102. package/compat/headers.d.cts +26 -0
  103. package/compat/image-config.cjs +3 -0
  104. package/compat/image-imports.d.ts +12 -0
  105. package/compat/image-shared.cjs +62 -0
  106. package/compat/image.cjs +64 -0
  107. package/compat/image.d.cts +35 -0
  108. package/compat/incremental-cache.cjs +186 -0
  109. package/compat/index.d.ts +159 -0
  110. package/compat/instant-samples.cjs +39 -0
  111. package/compat/instant.cjs +13 -0
  112. package/compat/link.cjs +68 -0
  113. package/compat/link.d.cts +17 -0
  114. package/compat/locale.cjs +30 -0
  115. package/compat/metadata.d.cts +77 -0
  116. package/compat/navigation-controls.cjs +27 -0
  117. package/compat/navigation-server.cjs +25 -0
  118. package/compat/navigation.cjs +52 -0
  119. package/compat/navigation.d.cts +28 -0
  120. package/compat/next-app-router-context.cjs +6 -0
  121. package/compat/next-router-context.cjs +3 -0
  122. package/compat/og.cjs +32 -0
  123. package/compat/og.d.cts +1 -0
  124. package/compat/paths.cjs +33 -0
  125. package/compat/preview.cjs +41 -0
  126. package/compat/rewrite.cjs +41 -0
  127. package/compat/route-request.cjs +64 -0
  128. package/compat/router.cjs +137 -0
  129. package/compat/router.d.cts +58 -0
  130. package/compat/script-context.cjs +5 -0
  131. package/compat/script-loader.cjs +135 -0
  132. package/compat/script.cjs +69 -0
  133. package/compat/script.d.cts +17 -0
  134. package/compat/server-primitives.cjs +109 -0
  135. package/compat/server.cjs +13 -0
  136. package/compat/server.d.cts +37 -0
  137. package/compat/static-generation.cjs +108 -0
  138. package/compat/use-cache.cjs +129 -0
  139. package/index.mjs +1 -0
  140. package/native/platforms.json +6 -0
  141. package/native/resolve.mjs +81 -0
  142. package/package.json +127 -0
  143. package/release/verify.mjs +59 -0
  144. package/runtime/action-client.mjs +19 -0
  145. package/runtime/action-crypto.mjs +138 -0
  146. package/runtime/action-reference.mjs +2 -0
  147. package/runtime/action-server.mjs +161 -0
  148. package/runtime/action-ssr.mjs +5 -0
  149. package/runtime/api.mjs +326 -0
  150. package/runtime/app-client.mjs +539 -0
  151. package/runtime/app-content.mjs +8 -0
  152. package/runtime/app-errors.mjs +12 -0
  153. package/runtime/app-instant.mjs +30 -0
  154. package/runtime/app-metadata.mjs +410 -0
  155. package/runtime/app-partial-model.mjs +228 -0
  156. package/runtime/app-partial.mjs +130 -0
  157. package/runtime/app-prefetch.mjs +34 -0
  158. package/runtime/app-recovery.mjs +87 -0
  159. package/runtime/app-render.mjs +544 -0
  160. package/runtime/app-routing-access.mjs +143 -0
  161. package/runtime/app-routing-context.mjs +35 -0
  162. package/runtime/app-routing-state.mjs +61 -0
  163. package/runtime/app-routing.mjs +323 -0
  164. package/runtime/app-segments.mjs +22 -0
  165. package/runtime/app-static-params.mjs +120 -0
  166. package/runtime/app-static.mjs +86 -0
  167. package/runtime/app-stream-channel.mjs +92 -0
  168. package/runtime/app-stream-html.mjs +279 -0
  169. package/runtime/build-directory.mjs +48 -0
  170. package/runtime/cache-arguments.mjs +33 -0
  171. package/runtime/cache-components-rsc.mjs +64 -0
  172. package/runtime/cache-request.mjs +23 -0
  173. package/runtime/client-navigation.mjs +40 -0
  174. package/runtime/dev-client.mjs +129 -0
  175. package/runtime/dev-file-changes.mjs +110 -0
  176. package/runtime/document.mjs +96 -0
  177. package/runtime/edge.mjs +105 -0
  178. package/runtime/env.mjs +68 -0
  179. package/runtime/fetch-cache.mjs +379 -0
  180. package/runtime/font-preload.mjs +4 -0
  181. package/runtime/http.mjs +190 -0
  182. package/runtime/incremental-cache.mjs +83 -0
  183. package/runtime/metadata-route.mjs +83 -0
  184. package/runtime/middleware.mjs +186 -0
  185. package/runtime/module-loader.mjs +21 -0
  186. package/runtime/navigation.mjs +16 -0
  187. package/runtime/og-loader.mjs +17 -0
  188. package/runtime/pages-client.mjs +197 -0
  189. package/runtime/pages-error-props.mjs +6 -0
  190. package/runtime/pages-errors.mjs +85 -0
  191. package/runtime/pages-initial-props.mjs +43 -0
  192. package/runtime/pages-navigation.mjs +565 -0
  193. package/runtime/pages-paths.mjs +80 -0
  194. package/runtime/pages-revalidate.mjs +33 -0
  195. package/runtime/partial-artifact-cache.mjs +37 -0
  196. package/runtime/partial-template.mjs +174 -0
  197. package/runtime/profile.mjs +9 -0
  198. package/runtime/profiles.json +47 -0
  199. package/runtime/render.mjs +304 -0
  200. package/runtime/request-transport.mjs +41 -0
  201. package/runtime/request-work.mjs +7 -0
  202. package/runtime/route-static.mjs +117 -0
  203. package/runtime/rsc-worker.mjs +426 -0
  204. package/runtime/script-html.mjs +61 -0
  205. package/runtime/stream-select.mjs +25 -0
  206. package/runtime/stream-utils.mjs +127 -0
  207. package/runtime/transport.mjs +108 -0
  208. package/runtime/watch-project.mjs +130 -0
  209. package/runtime/worker-artifacts.mjs +22 -0
  210. package/runtime/worker-metadata.mjs +31 -0
  211. package/runtime/worker.mjs +197 -0
@@ -0,0 +1,379 @@
1
+ import { createHash } from 'node:crypto';
2
+ import cache from '../compat/data-cache.cjs';
3
+ import { dynamicUsage, trackStaticDependency } from '../compat/static-generation.cjs';
4
+ import { recordCacheDependency } from '../compat/use-cache.cjs';
5
+
6
+ const { cachedValue, deferredValue, optionalRequestContext, cacheState, getCachePaths,
7
+ inCacheScope, nativeFetch, MAX_CACHE_VALUE_BYTES } = cache;
8
+ const PATCHED = Symbol.for('prnext.fetch-cache');
9
+ const MAX_MEMO_ENTRIES = 128;
10
+ const MAX_MEMO_BYTES = 8 * 1024 * 1024;
11
+ const CAPTURE_IDLE_MS = 1000;
12
+ const CAPTURE_LIFETIME_MS = 20_000;
13
+ let registration;
14
+
15
+ class UncacheableResponse extends Error {
16
+ constructor(response) { super('The fetch response is not cacheable'); this.response = response; }
17
+ async dispose() { await this.response.body?.cancel().catch(() => {}); }
18
+ }
19
+
20
+ function responseMetadata(response) {
21
+ return { version: 1, status: response.status, statusText: response.statusText,
22
+ headers: [...response.headers], url: response.url, redirected: response.redirected,
23
+ type: response.type, body: response.body !== null };
24
+ }
25
+
26
+ function decorate(response, metadata) {
27
+ for (const name of ['url', 'redirected', 'type']) Object.defineProperty(response, name, { value: metadata[name], configurable: true });
28
+ const clone = response.clone.bind(response);
29
+ Object.defineProperty(response, 'clone', { value: () => decorate(clone(), metadata), configurable: true });
30
+ return response;
31
+ }
32
+
33
+ function encodeResponse(metadata, chunks, length) {
34
+ const header = Buffer.from(JSON.stringify(metadata));
35
+ const value = Buffer.allocUnsafe(4 + header.byteLength + length);
36
+ value.writeUInt32BE(header.byteLength);
37
+ header.copy(value, 4);
38
+ let offset = 4 + header.byteLength;
39
+ for (const chunk of chunks) { value.set(chunk, offset); offset += chunk.byteLength; }
40
+ return value;
41
+ }
42
+
43
+ function decodeResponse(value) {
44
+ if (!Buffer.isBuffer(value) || value.byteLength < 4 || value.byteLength > MAX_CACHE_VALUE_BYTES) throw new Error('Invalid cached fetch response');
45
+ const offset = 4 + value.readUInt32BE(0);
46
+ if (offset > value.byteLength) throw new Error('Invalid cached fetch response');
47
+ const metadata = JSON.parse(value.subarray(4, offset).toString());
48
+ if (metadata.version !== 1 || !Array.isArray(metadata.headers) || !Number.isInteger(metadata.status) || metadata.status < 200 || metadata.status > 599) throw new Error('Invalid cached fetch response');
49
+ return decorate(new Response(metadata.body ? value.subarray(offset) : null, metadata), metadata);
50
+ }
51
+
52
+ function incrementalCodec(url) {
53
+ return { url,
54
+ encode(value) {
55
+ const offset = 4 + value.readUInt32BE(0);
56
+ const info = JSON.parse(value.subarray(4, offset).toString());
57
+ return { headers: Object.fromEntries(info.headers), status: info.status, url: info.url,
58
+ body: value.subarray(offset).toString('base64'), _prnext: { statusText: info.statusText, redirected: info.redirected, type: info.type, body: info.body } };
59
+ },
60
+ decode(entry) {
61
+ const data = entry.data;
62
+ if (!data || typeof data.body !== 'string' || data.body.length > Math.ceil(MAX_CACHE_VALUE_BYTES / 3) * 4 || data.status !== 200 || !data.headers || typeof data.headers !== 'object') throw new Error('Invalid incremental cached fetch');
63
+ const bytes = Buffer.from(data.body, 'base64');
64
+ const headers = new Headers(data.headers);
65
+ if (headers.has('set-cookie')) throw new Error('Incremental cached fetch contains a session mutation');
66
+ const info = { version: 1, status: data.status, statusText: data._prnext?.statusText || '', headers: [...headers],
67
+ url: typeof data.url === 'string' ? data.url : url, redirected: !!data._prnext?.redirected,
68
+ type: data._prnext?.type || 'basic', body: data._prnext?.body !== false };
69
+ if (bytes.length + 4 + Buffer.byteLength(JSON.stringify(info)) > MAX_CACHE_VALUE_BYTES) throw new Error('Incremental cached fetch exceeds 2 MiB');
70
+ return encodeResponse(info, [bytes], bytes.length);
71
+ },
72
+ };
73
+ }
74
+
75
+ // The origin is read once. The caller receives headers immediately; a bounded
76
+ // prefix is pumped ahead so small responses can fill the cache even if ignored.
77
+ // Once capture stops, only demand from that caller pulls further origin bytes.
78
+ function captureResponse(response, { signal, background = false } = {}) {
79
+ const metadata = responseMetadata(response);
80
+ const budget = MAX_CACHE_VALUE_BYTES - 4 - Buffer.byteLength(JSON.stringify(metadata));
81
+ const contentLength = Number(response.headers.get('content-length'));
82
+ if (budget < 0 || (Number.isFinite(contentLength) && contentLength > budget)) throw new UncacheableResponse(response);
83
+ if (!response.body) return { response, value: Promise.resolve(encodeResponse(metadata, [], 0)) };
84
+ const reader = response.body.getReader();
85
+ let capture = [];
86
+ let length = 0;
87
+ let collecting = true;
88
+ let stopped = false;
89
+ let ended = false;
90
+ let failure;
91
+ let pumping = false;
92
+ let waiting;
93
+ const queued = [];
94
+ let resolveValue;
95
+ let rejectValue;
96
+ const value = new Promise((resolve, reject) => { resolveValue = resolve; rejectValue = reject; });
97
+ // A body can fail after its headers have already been returned to the caller.
98
+ value.catch(() => {});
99
+ let idle;
100
+ const lifetime = setTimeout(() => finish(null), CAPTURE_LIFETIME_MS);
101
+ lifetime.unref();
102
+ const onAbort = () => finish(null);
103
+ function finish(result, error) {
104
+ if (!collecting) return;
105
+ collecting = false; capture = [];
106
+ clearTimeout(idle); clearTimeout(lifetime);
107
+ signal?.removeEventListener('abort', onAbort);
108
+ if (error) rejectValue(error); else resolveValue(result);
109
+ if (background) {
110
+ stopped = true; queued.length = 0;
111
+ void reader.cancel(error).catch(() => {});
112
+ }
113
+ }
114
+ function armIdle() {
115
+ clearTimeout(idle);
116
+ if (collecting) { idle = setTimeout(() => finish(null), CAPTURE_IDLE_MS); idle.unref(); }
117
+ }
118
+ function flushWaiter() {
119
+ if (!waiting) return;
120
+ const { controller, resolve } = waiting;
121
+ if (queued.length) { controller.enqueue(queued.shift()); waiting = undefined; resolve(); }
122
+ else if (failure) { controller.error(failure); waiting = undefined; resolve(); }
123
+ else if (ended || stopped) { controller.close(); waiting = undefined; resolve(); }
124
+ }
125
+ async function pump() {
126
+ if (pumping || stopped) return;
127
+ pumping = true;
128
+ try {
129
+ while (!stopped && !ended && (collecting || waiting)) {
130
+ const next = await reader.read();
131
+ if (stopped) break;
132
+ if (next.done) {
133
+ ended = true;
134
+ if (collecting) finish(encodeResponse(metadata, capture, length));
135
+ flushWaiter();
136
+ break;
137
+ }
138
+ if (collecting) {
139
+ length += next.value.byteLength;
140
+ if (length > budget) finish(null);
141
+ else {
142
+ // The caller may mutate bytes returned by a body reader. Capture
143
+ // owns a bounded copy, so such mutations cannot poison later hits.
144
+ capture.push(Uint8Array.from(next.value)); armIdle();
145
+ }
146
+ }
147
+ if (!stopped) queued.push(next.value);
148
+ flushWaiter();
149
+ }
150
+ } catch (error) {
151
+ failure = error; ended = true;
152
+ finish(null, error);
153
+ flushWaiter();
154
+ } finally { pumping = false; }
155
+ }
156
+ const body = new ReadableStream({
157
+ pull(controller) {
158
+ return new Promise(resolve => {
159
+ waiting = { controller, resolve };
160
+ flushWaiter();
161
+ if (waiting) void pump();
162
+ });
163
+ },
164
+ async cancel(reason) {
165
+ stopped = true; queued.length = 0;
166
+ finish(null);
167
+ flushWaiter();
168
+ await reader.cancel(reason);
169
+ },
170
+ }, { highWaterMark: 0 });
171
+ signal?.addEventListener('abort', onAbort, { once: true });
172
+ if (signal?.aborted) finish(null);
173
+ armIdle();
174
+ void pump();
175
+ return { response: decorate(new Response(body, metadata), metadata), value };
176
+ }
177
+
178
+ function validateFetchTags(value) {
179
+ if (value === undefined) return [];
180
+ if (!Array.isArray(value)) throw new TypeError('fetch next.tags must be an array');
181
+ const tags = [];
182
+ let invalid = false;
183
+ for (const tag of value) {
184
+ if (typeof tag !== 'string' || tag.length > 256 || tags.length >= 128) { invalid = true; continue; }
185
+ if (!tags.includes(tag)) tags.push(tag);
186
+ }
187
+ if (invalid) console.warn('[prnext] Invalid fetch tags were ignored; use at most 128 strings of at most 256 characters.');
188
+ return tags;
189
+ }
190
+
191
+ function fetchPolicy(input, init, context) {
192
+ const next = { ...input?.next, ...init?.next };
193
+ const tags = validateFetchTags(next.tags);
194
+ let revalidate = next.revalidate;
195
+ let mode = init?.cache ?? (input instanceof Request ? input.cache : undefined);
196
+ if (revalidate !== undefined && revalidate !== false &&
197
+ (typeof revalidate !== 'number' || Number.isNaN(revalidate) || revalidate < 0)) throw new TypeError('fetch next.revalidate must be a nonnegative number or false');
198
+ if ((mode === 'force-cache' && revalidate === 0) || (mode === 'no-store' && (revalidate === false || revalidate > 0))) {
199
+ if (!context.production) console.warn('[prnext] Conflicting fetch cache and next.revalidate options were ignored.');
200
+ mode = undefined; revalidate = undefined;
201
+ }
202
+ if (mode === 'default') mode = undefined;
203
+ const configured = context.cacheConfig?.forceNoStore ? 'force-no-store' : context.cacheConfig?.fetchCache || 'auto';
204
+ if (!['auto', 'default-cache', 'only-cache', 'force-cache', 'default-no-store', 'only-no-store', 'force-no-store'].includes(configured)) throw new TypeError('Invalid route fetchCache setting');
205
+ const uncached = mode === 'no-store' || mode === 'no-cache' || revalidate === 0;
206
+ const explicitCache = mode === 'force-cache' || revalidate === false || revalidate > 0;
207
+ if (configured === 'only-cache' && uncached) throw new Error('fetchCache: only-cache forbids uncached fetches');
208
+ if (configured === 'only-no-store' && explicitCache) throw new Error('fetchCache: only-no-store forbids cached fetches');
209
+ if (configured === 'force-no-store') { mode = 'no-store'; revalidate = 0; }
210
+ else if (configured === 'force-cache') { mode = 'force-cache'; if (revalidate === 0) revalidate = undefined; }
211
+ else if (!mode && revalidate === undefined) {
212
+ if (configured === 'default-no-store' || configured === 'only-no-store' || cacheState(context)?.noStore) mode = 'no-store';
213
+ else if (configured === 'default-cache' || configured === 'only-cache' || context.cacheConfig?.dynamic === 'error') mode = 'force-cache';
214
+ }
215
+ if (mode === 'no-store' || mode === 'no-cache' || revalidate === 0) dynamicUsage('an uncached fetch', context);
216
+ const persist = mode !== 'no-store' && mode !== 'no-cache' && revalidate !== 0 &&
217
+ (mode === 'force-cache' || revalidate === false || revalidate > 0);
218
+ if (!persist && context.cacheComponents && !inCacheScope()) dynamicUsage('an uncached fetch', context);
219
+ return { persist, tags, revalidate: revalidate === false || revalidate === Infinity || revalidate === undefined ? null : revalidate,
220
+ memoOptions: JSON.stringify([mode, revalidate, tags]) };
221
+ }
222
+
223
+ async function prepareRequest(input, init) {
224
+ // Opaque streaming request bodies cannot be hashed before dispatch without
225
+ // consuming/teeing the upload. They retain native streaming and bypass cache.
226
+ if (init?.body instanceof ReadableStream || (input instanceof Request && input.body && init?.body === undefined)) return null;
227
+ const known = init?.body;
228
+ if ((typeof known === 'string' && Buffer.byteLength(known) > MAX_CACHE_VALUE_BYTES) ||
229
+ (known instanceof Blob && known.size > MAX_CACHE_VALUE_BYTES) ||
230
+ (ArrayBuffer.isView(known) && known.byteLength > MAX_CACHE_VALUE_BYTES) ||
231
+ (known instanceof ArrayBuffer && known.byteLength > MAX_CACHE_VALUE_BYTES)) return null;
232
+ const form = known instanceof FormData ? [] : null;
233
+ if (form) {
234
+ let size = 0;
235
+ for (const [name, value] of known) {
236
+ size += Buffer.byteLength(name) + 128 + (typeof value === 'string' ? Buffer.byteLength(value) : value.size + Buffer.byteLength(value.name) + Buffer.byteLength(value.type));
237
+ if (size > MAX_CACHE_VALUE_BYTES) return null;
238
+ form.push([name, value]);
239
+ }
240
+ }
241
+ const request = new Request(input, init);
242
+ const headers = [...request.headers];
243
+ if (form && !new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)).has('content-type')) {
244
+ // FormData gets a new random wire boundary per Request. Match its ordered
245
+ // fields and file bytes, while retaining any caller-supplied content type.
246
+ const contentType = headers.find(header => header[0] === 'content-type');
247
+ if (contentType) contentType[1] = 'multipart/form-data';
248
+ }
249
+ const fields = [request.url, request.method, headers, request.mode, request.credentials,
250
+ request.redirect, request.referrer, request.referrerPolicy, request.integrity, request.keepalive, request.cache, request.duplex];
251
+ const metadata = JSON.stringify(fields);
252
+ if (Buffer.byteLength(metadata) > 64 * 1024) return null;
253
+ const hash = createHash('sha256').update('prnext-fetch-v1\0').update(metadata).update('\0');
254
+ if (!request.body) return { request, key: hash.digest('hex') };
255
+ if (form) {
256
+ hash.update('form-data\0');
257
+ for (const [name, value] of form) {
258
+ if (typeof value === 'string') hash.update(JSON.stringify([name, 'text', value]) + '\0');
259
+ else {
260
+ hash.update(JSON.stringify([name, 'file', value.name, value.type, value.size]) + '\0');
261
+ hash.update(new Uint8Array(await value.arrayBuffer())).update('\0');
262
+ }
263
+ }
264
+ return { request, key: hash.digest('hex') };
265
+ }
266
+ const chunks = [];
267
+ const reader = request.body.getReader();
268
+ let length = 0;
269
+ for (;;) {
270
+ const next = await reader.read();
271
+ if (next.done) break;
272
+ chunks.push(next.value); length += next.value.byteLength;
273
+ if (length > MAX_CACHE_VALUE_BYTES) {
274
+ // This is a known BodyInit (e.g. FormData), so the untouched caller input
275
+ // can still be sent normally after cancelling this temporary encoding.
276
+ await reader.cancel();
277
+ return null;
278
+ }
279
+ }
280
+ const bytes = Buffer.concat(chunks, length);
281
+ hash.update(bytes);
282
+ return { request: new Request(request, { body: bytes, duplex: 'half' }), key: hash.digest('hex') };
283
+ }
284
+
285
+ function memoState(state) {
286
+ return state.fetchMemo ||= { entries: new Map(), bytes: 0 };
287
+ }
288
+
289
+ export function installFetchCache() {
290
+ let dispatcher = globalThis.fetch[PATCHED];
291
+ if (!dispatcher) {
292
+ dispatcher = { handlers: [] };
293
+ const fetch = function fetch(input, init) {
294
+ for (let index = dispatcher.handlers.length - 1; index >= 0; index--) {
295
+ const handler = dispatcher.handlers[index];
296
+ const context = handler.context();
297
+ if (context) return handler.fetch(input, init, context);
298
+ }
299
+ return nativeFetch(input, init);
300
+ };
301
+ Object.defineProperty(fetch, PATCHED, { value: dispatcher });
302
+ globalThis.fetch = fetch;
303
+ }
304
+ if (registration && dispatcher.handlers.includes(registration)) return globalThis.fetch;
305
+ async function fetch(input, init, context) {
306
+ if (context.phase === 'pages' || init?.next?.internal) return nativeFetch(input, init);
307
+ if (inCacheScope()) {
308
+ const next = init?.next || input?.next || {};
309
+ if (next.revalidate !== undefined && next.revalidate !== false && (typeof next.revalidate !== 'number' || Number.isNaN(next.revalidate) || next.revalidate < 0)) throw new TypeError('fetch next.revalidate must be a nonnegative number or false');
310
+ recordCacheDependency(next);
311
+ return nativeFetch(input, init);
312
+ }
313
+ const policy = fetchPolicy(input, init, context);
314
+ if (context.staticState) trackStaticDependency({ tags: policy.tags, paths: getCachePaths(context),
315
+ revalidate: context.cacheConfig?.dynamic === 'force-static' && policy.revalidate === 0 ? null : policy.revalidate }, context);
316
+ if (init?.dispatcher || init?.agent) return nativeFetch(input, init);
317
+ const state = cacheState(context);
318
+ const explicitSignal = init?.signal !== undefined || input instanceof Request;
319
+ const method = String(init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();
320
+ const memoize = context.phase === 'render' && method === 'GET' && !explicitSignal;
321
+ const hardRefresh = !context.production && /(?:^|,)\s*no-cache\s*(?:,|$)/i.test(context.headers?.get?.('cache-control') || '');
322
+ const persist = policy.persist && !hardRefresh && !context.draftMode && !context.cacheConfig?.forceNoStore;
323
+ if (!persist && !memoize) return nativeFetch(input, init);
324
+ const prepared = await prepareRequest(input, init);
325
+ if (!prepared) return nativeFetch(input, init);
326
+ const request = prepared.request;
327
+ request.signal.throwIfAborted();
328
+ const memo = memoize ? memoState(state) : null;
329
+ const memoKey = `${prepared.key}:${policy.memoOptions}`;
330
+ await state.invalidations;
331
+ if (memo?.entries.has(memoKey)) {
332
+ const entry = memo.entries.get(memoKey);
333
+ const value = await entry.promise;
334
+ if (value) return decodeResponse(value);
335
+ // A live/noncacheable Response always belongs to one caller. A follower
336
+ // that cannot reuse completed bytes makes its own native request.
337
+ }
338
+ let entry;
339
+ if (memo && memo.entries.size < MAX_MEMO_ENTRIES && memo.bytes + MAX_CACHE_VALUE_BYTES <= MAX_MEMO_BYTES) {
340
+ let resolve;
341
+ entry = { size: MAX_CACHE_VALUE_BYTES, promise: new Promise(done => { resolve = done; }), resolve };
342
+ memo.entries.set(memoKey, entry); memo.bytes += entry.size;
343
+ }
344
+ function settleMemo(value) {
345
+ if (!entry) return;
346
+ memo.bytes -= entry.size;
347
+ if (value) { entry.size = value.byteLength; memo.bytes += entry.size; }
348
+ else if (memo.entries.get(memoKey) === entry) memo.entries.delete(memoKey);
349
+ entry.resolve(value);
350
+ entry = undefined;
351
+ }
352
+ const producer = async ({ background = false, signal, cache: canPersist = true } = {}) => {
353
+ const response = await nativeFetch(request, background && signal ? { signal: AbortSignal.any([request.signal, signal]) } : undefined);
354
+ // Set-Cookie responses must not replay a session mutation, even when the
355
+ // request itself explicitly opted into shared caching.
356
+ if (response.headers.has('set-cookie') || (persist && response.status !== 200) || (!canPersist && !entry)) throw new UncacheableResponse(response);
357
+ const captured = captureResponse(response, { signal, background });
358
+ captured.value.then(settleMemo, () => settleMemo(null));
359
+ return deferredValue(captured.response, captured.value, () => captured.response.body?.cancel());
360
+ };
361
+ try {
362
+ const result = persist ? await cachedValue(prepared.key, producer, {
363
+ tags: policy.tags, paths: getCachePaths(context), revalidate: policy.revalidate,
364
+ context, signal: request.signal, incremental: incrementalCodec(request.url),
365
+ }) : await producer({ cache: false });
366
+ if (Buffer.isBuffer(result)) { settleMemo(result); return decodeResponse(result); }
367
+ // Without persistent caching, we unwrap our own deferred value here.
368
+ if (!persist) return result.value;
369
+ return result;
370
+ } catch (error) {
371
+ settleMemo(null);
372
+ if (error instanceof UncacheableResponse) return error.response;
373
+ throw error;
374
+ }
375
+ }
376
+ registration = { context: optionalRequestContext, fetch };
377
+ dispatcher.handlers.push(registration);
378
+ return globalThis.fetch;
379
+ }
@@ -0,0 +1,4 @@
1
+ const escape = value => String(value).replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character]);
2
+ export function fontPreloads(fonts = [], nonce) {
3
+ return fonts.map(font => `<link rel="preload" as="font" href="${escape(font.href)}" type="${escape(font.type)}" crossorigin="anonymous"${nonce ? ` nonce="${escape(nonce)}"` : ''}>`).join('');
4
+ }
@@ -0,0 +1,190 @@
1
+ import { IncomingMessage, validateHeaderName, validateHeaderValue } from 'node:http';
2
+ import { Duplex, Writable } from 'node:stream';
3
+ import { revalidatePage } from './pages-revalidate.mjs';
4
+ import { currentRequest } from '../compat/headers.cjs';
5
+ import { draftCookie } from '../compat/draft.cjs';
6
+ import { previewCookies, NAME as PREVIEW_COOKIE } from '../compat/preview.cjs';
7
+ import { serializeCookie } from '../compat/cookies.cjs';
8
+
9
+ export const MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
10
+ export const forbiddenHeaders = new Set(['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade']);
11
+
12
+ export function escapeHtml(value) {
13
+ return String(value).replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character]);
14
+ }
15
+
16
+ export function serializeData(value) {
17
+ return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, character => ({ '<': '\\u003c', '>': '\\u003e', '&': '\\u0026', '\u2028': '\\u2028', '\u2029': '\\u2029' })[character]);
18
+ }
19
+
20
+ export function queryFromUrl(url, params = {}) {
21
+ const query = Object.create(null);
22
+ for (const [key, value] of url.searchParams) {
23
+ if (Object.hasOwn(query, key)) query[key] = [...(Array.isArray(query[key]) ? query[key] : [query[key]]), value];
24
+ else query[key] = value;
25
+ }
26
+ return Object.assign(query, params);
27
+ }
28
+
29
+ function parseCookies(header = '') {
30
+ const cookies = Object.create(null);
31
+ for (const part of String(header).split(';')) {
32
+ const separator = part.indexOf('=');
33
+ if (separator < 0) continue;
34
+ const name = part.slice(0, separator).trim();
35
+ if (!name || Object.hasOwn(cookies, name)) continue;
36
+ let value = part.slice(separator + 1).trim();
37
+ if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
38
+ try { cookies[name] = decodeURIComponent(value); } catch { cookies[name] = value; }
39
+ }
40
+ return cookies;
41
+ }
42
+
43
+ export function createRequest({ url, originalUrl, method = 'GET', headers = {}, body = '', params = {} }) {
44
+ const parsed = new URL(url, 'http://localhost');
45
+ const visible = originalUrl ? new URL(originalUrl, parsed) : parsed;
46
+ const socket = new Duplex({ read() {}, write(_chunk, _encoding, callback) { callback(); } });
47
+ socket.remoteAddress = '127.0.0.1';
48
+ const request = new IncomingMessage(socket);
49
+ request.method = method;
50
+ request.url = `${visible.pathname}${visible.search}`;
51
+ request.headers = Object.fromEntries(Object.entries(headers).map(([name, value]) => [name.toLowerCase(), value]));
52
+ request.rawHeaders = Object.entries(request.headers).flatMap(([name, value]) => [name, String(value)]);
53
+ request.httpVersion = '1.1';
54
+ request.query = queryFromUrl(parsed, params);
55
+ request.cookies = parseCookies(request.headers.cookie);
56
+ request.params = params;
57
+ request.complete = true;
58
+ const bytes = Buffer.isBuffer(body) ? body : Buffer.from(body || '', 'base64');
59
+ if (bytes.length) request.push(bytes);
60
+ request.push(null);
61
+ return { request, bytes, url: parsed };
62
+ }
63
+
64
+ export class CapturedResponse extends Writable {
65
+ constructor(options, canRevalidate = false) {
66
+ super(options);
67
+ this._canRevalidate = canRevalidate;
68
+ this.statusCode = 200;
69
+ this.statusMessage = '';
70
+ this.headersSent = false;
71
+ this._headers = Object.create(null);
72
+ this._chunks = [];
73
+ this._size = 0;
74
+ // renderPage can consume res.end() synchronously before the stream emits error.
75
+ // Keep errors observable through .errored/.result() without an unhandled event.
76
+ this.on('error', () => {});
77
+ }
78
+ _write(chunk, _encoding, callback) {
79
+ this._size += chunk.byteLength;
80
+ if (this._size > MAX_RESPONSE_BYTES) { callback(new Error('Response exceeds the 16 MiB PRNext limit')); return; }
81
+ this.headersSent = true;
82
+ this._chunks.push(Buffer.from(chunk));
83
+ callback();
84
+ }
85
+ get finished() { return this.writableEnded; }
86
+ setHeader(name, value) {
87
+ if (this.headersSent) throw new Error('Cannot set headers after they are sent');
88
+ validateHeaderName(name);
89
+ const normalized = Array.isArray(value) ? value.map(String) : String(value);
90
+ for (const item of Array.isArray(normalized) ? normalized : [normalized]) validateHeaderValue(name, item);
91
+ this._headers[name.toLowerCase()] = normalized;
92
+ return this;
93
+ }
94
+ getHeader(name) { return this._headers[String(name).toLowerCase()]; }
95
+ getHeaders() { return { ...this._headers }; }
96
+ getHeaderNames() { return Object.keys(this._headers); }
97
+ hasHeader(name) { return Object.hasOwn(this._headers, String(name).toLowerCase()); }
98
+ removeHeader(name) {
99
+ if (this.headersSent) throw new Error('Cannot remove headers after they are sent');
100
+ delete this._headers[String(name).toLowerCase()];
101
+ }
102
+ appendHeader(name, value) {
103
+ const existing = this.getHeader(name);
104
+ return this.setHeader(name, [...(existing === undefined ? [] : Array.isArray(existing) ? existing : [existing]), ...(Array.isArray(value) ? value : [value])]);
105
+ }
106
+ writeHead(statusCode, statusMessage, headers) {
107
+ this.status(statusCode);
108
+ if (typeof statusMessage === 'string') this.statusMessage = statusMessage;
109
+ else headers = statusMessage;
110
+ if (Array.isArray(headers)) {
111
+ for (let i = 0; i < headers.length; i += 2) this.appendHeader(headers[i], headers[i + 1]);
112
+ } else {
113
+ for (const [name, value] of Object.entries(headers || {})) this.setHeader(name, value);
114
+ }
115
+ this.headersSent = true;
116
+ return this;
117
+ }
118
+ flushHeaders() { this.headersSent = true; }
119
+ status(code) {
120
+ if (!Number.isInteger(code) || code < 100 || code > 599) throw new TypeError('Invalid HTTP status code');
121
+ this.statusCode = code;
122
+ return this;
123
+ }
124
+ setDraftMode({ enable = true } = {}) {
125
+ if (typeof enable !== 'boolean') throw new TypeError('setDraftMode enable must be a boolean');
126
+ const context = currentRequest();
127
+ this.appendHeader('set-cookie', serializeCookie(draftCookie(context, enable, { keepValue: true })));
128
+ this.setHeader('cache-control', 'private, no-cache, no-store, max-age=0');
129
+ context.draftChanged = true;
130
+ return this;
131
+ }
132
+ clearPreviewData({ path = '/' } = {}) {
133
+ const context = currentRequest();
134
+ this.appendHeader('set-cookie', serializeCookie(draftCookie(context, false, { path })));
135
+ this.appendHeader('set-cookie', serializeCookie({ ...draftCookie(context, false, { path }), name: PREVIEW_COOKIE }));
136
+ this.setHeader('cache-control', 'private, no-cache, no-store, max-age=0');
137
+ context.draftChanged = true;
138
+ return this;
139
+ }
140
+ setPreviewData(data, options) {
141
+ const context = currentRequest();
142
+ const cookies = previewCookies(context, data, options);
143
+ for (const cookie of cookies) this.appendHeader('set-cookie', serializeCookie(cookie));
144
+ this.setHeader('cache-control', 'private, no-cache, no-store, max-age=0');
145
+ context.draftChanged = true;
146
+ return this;
147
+ }
148
+ json(value) {
149
+ this.setHeader('content-type', 'application/json; charset=utf-8');
150
+ return this.end(JSON.stringify(value));
151
+ }
152
+ send(value) {
153
+ if (value === null || value === undefined) return this.end();
154
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
155
+ if (!this.hasHeader('content-type')) this.setHeader('content-type', 'application/octet-stream');
156
+ return this.end(value);
157
+ }
158
+ if (typeof value === 'object' || typeof value === 'boolean' || typeof value === 'number') return this.json(value);
159
+ if (!this.hasHeader('content-type')) this.setHeader('content-type', 'text/html; charset=utf-8');
160
+ return this.end(String(value));
161
+ }
162
+ redirect(code, destination) {
163
+ if (typeof code === 'string') { destination = code; code = 307; }
164
+ this.status(code);
165
+ this.setHeader('location', destination);
166
+ return this.end(destination);
167
+ }
168
+ revalidate(path, options) { return revalidatePage(this, path, options); }
169
+ end(chunk, encoding, callback) {
170
+ this.headersSent = true;
171
+ return super.end(chunk, encoding, callback);
172
+ }
173
+ result() {
174
+ if (this.errored) throw this.errored;
175
+ if (!Number.isInteger(this.statusCode) || this.statusCode < 100 || this.statusCode > 599) throw new Error('Invalid response status');
176
+ const headers = Object.fromEntries(Object.entries(this._headers).filter(([name]) => !forbiddenHeaders.has(name) && name !== 'content-length').map(([name, value]) => [name, Array.isArray(value) ? [...value] : value]));
177
+ return { status: this.statusCode, headers, body: [204, 205, 304].includes(this.statusCode) ? Buffer.alloc(0) : Buffer.concat(this._chunks) };
178
+ }
179
+ }
180
+
181
+ export function plainResponse(status, message) {
182
+ return { status, headers: { 'content-type': 'text/plain; charset=utf-8' }, body: Buffer.from(message) };
183
+ }
184
+
185
+ export function errorResponse(error, production = process.env.NODE_ENV === 'production') {
186
+ console.error('[prnext]', error?.stack || error);
187
+ const status = [400, 504].includes(error?.statusCode) ? error.statusCode : 500;
188
+ const message = status === 400 ? 'Bad Request' : status === 504 ? 'Gateway Timeout' : 'Internal Server Error';
189
+ return plainResponse(status, production || status === 400 ? message : String(error?.stack || error));
190
+ }
@@ -0,0 +1,83 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import legacy from '../compat/incremental-cache.cjs';
4
+
5
+ const MAX_BYTES = 16 * 1024 * 1024;
6
+ const kinds = new Set(['PAGES', 'APP_PAGE', 'APP_ROUTE']);
7
+ const empty = status => ({ status, headers: {}, body: Buffer.alloc(0) });
8
+ function bytes(value) {
9
+ if (!(value instanceof Uint8Array) || value.byteLength > MAX_BYTES) throw new Error('Invalid incremental cache bytes');
10
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
11
+ }
12
+ async function file(filename) {
13
+ if (typeof filename !== 'string') throw new Error('Missing incremental cache file');
14
+ if ((await stat(filename)).size > MAX_BYTES) throw new Error('Incremental cache file exceeds 16 MiB');
15
+ const value = await readFile(filename);
16
+ if (value.length > MAX_BYTES) throw new Error('Incremental cache file exceeds 16 MiB');
17
+ return value;
18
+ }
19
+ function metadata(value) {
20
+ const info = value?._prnext;
21
+ if (!info || info.version !== 1 || !Array.isArray(info.tags) || !Array.isArray(info.paths) ||
22
+ (info.revalidate !== false && (!Number.isInteger(info.revalidate) || info.revalidate < 0))) throw new Error('Invalid incremental cache metadata');
23
+ if (info.tags.length > 128 || info.paths.length > 128 || [...info.tags, ...info.paths].some(tag => typeof tag !== 'string' || tag.length > 8192)) throw new Error('Invalid incremental cache associations');
24
+ if (Buffer.byteLength(JSON.stringify(info)) > 48 * 1024) throw new Error('Incremental cache metadata exceeds its budget');
25
+ return info;
26
+ }
27
+ function implicit(paths = []) {
28
+ return paths.map(value => { const separator = value.indexOf(':'); return '_N_T_' + value.slice(separator + 1).replace(/\/$/, '') + '/' + value.slice(0, separator); });
29
+ }
30
+
31
+ /** Private worker operation. Rust still owns admission, routing, file pairs and transport. */
32
+ export async function runIncrementalCache(options) {
33
+ if (!options.manifest?.config?.cacheHandler) throw new Error('No incremental cache handler configured');
34
+ if (typeof options.body !== 'string' || options.body.length > 256 * 1024) throw new Error('Invalid incremental cache operation');
35
+ const input = JSON.parse(Buffer.from(options.body, 'base64').toString());
36
+ if (!input || typeof input.key !== 'string' || input.key.length > 8192 || !kinds.has(input.kind)) throw new Error('Invalid incremental cache key or kind');
37
+ const context = { ...options, cacheHandler: options.manifest.config.cacheHandler, cacheMaxMemorySize: options.manifest.config.cacheMaxMemorySize };
38
+ if (input.op === 'invalidate') {
39
+ await legacy.invalidate(context, { tags: input.tags, mode: 'expire' });
40
+ return empty(204);
41
+ }
42
+ if (input.op === 'set') {
43
+ const html = await file(input.htmlFile);
44
+ const data = input.kind === 'APP_ROUTE' ? Buffer.alloc(0) : await file(input.dataFile);
45
+ if (html.length + data.length > MAX_BYTES) return empty(413);
46
+ const info = { version: 1, revalidate: input.revalidate, tags: input.tags || [], paths: input.paths || [] };
47
+ const value = { kind: input.kind, headers: input.headers || {}, status: input.status, _prnext: info };
48
+ metadata(value);
49
+ if (input.kind === 'APP_ROUTE') value.body = html;
50
+ else {
51
+ value.html = html.toString();
52
+ if (input.kind === 'APP_PAGE') value.rscData = data;
53
+ else value.pageData = JSON.parse(data.toString());
54
+ }
55
+ const tags = [...new Set([...info.tags, ...implicit(info.paths)])];
56
+ if (tags.length) value.headers = { ...value.headers, 'x-next-cache-tags': tags.join(',') };
57
+ await legacy.setEntry(input.key, value, context, { tags, cacheControl: { revalidate: info.revalidate }, isRoutePPREnabled: false });
58
+ return empty(204);
59
+ }
60
+ if (input.op !== 'get') throw new Error('Unknown incremental cache operation');
61
+ const entry = await legacy.getEntry(input.key, input.kind, context, { tags: input.tags || [], softTags: implicit(input.paths) });
62
+ if (!entry?.value) return empty(204);
63
+ const value = entry.value;
64
+ if (value.kind !== input.kind || typeof entry.lastModified !== 'number' || !Number.isFinite(entry.lastModified) || entry.lastModified < -1 || entry.lastModified > Date.now() + 60_000) throw new Error('Invalid incremental cache entry');
65
+ const info = metadata(value);
66
+ if (!Number.isInteger(value.status) || value.status < 200 || value.status > 599) throw new Error('Invalid incremental cached status');
67
+ if (!value.headers || typeof value.headers !== 'object' || Array.isArray(value.headers) || Buffer.byteLength(JSON.stringify(value.headers)) > 48 * 1024) throw new Error('Invalid incremental cached headers');
68
+ let html, data = Buffer.alloc(0);
69
+ if (input.kind === 'APP_ROUTE') html = bytes(value.body);
70
+ else {
71
+ if (typeof value.html !== 'string' || Buffer.byteLength(value.html) > MAX_BYTES) throw new Error('Invalid incremental cached HTML');
72
+ html = Buffer.from(value.html);
73
+ data = input.kind === 'APP_PAGE' ? bytes(value.rscData) : Buffer.from(JSON.stringify(value.pageData));
74
+ }
75
+ if (html.length + data.length > MAX_BYTES) throw new Error('Incremental cache entry exceeds 16 MiB');
76
+ const cacheVersion = createHash('sha256').update(JSON.stringify([entry.lastModified, value.status, value.headers, info])).update(html).update(data).digest('hex');
77
+ if (input.knownVersion === cacheVersion) return { ...empty(304), headers: { 'x-prnext-cache-version': cacheVersion } };
78
+ const headers = { ...value.headers }; delete headers['x-next-cache-tags'];
79
+ return { status: value.status, headers, body: [html, data],
80
+ isr: { ...(input.kind === 'APP_ROUTE' ? { kind: 'route' } : {}), revalidate: entry.lastModified < 0 ? 0 : info.revalidate,
81
+ htmlLength: html.length, dataLength: data.length, tags: info.tags, paths: info.paths,
82
+ lastModified: entry.lastModified, cacheVersion } };
83
+ }