@thomas.f/prnext 0.1.17-alpha → 0.1.19-alpha

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 (202) hide show
  1. package/README.md +27 -868
  2. package/build/actions.mjs +27 -15
  3. package/build/analyze.mjs +180 -24
  4. package/build/app-config.mjs +13 -0
  5. package/build/app-static.mjs +34 -12
  6. package/build/app-styles.mjs +79 -0
  7. package/build/app.mjs +122 -46
  8. package/build/bundle-runtime.mjs +22 -9
  9. package/build/cache-components.mjs +1 -1
  10. package/build/cache-handlers.mjs +1 -1
  11. package/build/compact-css.mjs +53 -0
  12. package/build/compilation-cache.mjs +269 -0
  13. package/build/compilation-store.mjs +142 -0
  14. package/build/compiler.mjs +34 -10
  15. package/build/config.mjs +34 -3
  16. package/build/create.mjs +239 -0
  17. package/build/css-modules.mjs +56 -12
  18. package/build/deployment-assets.mjs +89 -0
  19. package/build/deployment-config.mjs +18 -0
  20. package/build/dev-css-assets.mjs +173 -0
  21. package/build/dev-css-update.mjs +18 -14
  22. package/build/dev-dependencies.mjs +1 -1
  23. package/build/dev-discovery.mjs +3 -3
  24. package/build/dev-lazy-build.mjs +77 -0
  25. package/build/dev.mjs +15 -8
  26. package/build/dynamic.mjs +14 -7
  27. package/build/edge.mjs +3 -3
  28. package/build/fonts.mjs +1 -1
  29. package/build/framework-imports.mjs +15 -5
  30. package/build/html-only-config.mjs +53 -0
  31. package/build/html-only-graph.mjs +67 -0
  32. package/build/incremental.mjs +112 -18
  33. package/build/index.mjs +90 -28
  34. package/build/instrumentation.mjs +30 -0
  35. package/build/lazy-dynamic-imports.mjs +137 -0
  36. package/build/metadata-routes.mjs +1 -1
  37. package/build/migrate.mjs +4 -4
  38. package/build/migration-audit.mjs +19 -31
  39. package/build/migration-imports.mjs +245 -0
  40. package/build/migration-scenarios-runner.mjs +73 -0
  41. package/build/migration-scenarios.mjs +72 -0
  42. package/build/native-discovery.mjs +65 -5
  43. package/build/native-packages.mjs +2 -0
  44. package/build/native-type-config.mjs +37 -0
  45. package/build/npm-packages.mjs +1 -0
  46. package/build/persistent-cache.mjs +18 -3
  47. package/build/precompress.mjs +1 -1
  48. package/build/prerender-cache.mjs +70 -0
  49. package/build/prerender-config.mjs +29 -0
  50. package/build/prerender-pages.mjs +19 -9
  51. package/build/prerender-pool.mjs +165 -0
  52. package/build/prerender-process-worker.mjs +58 -0
  53. package/build/prerender-worker.mjs +5 -5
  54. package/build/production-builder.mjs +59 -10
  55. package/build/react-compiler-report.mjs +225 -0
  56. package/build/react-compiler.mjs +139 -0
  57. package/build/runtime-contract.mjs +2 -2
  58. package/build/standalone.mjs +21 -3
  59. package/build/static-export.mjs +4 -4
  60. package/build/styles.mjs +6 -4
  61. package/build/transform.mjs +1 -1
  62. package/build/typed-routes.mjs +4 -0
  63. package/build/typescript-dev.mjs +35 -8
  64. package/build/typescript-native-worker.mjs +55 -0
  65. package/build/typescript-native.mjs +18 -0
  66. package/build/typescript-toolchain.mjs +19 -0
  67. package/build/typescript.mjs +6 -4
  68. package/build/webpack-graph.mjs +19 -6
  69. package/build/worker-memory.mjs +9 -0
  70. package/cli.mjs +70 -5
  71. package/compat/activity-cache.cjs +26 -0
  72. package/compat/after.cjs +6 -4
  73. package/compat/app-dynamic.cjs +12 -1
  74. package/compat/app-layout-context.cjs +52 -2
  75. package/compat/base64.cjs +10 -0
  76. package/compat/cache-handlers.cjs +15 -3
  77. package/compat/cache-wire.cjs +28 -0
  78. package/compat/cache.cjs +2 -1
  79. package/compat/cache.d.cts +4 -0
  80. package/compat/cookies.cjs +21 -5
  81. package/compat/data-cache.cjs +12 -8
  82. package/compat/flight-delta.cjs +152 -0
  83. package/compat/headers.cjs +4 -2
  84. package/compat/incremental-cache.cjs +20 -7
  85. package/compat/index.d.ts +69 -4
  86. package/compat/link.cjs +13 -10
  87. package/compat/navigation-authorization.cjs +9 -0
  88. package/compat/navigation-stages.cjs +19 -0
  89. package/compat/offline-store.cjs +11 -0
  90. package/compat/offline.cjs +5 -0
  91. package/compat/offline.d.cts +2 -0
  92. package/compat/redis-cache.cjs +201 -0
  93. package/compat/redis-cache.d.cts +37 -0
  94. package/compat/server-primitives.cjs +31 -11
  95. package/compat/server.d.cts +15 -2
  96. package/compat/static-generation.cjs +7 -0
  97. package/compat/use-cache.cjs +3 -0
  98. package/docs/ai/compatibility.md +248 -2
  99. package/docs/ai/data.md +165 -5
  100. package/docs/ai/deployment.md +158 -0
  101. package/docs/ai/quickstart.md +29 -4
  102. package/docs/ai/routing.md +93 -0
  103. package/docs/ai/troubleshooting.md +116 -2
  104. package/package.json +40 -9
  105. package/runtime/action-client.mjs +10 -4
  106. package/runtime/action-server.mjs +2 -0
  107. package/runtime/app-client.mjs +316 -92
  108. package/runtime/app-content.mjs +4 -3
  109. package/runtime/app-partial-client.mjs +3 -0
  110. package/runtime/app-partial.mjs +1 -1
  111. package/runtime/app-prefetch-constants.mjs +4 -0
  112. package/runtime/app-prefetch.mjs +2 -3
  113. package/runtime/app-render-key.mjs +5 -0
  114. package/runtime/app-render.mjs +88 -28
  115. package/runtime/app-routing.mjs +22 -9
  116. package/runtime/app-static.mjs +18 -8
  117. package/runtime/app-stream-html.mjs +10 -53
  118. package/runtime/app-style-selection.mjs +34 -0
  119. package/runtime/app-styles.mjs +154 -0
  120. package/runtime/atomic-file.mjs +27 -0
  121. package/runtime/cache-components-rsc.mjs +7 -2
  122. package/runtime/cache-request.mjs +2 -1
  123. package/runtime/deployment-client.mjs +19 -0
  124. package/runtime/dev-client.mjs +13 -3
  125. package/runtime/dev-compiler-broker.mjs +42 -0
  126. package/runtime/dev-file-changes.mjs +123 -8
  127. package/runtime/dev-indicator.mjs +8 -3
  128. package/runtime/dev-lazy-imports.mjs +121 -0
  129. package/runtime/dev-lazy-staging.mjs +14 -0
  130. package/runtime/dev-lazy-tool.mjs +16 -0
  131. package/runtime/dev-style-retention.mjs +15 -0
  132. package/runtime/development.mjs +83 -25
  133. package/runtime/document.mjs +1 -1
  134. package/runtime/edge.mjs +6 -5
  135. package/runtime/fetch-cache.mjs +10 -8
  136. package/runtime/flight-delta.mjs +74 -0
  137. package/runtime/flight-tap.mjs +28 -0
  138. package/runtime/fs.mjs +15 -8
  139. package/runtime/html-only.mjs +27 -0
  140. package/runtime/http.mjs +2 -0
  141. package/runtime/incremental-cache.mjs +25 -4
  142. package/runtime/instrumentation.mjs +49 -0
  143. package/runtime/lazy-import-client.mjs +38 -0
  144. package/runtime/navigation-recovery.mjs +36 -0
  145. package/runtime/navigation.mjs +7 -0
  146. package/runtime/node-html-stream.mjs +47 -0
  147. package/runtime/pages-client.mjs +14 -7
  148. package/runtime/pages-navigation.mjs +48 -63
  149. package/runtime/prefetch-scheduler.mjs +110 -0
  150. package/runtime/profile.mjs +14 -0
  151. package/runtime/profiles.json +2 -2
  152. package/runtime/render.mjs +3 -0
  153. package/runtime/route-pattern.mjs +44 -0
  154. package/runtime/rsc-worker.mjs +34 -12
  155. package/runtime/trace.mjs +78 -0
  156. package/runtime/watch-project.mjs +19 -15
  157. package/runtime/worker.mjs +70 -26
  158. package/vendor/nft/LICENSE +7 -0
  159. package/vendor/nft/README.md +16 -0
  160. package/vendor/nft/out/analyze.d.ts +8 -0
  161. package/vendor/nft/out/analyze.js +1216 -0
  162. package/vendor/nft/out/analyze.js.map +1 -0
  163. package/vendor/nft/out/cli.d.ts +2 -0
  164. package/vendor/nft/out/cli.js +94 -0
  165. package/vendor/nft/out/cli.js.map +1 -0
  166. package/vendor/nft/out/fs.d.ts +22 -0
  167. package/vendor/nft/out/fs.js +106 -0
  168. package/vendor/nft/out/fs.js.map +1 -0
  169. package/vendor/nft/out/index.d.ts +4 -0
  170. package/vendor/nft/out/index.js +25 -0
  171. package/vendor/nft/out/index.js.map +1 -0
  172. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  173. package/vendor/nft/out/node-file-trace.js +380 -0
  174. package/vendor/nft/out/node-file-trace.js.map +1 -0
  175. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  176. package/vendor/nft/out/resolve-dependency.js +427 -0
  177. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  178. package/vendor/nft/out/types.d.ts +66 -0
  179. package/vendor/nft/out/types.js +2 -0
  180. package/vendor/nft/out/types.js.map +1 -0
  181. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  182. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  183. package/vendor/nft/out/utils/binary-locators.js +88 -0
  184. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  185. package/vendor/nft/out/utils/get-package-base.js +30 -0
  186. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  187. package/vendor/nft/out/utils/interop-require.js +24 -0
  188. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  189. package/vendor/nft/out/utils/node-version.js +10 -0
  190. package/vendor/nft/out/utils/node-version.js.map +1 -0
  191. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  192. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  193. package/vendor/nft/out/utils/special-cases.js +347 -0
  194. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  195. package/vendor/nft/out/utils/static-eval.js +579 -0
  196. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  197. package/vendor/nft/out/utils/types.js +2 -0
  198. package/vendor/nft/out/utils/types.js.map +1 -0
  199. package/vendor/nft/out/utils/wrappers.js +659 -0
  200. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  201. package/vendor/nft/package.json +4 -0
  202. package/vendor/nft/upstream.json +51 -0
@@ -64,7 +64,7 @@ export async function renderDocument({ Document = DefaultDocument, App = Default
64
64
  throw new TypeError('Document.getInitialProps must return an object with an html string');
65
65
  }
66
66
  if (Buffer.byteLength(initial.html) > MAX_RESPONSE_BYTES) throw new Error('Response exceeds the 16 MiB PRNext limit');
67
- const payload = { props, appProps, router: snapshot, buildId, route: { pattern: snapshot.pathname }, dynamicIds: [...dynamicModules] };
67
+ const payload = { props, appProps, router: snapshot, buildId, ...(manifest?.config?.deploymentId ? {deploymentId:manifest.config.deploymentId} : {}), route: { pattern: snapshot.pathname }, dynamicIds: [...dynamicModules] };
68
68
  const assetPrefix = manifest?.config?.assetPrefix || manifest?.config?.basePath || '';
69
69
  const nextData = { props: { ...appProps, pageProps: props }, page: snapshot.pathname, query: snapshot.query, buildId,
70
70
  ...(snapshot.locale ? {locale:snapshot.locale,locales:snapshot.locales,defaultLocale:snapshot.defaultLocale} : {}),
package/runtime/edge.mjs CHANGED
@@ -75,14 +75,15 @@ async function createEdgeVM(reactMode) {
75
75
  export async function loadEdgeModule(code, name, reactMode) {
76
76
  if (reactMode !== undefined && reactMode !== 'rsc' && reactMode !== 'ssr') throw new Error('Unknown Edge React mode');
77
77
  let pending;
78
- if (reactMode) {
79
- pending = reactVMs.get(reactMode);
78
+ {
79
+ const key=reactMode||'handler';
80
+ pending = reactVMs.get(key);
80
81
  if (!pending) {
81
82
  pending = createEdgeVM(reactMode);
82
- reactVMs.set(reactMode, pending);
83
- void pending.catch(() => { if (reactVMs.get(reactMode) === pending) reactVMs.delete(reactMode); });
83
+ reactVMs.set(key, pending);
84
+ void pending.catch(() => { if (reactVMs.get(key) === pending) reactVMs.delete(key); });
84
85
  }
85
- } else pending = createEdgeVM();
86
+ }
86
87
  // This runtime module belongs to one compiled build. Its two React realms
87
88
  // are shared lazily by all pages/boundaries; each bundle has its own factory.
88
89
  const vm = await pending;
@@ -1,3 +1,4 @@
1
+ import {withTrace,propagatedFetch} from './trace.mjs';
1
2
  import { createHash } from 'node:crypto';
2
3
  import cache from '../compat/data-cache.cjs';
3
4
  import { dynamicUsage, trackStaticDependency } from '../compat/static-generation.cjs';
@@ -12,6 +13,7 @@ const MAX_MEMO_BYTES = 8 * 1024 * 1024;
12
13
  const CAPTURE_IDLE_MS = 1000;
13
14
  const CAPTURE_LIFETIME_MS = 20_000;
14
15
  let registration;
16
+ const telemetryFetch=(input,init)=>nativeFetch(...propagatedFetch(input,init));
15
17
 
16
18
  class UncacheableResponse extends Error {
17
19
  constructor(response) { super('The fetch response is not cacheable'); this.response = response; }
@@ -305,24 +307,24 @@ export function installFetchCache() {
305
307
  }, error => { diagnostics.record('fetch', { ...fields, error: true, durationMs: performance.now() - started }); throw error; });
306
308
  }
307
309
  }
308
- return nativeFetch(input, init);
310
+ return telemetryFetch(input, init);
309
311
  };
310
312
  Object.defineProperty(fetch, PATCHED, { value: dispatcher });
311
313
  globalThis.fetch = fetch;
312
314
  }
313
315
  if (registration && dispatcher.handlers.includes(registration)) return globalThis.fetch;
314
316
  async function fetch(input, init, context) {
315
- if (context.phase === 'pages' || init?.next?.internal) return nativeFetch(input, init);
317
+ if (context.phase === 'pages' || init?.next?.internal) return telemetryFetch(input, init);
316
318
  if (inCacheScope()) {
317
319
  const next = init?.next || input?.next || {};
318
320
  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');
319
321
  recordCacheDependency(next);
320
- return nativeFetch(input, init);
322
+ return telemetryFetch(input, init);
321
323
  }
322
324
  const policy = fetchPolicy(input, init, context);
323
325
  if (context.staticState) trackStaticDependency({ tags: policy.tags, paths: getCachePaths(context),
324
326
  revalidate: context.cacheConfig?.dynamic === 'force-static' && policy.revalidate === 0 ? null : policy.revalidate }, context);
325
- if (init?.dispatcher || init?.agent) return nativeFetch(input, init);
327
+ if (init?.dispatcher || init?.agent) return telemetryFetch(input, init);
326
328
  const state = cacheState(context);
327
329
  const explicitSignal = init?.signal !== undefined || input instanceof Request;
328
330
  const method = String(init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();
@@ -331,9 +333,9 @@ export function installFetchCache() {
331
333
  const persist = policy.persist && !hardRefresh && !context.draftMode && !context.cacheConfig?.forceNoStore;
332
334
  if (diagnostics.enabled && !persist) diagnostics.record('cache', { route: diagnostics.route(context), state: 'bypass',
333
335
  reason: context.draftMode ? 'draft mode' : hardRefresh ? 'development hard refresh' : context.cacheConfig?.forceNoStore ? 'route disables caching' : 'fetch policy does not opt into persistent caching' });
334
- if (!persist && !memoize) return nativeFetch(input, init);
336
+ if (!persist && !memoize) return telemetryFetch(input, init);
335
337
  const prepared = await prepareRequest(input, init);
336
- if (!prepared) return nativeFetch(input, init);
338
+ if (!prepared) return telemetryFetch(input, init);
337
339
  const request = prepared.request;
338
340
  request.signal.throwIfAborted();
339
341
  const memo = memoize ? memoState(state) : null;
@@ -361,7 +363,7 @@ export function installFetchCache() {
361
363
  entry = undefined;
362
364
  }
363
365
  const producer = async ({ background = false, signal, cache: canPersist = true } = {}) => {
364
- const response = await nativeFetch(request, background && signal ? { signal: AbortSignal.any([request.signal, signal]) } : undefined);
366
+ const response = await telemetryFetch(request, background && signal ? { signal: AbortSignal.any([request.signal, signal]) } : undefined);
365
367
  // Set-Cookie responses must not replay a session mutation, even when the
366
368
  // request itself explicitly opted into shared caching.
367
369
  if (response.headers.has('set-cookie') || (persist && response.status !== 200) || (!canPersist && !entry)) throw new UncacheableResponse(response);
@@ -384,7 +386,7 @@ export function installFetchCache() {
384
386
  throw error;
385
387
  }
386
388
  }
387
- registration = { context: optionalRequestContext, fetch };
389
+ registration = { context: optionalRequestContext, fetch:(input,init,context)=>withTrace({kind:3},'prnext.fetch',()=>fetch(input,init,context)) };
388
390
  dispatcher.handlers.push(registration);
389
391
  return globalThis.fetch;
390
392
  }
@@ -0,0 +1,74 @@
1
+ import { randomBytes, createHmac } from 'node:crypto';
2
+ import { deflateRawSync } from 'node:zlib';
3
+ import { TYPE, MAX_VALUE, collectStream, deltaStream } from '../compat/flight-delta.cjs';
4
+
5
+ export function createFlightStore({maxBytes=32*1024*1024,maxEntries=64,ttl=30000,maxCaptures=8,maxEncoders=2,now=Date.now,setTimer=setTimeout,clearTimer=clearTimeout}={}) {
6
+ if(!Number.isFinite(ttl)||ttl<0||typeof now!=='function'||typeof setTimer!=='function'||typeof clearTimer!=='function')throw new TypeError('Invalid Flight store expiration policy');
7
+ const entries=new Map();let size=0,captures=0,encoders=0,timer,timerExpires;
8
+ function reserve(kind){if(kind==='capture'?captures>=maxCaptures:encoders>=maxEncoders)return;kind==='capture'?captures++:encoders++;let released=false;return()=>{if(released)return;released=true;kind==='capture'?captures--:encoders--;};}
9
+ function remove(token){const entry=entries.get(token);if(entry){size-=entry.bytes.length;entries.delete(token);}}
10
+ function schedule(at){
11
+ let expires=Infinity;
12
+ for(const entry of entries.values())expires=Math.min(expires,entry.expires);
13
+ if(timer!==undefined&&timerExpires===expires)return;
14
+ if(timer!==undefined)clearTimer(timer);
15
+ timer=timerExpires=undefined;
16
+ if(!entries.size)return;
17
+ timerExpires=expires;
18
+ // One timer for the bounded store, not one per response. A cancelled cache
19
+ // entry must not leave its former deadline armed. Clamp long TTLs to Node's
20
+ // timer range; an early wake simply checks the authoritative clock again.
21
+ const handle=setTimer(()=>{
22
+ if(timer!==handle)return;
23
+ timer=timerExpires=undefined;prune(true);
24
+ },Math.min(2147483647,Math.max(1,expires-at)));
25
+ timer=handle;timer?.unref?.();
26
+ }
27
+ function prune(changed=false){
28
+ const at=now();
29
+ for(const[token,entry]of entries)if(entry.expires<=at){remove(token);changed=true;}
30
+ while(size>maxBytes||entries.size>maxEntries){remove(entries.keys().next().value);changed=true;}
31
+ // Ordinary hits keep the same timer without another full entry scan.
32
+ if(changed)schedule(at);
33
+ }
34
+ return {
35
+ get(token,identity){prune();const entry=entries.get(token);if(entry?.identity!==identity)return;return entry?.bytes;},
36
+ set(token,identity,bytes){if(bytes.length>MAX_VALUE)return;remove(token);entries.set(token,{identity,bytes,expires:now()+ttl});size+=bytes.length;prune(true);},
37
+ capture:()=>reserve('capture'),encoder:()=>reserve('encoder'),get inFlight(){return {captures,encoders};},
38
+ get bytes(){return size;},get size(){return entries.size;},
39
+ };
40
+ }
41
+ const store=createFlightStore();
42
+ export function compressedDeltaPolicy(acceptEncoding='') {
43
+ const compressed=acceptEncoding.split(',').some(value=>{
44
+ const [name,...parameters]=value.trim().toLowerCase().split(';');
45
+ return /^(gzip|deflate|br|zstd|\*)$/.test(name)&&!parameters.some(p=>/^\s*q\s*=\s*0(?:\.0*)?\s*$/.test(p));
46
+ });
47
+ if(!compressed)return;
48
+ // This conservative, bounded probe is not the response compressor. It avoids
49
+ // choosing thousands of references to JSON that gzip already compresses well.
50
+ // A large/expensive chunk keeps ordinary Flight instead of delaying a flush.
51
+ return (raw,encoded)=>raw.length<=64*1024&&encoded.length+32<raw.length&&
52
+ deflateRawSync(encoded,{level:1}).length+32<deflateRawSync(raw,{level:1}).length*0.9;
53
+ }
54
+ export async function differentialFlight(response,options,cache=store) {
55
+ const headers=new Headers(options.headers), outgoing=new Headers(response.headers);
56
+ if((options.method||'GET')!=='GET'||headers.get('rsc')!=='1'||headers.get('x-prnext-flight-accept')!=='v1'||headers.get('x-prnext-prefetch')==='1'||response.status!==200||!outgoing.get('content-type')?.startsWith('text/x-component'))return response;
57
+ const secret=options.manifest?.app?.actionKey;if(!secret)return response;
58
+ const identity=createHmac('sha256',secret).update(JSON.stringify([options.manifest.cacheId,headers.get('cookie')||'',headers.get('authorization')||''])).digest('hex');
59
+ const releaseCapture=cache.capture(),token=releaseCapture?randomBytes(24).toString('hex'):undefined,previous=headers.get('x-prnext-flight-base'),candidate=cache.get(previous,identity),releaseEncoder=candidate&&cache.encoder(),base=releaseEncoder?candidate:undefined;
60
+ let encoded;
61
+ try{
62
+ const raw=response.body;
63
+ const body=raw instanceof ReadableStream?raw:raw?.[Symbol.asyncIterator]?ReadableStream.from(raw):new ReadableStream({start(controller){
64
+ controller.enqueue(raw instanceof Uint8Array?raw:raw instanceof ArrayBuffer?new Uint8Array(raw):new TextEncoder().encode(String(raw??'')));controller.close();
65
+ }});
66
+ const collected=collectStream(body,{capture:!!releaseCapture,onComplete:bytes=>cache.set(token,identity,bytes),onFinally:releaseCapture});
67
+ encoded=base?await deltaStream(collected,base,{assess:compressedDeltaPolicy(headers.get('accept-encoding')||'')}):{delta:false,body:collected};
68
+ if(!encoded.delta)releaseEncoder?.();
69
+ else encoded.body=collectStream(encoded.body,{capture:false,onFinally:releaseEncoder});
70
+ }catch(error){releaseEncoder?.();releaseCapture?.();throw error;}
71
+ if(token)outgoing.set('x-prnext-flight-token',token);else outgoing.delete('x-prnext-flight-token');outgoing.set('cache-control','private, no-store');outgoing.delete('content-length');
72
+ if(encoded.delta){outgoing.set('content-type',TYPE);outgoing.set('x-prnext-flight-base',previous);}
73
+ return {...response,headers:Object.fromEntries(outgoing),body:encoded.body,cancel:reason=>{releaseEncoder?.();releaseCapture?.();void encoded.body.cancel(reason).catch(()=>{});return response.cancel?.(reason);}};
74
+ }
@@ -0,0 +1,28 @@
1
+ // Share Flight allocations with the SSR decoder, then group only already
2
+ // available bootstrap bytes. Never wait for a future Suspense chunk to fill a
3
+ // block. The queue's pre-shell ceiling remains the overall response budget.
4
+ export function flightTap(source) {
5
+ const reader=source.getReader(),queue=[];
6
+ let head=0,bytes=0,done=false,failure,outputStarted=false,wakeData,wakeSpace,cancelled;
7
+ const finish=error=>{done=true;failure=error;wakeData?.();wakeData=undefined;wakeSpace?.();wakeSpace=undefined;};
8
+ const cancel=reason=>cancelled??=(async()=>{finish(reason);queue.length=0;head=0;bytes=0;await reader.cancel(reason).catch(()=>{});})();
9
+ const body=new ReadableStream({
10
+ async pull(controller){try{
11
+ while(!done&&outputStarted&&bytes>=128*1024)await new Promise(resolve=>{wakeSpace=resolve;});
12
+ if(done){if(failure)controller.error(failure);else controller.close();return;}
13
+ const next=await reader.read();if(next.done){finish();controller.close();return;}
14
+ queue.push(next.value);bytes+=next.value.byteLength;wakeData?.();wakeData=undefined;controller.enqueue(next.value);
15
+ }catch(error){finish(error);controller.error(error);}},cancel,
16
+ },{highWaterMark:0});
17
+ return {body,cancel,startOutput(){outputStarted=true;},async next(){
18
+ while(head===queue.length&&!done)await new Promise(resolve=>{wakeData=resolve;});
19
+ if(head<queue.length){
20
+ const parts=[];let size=0;
21
+ do{const value=queue[head];queue[head++]=undefined;parts.push(value);size+=value.byteLength;}while(head<queue.length&&size+queue[head].byteLength<=32*1024);
22
+ bytes-=size;if(head===queue.length){queue.length=0;head=0;}else if(head>=64){queue.splice(0,head);head=0;}
23
+ wakeSpace?.();wakeSpace=undefined;
24
+ return {value:parts.length===1?parts[0]:Buffer.concat(parts,size),done:false};
25
+ }
26
+ if(failure)throw failure;return {done:true};
27
+ }};
28
+ }
package/runtime/fs.mjs CHANGED
@@ -1,15 +1,22 @@
1
1
  import { rename as move } from 'node:fs/promises';
2
2
  import { setTimeout as delay } from 'node:timers/promises';
3
+ import path from 'node:path';
3
4
 
4
5
  // Windows scanners and recently closed file handles can briefly deny rename.
5
6
  // Keep the atomic swap: never delete the destination to work around a lock.
6
- export async function rename(source, destination) {
7
- const deadline = Date.now() + 2000;
8
- for (let attempt = 0; ; attempt++) {
9
- try { return await move(source, destination); }
10
- catch (error) {
11
- if (process.platform !== 'win32' || !['EPERM', 'EACCES', 'EBUSY'].includes(error.code) || Date.now() >= deadline) throw error;
12
- await delay(Math.min(10 * (attempt + 1), 100));
7
+ export function createAtomicRenamer({moveFile=move,wait=delay,platform=process.platform,now=()=>performance.now(),retryMs=2000}={}) {
8
+ return async function rename(source,destination) {
9
+ const started=now(),deadline=started+retryMs;
10
+ for(let attempt=1;;attempt++){
11
+ try{return await moveFile(source,destination);}
12
+ catch(error){
13
+ if(platform!=='win32'||!['EPERM','EACCES','EBUSY'].includes(error.code)||now()>=deadline){
14
+ error.atomicRename={operation:'rename',code:error.code||'UNKNOWN',attempts:attempt,elapsedMs:Math.max(0,now()-started),destination:path.basename(destination)};
15
+ throw error;
16
+ }
17
+ await wait(Math.min(10*attempt,100,Math.max(0,deadline-now())));
18
+ }
13
19
  }
14
- }
20
+ };
15
21
  }
22
+ export const rename=createAtomicRenamer();
@@ -0,0 +1,27 @@
1
+ /** Reject executable markup instead of silently stripping author content or
2
+ * React reveal instructions. HTML-only documents must already be complete. */
3
+ export function assertHtmlOnlyDocument(html, pattern = '') {
4
+ const fail = reason => { throw new Error(`htmlOnly route ${pattern} emitted ${reason}. Use static HTML elements without JavaScript.`); };
5
+ if (typeof html !== 'string') fail('an invalid HTML document');
6
+ if (/<\s*(?:script|iframe|object|embed)(?=[\s/>])/i.test(html)) fail('a script or embedded browsing context');
7
+ const tags = /<[a-z][^<>"']*(?:(?:"[^"]*"|'[^']*')[^<>"']*)*>/gi;
8
+ const attributes = /([^\s/>=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
9
+ const decode = value => value.replace(/&#(?:x([\da-f]+)|(\d+));?/gi, (_, hex, decimal) => String.fromCodePoint(Math.min(0x10ffff, parseInt(hex || decimal, hex ? 16 : 10)))).replace(/&(?:colon|tab|newline);/gi, entity => ({'&colon;':':','&tab;':'\t','&newline;':'\n'})[entity.toLowerCase()]);
10
+ for (const match of html.matchAll(tags)) {
11
+ const tag = match[0], start = /^<[\w:-]+/.exec(tag)[0].length;
12
+ const values = new Map();
13
+ for (const attribute of tag.slice(start).matchAll(attributes)) {
14
+ const name = attribute[1].toLowerCase(), value = decode(attribute[2] ?? attribute[3] ?? attribute[4] ?? '');
15
+ // HTML ignores later duplicate attributes; preserve the same first value
16
+ // when interpreting link token lists and resource destinations.
17
+ if (!values.has(name)) values.set(name,value);
18
+ if (/^on[a-z]/.test(name) || name === 'srcdoc') fail(`interactive attribute ${name}`);
19
+ if (name === 'name' && value.startsWith('$ACTION_')) fail('a Server Action form');
20
+ if (['href','src','action','formaction','xlink:href'].includes(name)) {
21
+ if (/^javascript:/i.test(value.replace(/[\u0000-\u0020]/g, ''))) fail('a JavaScript URL');
22
+ }
23
+ }
24
+ if (/^<link\b/i.test(tag) && ((values.get('rel') || '').toLowerCase().split(/[\t\n\f\r ]+/).includes('modulepreload') || (values.get('as') || '').trim().toLowerCase() === 'script')) fail('a JavaScript preload');
25
+ }
26
+ return html;
27
+ }
package/runtime/http.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import {reportRequestError} from './instrumentation.mjs';
1
2
  import { createRequire } from 'node:module';
2
3
  // ESM node:http evaluates lazy WebSocket exports and initializes Undici.
3
4
  // Use the module object to avoid that startup cost while preserving HTTP patches.
@@ -186,6 +187,7 @@ export function plainResponse(status, message) {
186
187
  }
187
188
 
188
189
  export function errorResponse(error, production = process.env.NODE_ENV === 'production') {
190
+ reportRequestError(error);
189
191
  console.error('[prnext]', error?.stack || error);
190
192
  const status = [400, 503, 504].includes(error?.statusCode) ? error.statusCode : 500;
191
193
  const message = status === 400 ? 'Bad Request' : status === 503 ? 'Service Unavailable' : status === 504 ? 'Gateway Timeout' : 'Internal Server Error';
@@ -35,6 +35,20 @@ export async function runIncrementalCache(options) {
35
35
  const input = JSON.parse(Buffer.from(options.body, 'base64').toString());
36
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
37
  const context = { ...options, cacheHandler: options.manifest.config.cacheHandler, cacheMaxMemorySize: options.manifest.config.cacheMaxMemorySize };
38
+ if (input.op === 'acquire' || input.op === 'release') {
39
+ const { handler } = await legacy.load(context);
40
+ if (input.op === 'release') {
41
+ if (handler.prnextLeaseVersion === 1 && typeof handler.release === 'function' && input.lease) await legacy.invoke(handler, 'release', [input.key, input.lease], context);
42
+ return empty(204);
43
+ }
44
+ if (handler.prnextLeaseVersion !== 1 || typeof handler.acquire !== 'function') return empty(204);
45
+ let lease;
46
+ try { lease = await legacy.invoke(handler, 'acquire', [input.key, { signal: options.signal }], context); }
47
+ catch (error) { if (error.code === 'PRNEXT_REDIS_BUSY') return empty(425); throw error; }
48
+ const encoded = JSON.stringify(lease);
49
+ if (!lease || encoded.length > 4096 || /[\r\n]/.test(encoded)) throw new Error('Invalid incremental cache generation lease');
50
+ return { ...empty(204), headers: { 'x-prnext-cache-lease': encoded } };
51
+ }
38
52
  if (input.op === 'invalidate') {
39
53
  await legacy.invalidate(context, { tags: input.tags, mode: 'expire' });
40
54
  return empty(204);
@@ -54,7 +68,8 @@ export async function runIncrementalCache(options) {
54
68
  }
55
69
  const tags = [...new Set([...info.tags, ...implicit(info.paths)])];
56
70
  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 });
71
+ const stored = await legacy.setEntry(input.key, value, context, { tags, cacheControl: { revalidate: info.revalidate }, isRoutePPREnabled: false, prnextLease: input.lease });
72
+ if (stored === false) return empty(409);
58
73
  return empty(204);
59
74
  }
60
75
  if (input.op !== 'get') throw new Error('Unknown incremental cache operation');
@@ -73,11 +88,17 @@ export async function runIncrementalCache(options) {
73
88
  data = input.kind === 'APP_PAGE' ? bytes(value.rscData) : Buffer.from(JSON.stringify(value.pageData));
74
89
  }
75
90
  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');
91
+ const softStale = entry.isStale === true;
92
+ const cacheVersion = createHash('sha256').update(JSON.stringify([entry.lastModified, softStale, value.status, value.headers, info])).update(html).update(data).digest('hex');
77
93
  if (input.knownVersion === cacheVersion) return { ...empty(304), headers: { 'x-prnext-cache-version': cacheVersion } };
78
94
  const headers = { ...value.headers }; delete headers['x-next-cache-tags'];
95
+ // A positive expired generation uses native stale-while-revalidate. -1/zero
96
+ // remains the existing hard-expiration path. The cacheVersion above stays
97
+ // stable even when replicas have slightly different wall clocks.
98
+ const revalidate = softStale && info.revalidate === false ? 1 : info.revalidate;
99
+ const lastModified = softStale ? Math.max(0, Math.min(Date.now(), entry.lastModified) - Math.max(1, revalidate || 1) * 1000 - 1) : entry.lastModified;
79
100
  return { status: value.status, headers, body: [html, data],
80
- isr: { ...(input.kind === 'APP_ROUTE' ? { kind: 'route' } : {}), revalidate: entry.lastModified < 0 ? 0 : info.revalidate,
101
+ isr: { ...(input.kind === 'APP_ROUTE' ? { kind: 'route' } : {}), revalidate: entry.lastModified < 0 ? 0 : revalidate,
81
102
  htmlLength: html.length, dataLength: data.length, tags: info.tags, paths: info.paths,
82
- lastModified: entry.lastModified, cacheVersion } };
103
+ lastModified, cacheVersion } };
83
104
  }
@@ -0,0 +1,49 @@
1
+ import {withTrace,markTraceError} from './trace.mjs';
2
+ import {readFile} from 'node:fs/promises';
3
+ import {AsyncLocalStorage} from 'node:async_hooks';
4
+ import {pathToFileURL} from 'node:url';
5
+ import path from 'node:path';
6
+ import {isControlError} from './navigation.mjs';
7
+
8
+ const modules=new Map(),scope=new AsyncLocalStorage(),reported=new WeakSet();
9
+ function bounded(promise,milliseconds,label){
10
+ let timer;
11
+ return Promise.race([Promise.resolve(promise),new Promise((_,reject)=>{timer=setTimeout(()=>reject(Error(label+' timed out')),milliseconds);})]).finally(()=>clearTimeout(timer));
12
+ }
13
+ export function initializeInstrumentation(distDir,manifest,{realm='node',reactMode,loader}={}) {
14
+ const file=manifest.instrumentation?.[realm];if(!file)return Promise.resolve({});
15
+ const filename=path.resolve(distDir,file),key=filename+':'+(reactMode||realm);let pending=modules.get(key);
16
+ loader??=realm==='edge'?async file=>{const {loadEdgeModule}=await import('./edge.mjs');const asset=file.replace(/\.mjs$/,'.edge.js');return loadEdgeModule(await readFile(asset,'utf8'),asset,reactMode);}:file=>import(pathToFileURL(file).href);
17
+ if(!pending){
18
+ pending=(async()=>{const module=await loader(filename);if(module.register!==undefined&&typeof module.register!=='function')throw Error('instrumentation.register must be a function');if(module.onRequestError!==undefined&&typeof module.onRequestError!=='function')throw Error('instrumentation.onRequestError must be a function');await bounded(Promise.resolve().then(()=>module.register?.()),30000,'instrumentation.register');return module;})();
19
+ // Retain failures too: a failed initialization must never run application code.
20
+ modules.set(key,pending);
21
+ }
22
+ return pending;
23
+ }
24
+ export function wasRequestErrorReported(error){return !!error&&typeof error==='object'&&(error.instrumentationReported===true||reported.has(error));}
25
+ export function acknowledgeRequestError(error){if(error&&typeof error==='object'){scope.getStore()?.seen.add(error);reported.add(error);}markTraceError();}
26
+ export function reportRequestError(error,extra={}) {
27
+ if(isControlError(error))return;markTraceError();
28
+ const state=scope.getStore();if(error?.instrumentationReported===true||!state?.module.onRequestError||state.reporting)return;
29
+ const value=error&&typeof error==='object'?error:new Error(String(error));
30
+ if(state.seen.has(value)||state.seen.size>=32)return;state.seen.add(value);reported.add(value);
31
+ const pending=scope.run({...state,reporting:true},()=>bounded(Promise.resolve().then(()=>state.module.onRequestError(value,state.request,{...state.context,...extra})),5000,'instrumentation.onRequestError')).catch(()=>{console.error('[prnext] instrumentation.onRequestError failed');});
32
+ state.pending.add(pending);void pending.finally(()=>state.pending.delete(pending));return pending;
33
+ }
34
+ export async function withInstrumentation(options,callback,{realm='node',module}={}) {
35
+ const edge=options.route?.cacheConfig?.runtime==='edge'||options.route?.handlerConfig?.runtime==='edge'||options.route?.runtime==='edge';
36
+ const renderRealm=realm;
37
+ if(edge)realm='edge';
38
+ module??=await initializeInstrumentation(options.distDir,options.manifest||{},{realm,reactMode:edge&&options.route?.router==='app'&&options.route?.kind==='page'?(renderRealm==='rsc'?'rsc':'ssr'):undefined});
39
+ const traced=()=>withTrace(options,renderRealm==='rsc'?'prnext.rsc':'prnext.node',callback);
40
+ if(!module.onRequestError)return traced();
41
+ const url=new URL(options.originalUrl||options.url||'http://localhost/'),route=options.route||{};
42
+ const request=Object.freeze({path:url.pathname+url.search,method:options.method||'GET',headers:Object.freeze(Object.fromEntries(new Headers(options.headers)))});
43
+ const context={routerKind:route.router==='app'?'App Router':'Pages Router',routePath:route.pattern||options.routePattern||url.pathname,
44
+ routeType:options.action||new Headers(options.headers).has('next-action')?'action':route.kind==='middleware'?'proxy':route.kind==='api'?'route':'render',
45
+ renderSource:renderRealm==='rsc'?'react-server-components':new Headers(options.headers).get('rsc')==='1'?'react-server-components-payload':'server-rendering',
46
+ revalidateReason:['stale','on-demand'].includes(options.revalidateReason)?options.revalidateReason:undefined,renderType:options.partialFlight?'dynamic-resume':'dynamic'};
47
+ const state={module,request,context,seen:new Set(),pending:new Set()};
48
+ return scope.run(state,async()=>{try{return await traced();}catch(error){if(!options.signal?.aborted||error?.statusCode===504)reportRequestError(error);throw error;}finally{await Promise.all([...state.pending]);}});
49
+ }
@@ -0,0 +1,38 @@
1
+ // Only emitted by the opt-in development compiler. Successful imports retain
2
+ // native ESM caching; failed compilation is retryable after a source correction.
3
+ const pending=new Map(),styles=new Map();
4
+ function stylesheet(url){
5
+ const existing=[...document.querySelectorAll('link[rel="stylesheet"]')].find(link=>link.href===new URL(url,location.href).href);
6
+ if(existing?.sheet)return Promise.resolve();
7
+ if(existing&&styles.has(url))return styles.get(url);
8
+ const promise=new Promise((resolve,reject)=>{
9
+ const link=existing||document.createElement('link');
10
+ const timer=setTimeout(()=>finish(Error('Deferred stylesheet timed out')),30000);
11
+ const finish=error=>{clearTimeout(timer);link.removeEventListener('load',loaded);link.removeEventListener('error',failed);if(error){styles.delete(url);if(!existing)link.remove();reject(error);}else resolve();};
12
+ const loaded=()=>finish(),failed=()=>finish(Error('Deferred stylesheet failed to load'));
13
+ link.addEventListener('load',loaded,{once:true});link.addEventListener('error',failed,{once:true});
14
+ if(!existing){link.rel='stylesheet';link.href=url;document.head.appendChild(link);}
15
+ });
16
+ styles.set(url,promise);
17
+ void promise.then(()=>styles.delete(url),()=>styles.delete(url));
18
+ return promise;
19
+ }
20
+ export function lazyImport(id,basePath=''){
21
+ const key=basePath+':'+id;
22
+ if(pending.has(key))return pending.get(key);
23
+ const task=(async()=>{
24
+ const path='/_prnext/lazy/'+id;
25
+ const response=await fetch(`${basePath}/_prnext/ensure?path=${encodeURIComponent(path)}`,{cache:'no-store',credentials:'same-origin',signal:AbortSignal.timeout(120000)});
26
+ if(!response.ok)throw Error(await response.text());
27
+ const value=await response.json(),target=value.lazy;
28
+ if(!target||typeof target.url!=='string'||!Array.isArray(target.css))throw Error('Invalid deferred import response');
29
+ for(const url of [target.url,...target.css]){
30
+ const parsed=new URL(url,location.href);
31
+ if(parsed.origin!==location.origin||!parsed.pathname.includes('/_prnext/assets/'))throw Error('Invalid deferred import asset URL');
32
+ }
33
+ await Promise.all(target.css.map(stylesheet));
34
+ const module=await import(target.url);
35
+ return target.native?module:module.__prnextLazyModule;
36
+ })().finally(()=>pending.delete(key));
37
+ pending.set(key,task);return task;
38
+ }
@@ -0,0 +1,36 @@
1
+ import offline from '../compat/offline-store.cjs';
2
+
3
+ export function networkFailure(error){
4
+ return error?.name==='NetworkError'||((error instanceof TypeError||error?.name==='TypeError')&&/fetch|network|load failed|terminated/i.test(error.message||''))||/^Connection closed\.?$/.test(error?.message||'');
5
+ }
6
+ function waitForConnection({signal,events,online,milliseconds,waitingOffline}){
7
+ return new Promise((resolve,reject)=>{
8
+ const finish=callback=>value=>{clearTimeout(timer);signal?.removeEventListener('abort',aborted);events?.removeEventListener('online',connected);callback(value);};
9
+ const done=finish(resolve),fail=finish(reject),aborted=()=>fail(signal.reason||new DOMException('Navigation cancelled','AbortError'));
10
+ const connected=()=>{if(online())done();};
11
+ const timer=setTimeout(()=>done(),milliseconds);
12
+ signal?.addEventListener('abort',aborted,{once:true});events?.addEventListener('online',connected);
13
+ if(signal?.aborted)aborted();
14
+ else if(waitingOffline&&online())done();
15
+ });
16
+ }
17
+ // Called exclusively around navigation GETs. Server Actions never enter this
18
+ // path: a lost response does not prove their mutation failed to execute.
19
+ export async function recoverNavigation(read,{signal,events=globalThis.window,online=()=>globalThis.navigator?.onLine!==false,budgetMs=30000,maxAttempts=3}={}){
20
+ const until=performance.now()+budgetMs;let waiting,lastError,attempts=0;
21
+ const failure=()=>Object.assign(new Error('Unable to load the page. Check your connection and try again.',{cause:lastError}),{code:'PRNEXT_NAVIGATION_NETWORK'});
22
+ const deadline=new AbortController(),combined=signal?AbortSignal.any([signal,deadline.signal]):deadline.signal;
23
+ const timer=setTimeout(()=>deadline.abort(failure()),budgetMs);
24
+ try{
25
+ for(;;){
26
+ combined.throwIfAborted();
27
+ if(online()||attempts===0){
28
+ try{return await read(combined);}catch(error){if(combined.aborted)throw combined.reason;if(!networkFailure(error))throw error;lastError=error;attempts++;waiting||=offline.beginWait();}
29
+ }
30
+ const remaining=until-performance.now();
31
+ if(remaining<=0||attempts>=maxAttempts)throw failure();
32
+ const waitingOffline=!online();
33
+ await waitForConnection({signal:combined,events,online,waitingOffline,milliseconds:Math.min(remaining,waitingOffline?remaining:250*2**Math.max(0,attempts-1))});
34
+ }
35
+ }finally{clearTimeout(timer);waiting?.();}
36
+ }
@@ -14,3 +14,10 @@ export function navigationResponse(error) {
14
14
  if (!['http:', 'https:'].includes(parsed.protocol)) return null;
15
15
  return { status, headers: { location: destination, 'cache-control': 'no-store' }, body: Buffer.alloc(0) };
16
16
  }
17
+
18
+ /** Expected routing/render controls must not become application error reports. */
19
+ export function isControlError(error) {
20
+ return !!navigationResponse(error) || error?.digest === 'BAILOUT_TO_CLIENT_SIDE_RENDERING'
21
+ || error?.code === 'PRNEXT_DYNAMIC_SERVER_USAGE' || error?.code === 'PRNEXT_PPR_DYNAMIC'
22
+ || error?.digest === 'PRNEXT_PPR_DYNAMIC';
23
+ }
@@ -0,0 +1,47 @@
1
+ import {Writable} from 'node:stream';
2
+ import {renderToPipeableStream} from 'react-dom/server';
3
+
4
+ // One bounded Node stream, without a second Web reader queue. Partial React
5
+ // continuations keep using their original Web API and opaque resume state.
6
+ export function nodeHtmlReader(element,options={}){
7
+ return new Promise((resolve,reject)=>{
8
+ const {signal,...renderOptions}=options;
9
+ let renderer,settled=false,finished=false,slot,pending,acknowledge,failure;
10
+ const cleanup=()=>signal?.removeEventListener('abort',aborted);
11
+ const deliver=()=>{
12
+ if(!pending)return;
13
+ const {resolve,reject}=pending;
14
+ if(failure){pending=undefined;reject(failure);}
15
+ else if(slot){pending=undefined;const current=slot;slot=undefined;acknowledge=current.done;resolve({value:current.chunk,done:false});}
16
+ else if(finished){pending=undefined;resolve({done:true});}
17
+ };
18
+ // A single writable replaces PassThrough's writable+readable machinery.
19
+ // Keep its write callback until the next read: React may reuse a chunk only
20
+ // after its consumer has finished, and native backpressure stays bounded.
21
+ const stream=new Writable({highWaterMark:64*1024,
22
+ write(chunk,_encoding,done){slot={chunk,done};deliver();},
23
+ final(done){finished=true;cleanup();deliver();done();},
24
+ });
25
+ const fail=error=>{failure=error;if(!settled){settled=true;cleanup();reject(error);}deliver();stream.destroy(error);};
26
+ stream.on('error',error=>{failure=error;cleanup();deliver();});
27
+ const aborted=()=>{const error=signal.reason||new Error('HTML render was cancelled');renderer?.abort(error);fail(error);};
28
+ if(signal?.aborted){aborted();return;}
29
+ signal?.addEventListener('abort',aborted,{once:true});
30
+ try{
31
+ renderer=renderToPipeableStream(element,{...renderOptions,onShellError:fail,onShellReady(){
32
+ if(settled)return;settled=true;
33
+ resolve({
34
+ read(){
35
+ if(pending)return Promise.reject(new Error('HTML reads must be sequential.'));
36
+ const done=acknowledge;acknowledge=undefined;done?.();
37
+ return new Promise((resolve,reject)=>{pending={resolve,reject};deliver();});
38
+ },
39
+ async cancel(reason){if(finished)return;finished=true;cleanup();renderer.abort(reason);const done=acknowledge||slot?.done;acknowledge=undefined;slot=undefined;stream.destroy();done?.();deliver();},
40
+ releaseLock(){if(finished)cleanup();},
41
+ });
42
+ renderer.pipe(stream);
43
+ }});
44
+ }catch(error){fail(error);}
45
+ });
46
+ }
47
+ export const useNodeHtml=()=>process.env.PRNEXT_SSR_STREAM==='node';
@@ -1,3 +1,4 @@
1
+ import { deploymentHeaders, assertDeploymentResponse } from './deployment-client.mjs';
1
2
  import React from 'react';
2
3
  import { hydrateRoot } from 'react-dom/client';
3
4
  import Router, { RouterProvider, useRouter, installPagesRouter } from '../compat/router.cjs';
@@ -62,9 +63,10 @@ async function readPageData(response, limit = MAX_DATA_BYTES, { discard = false,
62
63
  }
63
64
 
64
65
  export async function readPageDataResponse(response, visibleURL, {
65
- maxBytes = MAX_DATA_BYTES, includeBytes = false, fetcher = globalThis.fetch, signal, requestURL = visibleURL,
66
+ maxBytes = MAX_DATA_BYTES, includeBytes = false, deploymentId, fetcher = globalThis.fetch, signal, requestURL = visibleURL,
66
67
  } = {}) {
67
68
  try {
69
+ await assertDeploymentResponse(response, deploymentId);
68
70
  let legacy = false;
69
71
  for (let redirects = 0; response.headers.get('x-prnext-legacy-navigation') === '1'; redirects++) {
70
72
  legacy = true;
@@ -79,7 +81,8 @@ export async function readPageDataResponse(response, visibleURL, {
79
81
  await response.body?.cancel();
80
82
  signal?.throwIfAborted();
81
83
  response = await fetcher(target.href, { credentials: 'same-origin', redirect: 'follow', signal,
82
- headers: { accept: 'application/json', 'x-nextjs-data': '1', 'x-prnext-navigation': '1' } });
84
+ headers: { ...deploymentHeaders(deploymentId), accept: 'application/json', 'x-nextjs-data': '1', 'x-prnext-navigation': '1' } });
85
+ await assertDeploymentResponse(response, deploymentId);
83
86
  requestURL = target.href;
84
87
  }
85
88
  if (legacy && !response.ok && (response.status < 300 || response.status >= 400)) throw new Error(`Page data request failed (${response.status})`);
@@ -140,9 +143,10 @@ function pagesView(props) {
140
143
  }
141
144
 
142
145
  let application;
143
- async function startPages({ Page, App, pattern, manifestUrl, basePath = '', trailingSlash = false, skipTrailingSlashRedirect = false, assetBase, strictMode = false }) {
146
+ async function startPages({ deploymentId, Page, App, pattern, manifestUrl, basePath = '', trailingSlash = false, skipTrailingSlashRedirect = false, assetBase, strictMode = false }) {
144
147
  const data = window.__PRNEXT_DATA__;
145
148
  if (!data?.router || !data.props) throw new Error('Missing PRNext page data');
149
+ if (deploymentId && deploymentId !== data.deploymentId) throw new Error('The document and its JavaScript belong to different deployments.');
146
150
  const rewrite = readRewriteMarker(document);
147
151
  const initial = { ...data, router: { ...data.router, basePath, trailingSlash, skipTrailingSlashRedirect }, Page, App, ...(rewrite ? { rewrite } : {}) };
148
152
  let buildId = data.buildId;
@@ -163,13 +167,13 @@ async function startPages({ Page, App, pattern, manifestUrl, basePath = '', trai
163
167
  if (signal.aborted) cancel();
164
168
  const timer = setTimeout(() => abort.abort(new Error('Page data request timed out')), 30_000);
165
169
  try {
166
- const requestURL = pageDataURL(buildId, url.href, basePath);
170
+ const requestURL = pageDataURL(window.__PRNEXT_STATIC_EXPORT__ && deploymentId ? buildId + '~' + deploymentId : buildId, url.href, basePath);
167
171
  const response = await fetcher(requestURL, {
168
172
  credentials: 'same-origin', redirect: 'manual', signal: abort.signal,
169
- headers: { accept: 'application/json', 'x-nextjs-data': '1', 'x-prnext-navigation': '1', ...(speculative ? { purpose: 'prefetch' } : {}) },
173
+ headers: { ...deploymentHeaders(deploymentId), accept: 'application/json', 'x-nextjs-data': '1', 'x-prnext-navigation': '1', ...(speculative ? { purpose: 'prefetch' } : {}) },
170
174
  });
171
175
  return await readPageDataResponse(response, url.href, { maxBytes: speculative ? 2 * 1024 * 1024 : MAX_DATA_BYTES,
172
- includeBytes: speculative, fetcher, signal: abort.signal, requestURL });
176
+ deploymentId: window.__PRNEXT_STATIC_EXPORT__ ? undefined : deploymentId, includeBytes: speculative, fetcher, signal: abort.signal, requestURL });
173
177
  } finally { clearTimeout(timer); signal.removeEventListener('abort', cancel); }
174
178
  },
175
179
  });
@@ -201,6 +205,9 @@ export function bootstrapPages(options) {
201
205
  if (process.env.NODE_ENV === 'development' && options.dev && globalThis.__PRNEXT_DEV__) {
202
206
  return globalThis.__PRNEXT_DEV__.bootstrap('pages', options, () => startPages(options));
203
207
  }
204
- application ||= startPages(options);
208
+ if (!application) {
209
+ application = startPages(options);
210
+ void application.catch(error => console.error('[PRNext] Pages startup failed:', error));
211
+ }
205
212
  return application;
206
213
  }