@thomas.f/prnext 0.1.16-alpha → 0.1.18-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 (215) hide show
  1. package/AI.md +63 -0
  2. package/README.md +35 -503
  3. package/build/ADAPTERS.md +27 -2
  4. package/build/actions.mjs +6 -1
  5. package/build/analyze.mjs +340 -0
  6. package/build/app-config.mjs +20 -8
  7. package/build/app-static.mjs +41 -14
  8. package/build/app-styles.mjs +79 -0
  9. package/build/app.mjs +99 -31
  10. package/build/build-lock.mjs +6 -6
  11. package/build/bundle-runtime.mjs +39 -11
  12. package/build/cache-components.mjs +1 -1
  13. package/build/cache-handlers.mjs +1 -1
  14. package/build/check.mjs +56 -11
  15. package/build/compact-css.mjs +53 -0
  16. package/build/compilation-cache.mjs +269 -0
  17. package/build/compilation-store.mjs +142 -0
  18. package/build/compiler.mjs +51 -4
  19. package/build/concurrent.mjs +12 -0
  20. package/build/config.mjs +50 -13
  21. package/build/create.mjs +239 -0
  22. package/build/css-modules.mjs +65 -10
  23. package/build/deployment-assets.mjs +89 -0
  24. package/build/deployment-config.mjs +18 -0
  25. package/build/dev-compiler-health.mjs +11 -0
  26. package/build/dev-css-update.mjs +85 -0
  27. package/build/dev-dependencies.mjs +44 -0
  28. package/build/dev-discovery.mjs +28 -0
  29. package/build/dev-worker.mjs +14 -5
  30. package/build/dev.mjs +64 -6
  31. package/build/dynamic.mjs +1 -1
  32. package/build/edge.mjs +5 -4
  33. package/build/fonts.mjs +1 -1
  34. package/build/framework-imports.mjs +50 -0
  35. package/build/html-only-config.mjs +53 -0
  36. package/build/html-only-graph.mjs +67 -0
  37. package/build/incremental.mjs +8 -4
  38. package/build/index.mjs +140 -112
  39. package/build/inspect.mjs +3 -0
  40. package/build/inspection-summary.mjs +37 -4
  41. package/build/instrumentation.mjs +30 -0
  42. package/build/loader-cache.mjs +53 -26
  43. package/build/metadata-routes.mjs +3 -4
  44. package/build/migrate.mjs +60 -57
  45. package/build/migration-audit.mjs +1 -1
  46. package/build/native-type-config.mjs +37 -0
  47. package/build/npm-packages.mjs +3 -18
  48. package/build/package-manager.mjs +308 -0
  49. package/build/persistent-cache.mjs +112 -0
  50. package/build/precompress.mjs +62 -30
  51. package/build/prerender-cache.mjs +70 -0
  52. package/build/prerender-config.mjs +29 -0
  53. package/build/prerender-pages.mjs +100 -0
  54. package/build/prerender-pool.mjs +165 -0
  55. package/build/prerender-process-worker.mjs +58 -0
  56. package/build/prerender-worker.mjs +10 -0
  57. package/build/production-builder.mjs +129 -0
  58. package/build/react-compiler-report.mjs +225 -0
  59. package/build/react-compiler.mjs +139 -0
  60. package/build/runtime-contract.mjs +2 -2
  61. package/build/scan.mjs +4 -4
  62. package/build/source-analysis.mjs +42 -0
  63. package/build/static-adapter.mjs +13 -5
  64. package/build/static-export.mjs +4 -4
  65. package/build/static-input.mjs +48 -0
  66. package/build/styles.mjs +17 -3
  67. package/build/transform-cache.mjs +37 -4
  68. package/build/transform.mjs +1 -1
  69. package/build/type-command.mjs +41 -0
  70. package/build/typed-routes.mjs +95 -0
  71. package/build/typescript-dev-worker.mjs +18 -0
  72. package/build/typescript-dev.mjs +49 -0
  73. package/build/typescript-native-worker.mjs +55 -0
  74. package/build/typescript-native.mjs +18 -0
  75. package/build/typescript-toolchain.mjs +19 -0
  76. package/build/typescript-watch.mjs +22 -0
  77. package/build/typescript.mjs +11 -8
  78. package/build/webpack-graph.mjs +170 -30
  79. package/build/worker-memory.mjs +9 -0
  80. package/build/worker-preload.mjs +9 -0
  81. package/cli.mjs +162 -122
  82. package/compat/activity-cache.cjs +26 -0
  83. package/compat/after.cjs +6 -4
  84. package/compat/app-layout-context.cjs +52 -2
  85. package/compat/base64.cjs +10 -0
  86. package/compat/cache-handlers.cjs +12 -2
  87. package/compat/cache-wire.cjs +28 -0
  88. package/compat/cache.cjs +2 -1
  89. package/compat/cache.d.cts +4 -0
  90. package/compat/cookies.cjs +21 -5
  91. package/compat/data-cache.cjs +12 -8
  92. package/compat/flight-delta.cjs +150 -0
  93. package/compat/headers.cjs +4 -2
  94. package/compat/incremental-cache.cjs +20 -7
  95. package/compat/index.d.ts +89 -10
  96. package/compat/link.cjs +12 -9
  97. package/compat/link.d.cts +4 -3
  98. package/compat/navigation-authorization.cjs +9 -0
  99. package/compat/navigation-stages.cjs +19 -0
  100. package/compat/navigation.d.cts +4 -3
  101. package/compat/offline-store.cjs +11 -0
  102. package/compat/offline.cjs +5 -0
  103. package/compat/offline.d.cts +2 -0
  104. package/compat/redis-cache.cjs +201 -0
  105. package/compat/redis-cache.d.cts +37 -0
  106. package/compat/router.d.cts +4 -3
  107. package/compat/server-primitives.cjs +31 -11
  108. package/compat/server.d.cts +15 -2
  109. package/compat/static-generation.cjs +7 -0
  110. package/compat/typed-route.d.cts +17 -0
  111. package/compat/use-cache.cjs +3 -0
  112. package/docs/ai/compatibility.md +275 -0
  113. package/docs/ai/data.md +288 -0
  114. package/docs/ai/deployment.md +269 -0
  115. package/docs/ai/manifest.json +11 -0
  116. package/docs/ai/quickstart.md +171 -0
  117. package/docs/ai/routing.md +214 -0
  118. package/docs/ai/troubleshooting.md +155 -0
  119. package/index.mjs +2 -0
  120. package/native/diagnostics.mjs +34 -0
  121. package/native/resolve.mjs +7 -2
  122. package/package.json +44 -7
  123. package/release/ai-docs.mjs +48 -0
  124. package/release/verify.mjs +2 -0
  125. package/runtime/action-client.mjs +10 -4
  126. package/runtime/action-server.mjs +2 -0
  127. package/runtime/ai-docs.mjs +202 -0
  128. package/runtime/app-client.mjs +226 -68
  129. package/runtime/app-content.mjs +4 -3
  130. package/runtime/app-partial-client.mjs +3 -0
  131. package/runtime/app-partial.mjs +1 -1
  132. package/runtime/app-prefetch-constants.mjs +4 -0
  133. package/runtime/app-prefetch.mjs +2 -3
  134. package/runtime/app-render-key.mjs +5 -0
  135. package/runtime/app-render.mjs +100 -28
  136. package/runtime/app-routing.mjs +22 -9
  137. package/runtime/app-static.mjs +18 -8
  138. package/runtime/app-stream-html.mjs +10 -53
  139. package/runtime/app-style-selection.mjs +34 -0
  140. package/runtime/app-styles.mjs +154 -0
  141. package/runtime/cache-components-rsc.mjs +4 -1
  142. package/runtime/cache-request.mjs +2 -1
  143. package/runtime/cloud/archive.mjs +5 -6
  144. package/runtime/compile-cache.mjs +8 -0
  145. package/runtime/deployment-client.mjs +19 -0
  146. package/runtime/dev-ai-brands.mjs +26 -0
  147. package/runtime/dev-ai-style.mjs +99 -0
  148. package/runtime/dev-build-reasons.mjs +25 -0
  149. package/runtime/dev-cache-control.mjs +53 -0
  150. package/runtime/dev-client.mjs +245 -46
  151. package/runtime/dev-compiler-broker.mjs +42 -0
  152. package/runtime/dev-console-copy.mjs +19 -0
  153. package/runtime/dev-console.mjs +106 -0
  154. package/runtime/dev-dependency-graph.mjs +40 -0
  155. package/runtime/dev-element-picker-style.mjs +27 -0
  156. package/runtime/dev-element-picker.mjs +213 -0
  157. package/runtime/dev-error-overlay-style.mjs +41 -0
  158. package/runtime/dev-error-overlay.mjs +167 -0
  159. package/runtime/dev-error-report.mjs +121 -0
  160. package/runtime/dev-file-changes.mjs +1 -1
  161. package/runtime/dev-gateway.mjs +44 -0
  162. package/runtime/dev-host-relay.mjs +66 -0
  163. package/runtime/dev-indicator.mjs +191 -0
  164. package/runtime/dev-issue-motion.mjs +214 -0
  165. package/runtime/dev-lazy-tool.mjs +34 -0
  166. package/runtime/dev-logo.mjs +2 -0
  167. package/runtime/dev-notification.mjs +81 -0
  168. package/runtime/dev-popout.mjs +34 -0
  169. package/runtime/dev-position.mjs +107 -0
  170. package/runtime/dev-preview-touch.mjs +122 -0
  171. package/runtime/dev-runtime-pool.mjs +45 -0
  172. package/runtime/dev-runtime-spare.mjs +57 -0
  173. package/runtime/dev-tools-client.mjs +155 -0
  174. package/runtime/dev-tools-mcp.mjs +126 -0
  175. package/runtime/dev-tools-metrics.mjs +31 -0
  176. package/runtime/dev-tools-model.mjs +42 -0
  177. package/runtime/dev-tools-panel.mjs +341 -0
  178. package/runtime/dev-tools-result.mjs +37 -0
  179. package/runtime/dev-tools-server.mjs +256 -0
  180. package/runtime/dev-tools-source.mjs +104 -0
  181. package/runtime/dev-tooltip.mjs +94 -0
  182. package/runtime/dev-ui-icons.mjs +8 -0
  183. package/runtime/dev-workbench-style.mjs +106 -0
  184. package/runtime/dev-workbench.mjs +428 -0
  185. package/runtime/dev-worker-logs.mjs +31 -0
  186. package/runtime/development.mjs +324 -0
  187. package/runtime/document.mjs +1 -1
  188. package/runtime/edge.mjs +6 -5
  189. package/runtime/env.mjs +2 -1
  190. package/runtime/fetch-cache.mjs +10 -8
  191. package/runtime/flight-delta.mjs +50 -0
  192. package/runtime/flight-tap.mjs +28 -0
  193. package/runtime/html-only.mjs +27 -0
  194. package/runtime/http.mjs +2 -0
  195. package/runtime/incremental-cache.mjs +25 -4
  196. package/runtime/instrumentation.mjs +49 -0
  197. package/runtime/navigation-recovery.mjs +36 -0
  198. package/runtime/navigation.mjs +7 -0
  199. package/runtime/node-html-stream.mjs +47 -0
  200. package/runtime/pages-client.mjs +24 -8
  201. package/runtime/pages-navigation.mjs +54 -64
  202. package/runtime/persistent/client.mjs +6 -5
  203. package/runtime/persistent/daemon.mjs +66 -56
  204. package/runtime/persistent/native-proxy.mjs +63 -0
  205. package/runtime/prefetch-scheduler.mjs +110 -0
  206. package/runtime/preload.mjs +12 -0
  207. package/runtime/profile.mjs +14 -0
  208. package/runtime/profiles.json +2 -2
  209. package/runtime/render.mjs +3 -0
  210. package/runtime/route-pattern.mjs +44 -0
  211. package/runtime/rsc-worker.mjs +41 -12
  212. package/runtime/trace.mjs +78 -0
  213. package/runtime/typed-route.mjs +19 -0
  214. package/runtime/watch-project.mjs +29 -18
  215. package/runtime/worker.mjs +102 -24
@@ -0,0 +1,5 @@
1
+ // Local counters can repeat in independent prerender processes. Cached Flight
2
+ // must not give different documents the same error/navigation reset identity.
3
+ export function renderKeyFor(request, id) {
4
+ return JSON.stringify([new URL(request.originalUrl || request.url || 'http://localhost/').pathname, id]);
5
+ }
@@ -1,6 +1,9 @@
1
+ import {reportRequestError,acknowledgeRequestError} from './instrumentation.mjs';
2
+ import {currentTraceparent} from './trace.mjs';
1
3
  import React from 'react';
4
+ import { randomUUID, createHmac, randomBytes } from 'node:crypto';
2
5
  import diagnostics from '../compat/diagnostics.cjs';
3
- import { runtimeProfile } from './profile.mjs';
6
+ import { rscResourceLimits } from './profile.mjs';
4
7
  import { fontPreloads } from './font-preload.mjs';
5
8
  import { advancedRoutingSources } from './app-routing-state.mjs';
6
9
  import { addBasePath, removeBasePath } from '../compat/paths.cjs';
@@ -29,6 +32,8 @@ import { isDraftRequest } from '../compat/draft.cjs';
29
32
  import { createRscLifecycle } from './rsc-lifecycle.mjs';
30
33
  import { trackRequestWork } from './request-work.mjs';
31
34
  import { createRscWorkWatchdog } from './rsc-work-watchdog.mjs';
35
+ import {assertHtmlOnlyDocument} from './html-only.mjs';
36
+ import {nodeHtmlReader,useNodeHtml} from './node-html-stream.mjs';
32
37
 
33
38
  installFetchCache();
34
39
 
@@ -38,6 +43,7 @@ const metadata = new WeakMap();
38
43
  const artifacts = new WeakMap();
39
44
  const edgeManifests = new WeakMap();
40
45
  const emptyManifest = Object.freeze({});
46
+ const developmentAuthorizationSecret = randomBytes(32).toString('hex');
41
47
  let nextId = 0;
42
48
  const pending = new Map();
43
49
  const noop = () => {};
@@ -54,7 +60,7 @@ const ssrModuleLoaders = new Map();
54
60
  const ssrManifests = new WeakMap();
55
61
  const require = createRequire(import.meta.url);
56
62
  const flightDecoders = new Map();
57
- let htmlRuntime, recoveryRuntime, domRuntime;
63
+ let htmlRuntime, recoveryRuntime, domRuntime, staticRuntime;
58
64
  async function renderProgressiveAppHtml(options) {
59
65
  htmlRuntime ??= await import('./app-stream-html.mjs');
60
66
  return htmlRuntime.renderProgressiveAppHtml(options);
@@ -108,7 +114,7 @@ function getWorker(production) {
108
114
  execArgv: ['--conditions=react-server'], stdout: true, stderr: true,
109
115
  // Production nursery policy; an explicit Node semi-space flag takes
110
116
  // precedence. This does not cap the old generation or process RSS.
111
- ...(production ? { resourceLimits: { maxYoungGenerationSizeMb: runtimeProfile(process.env, production).youngGenerationMiB } } : {}),
117
+ resourceLimits:rscResourceLimits(process.env,production),
112
118
  env: { ...process.env, NODE_ENV: production ? 'production' : 'development' },
113
119
  });
114
120
  workers.set(production, current);
@@ -155,7 +161,7 @@ function getWorker(production) {
155
161
  request.reject(error);
156
162
  }
157
163
  }
158
- current.on('message', ({ id, type, token, chunk, byteOffset, byteLength, done, body, status, rscError, boundaryIndex, headers, error, staticParams, staticMetadata, navigation }) => {
164
+ current.on('message', ({ id, type, token, chunk, byteOffset, byteLength, done, body, status, rscError, boundaryIndex, headers, error, staticParams, staticMetadata, navigation, value }) => {
159
165
  if (type === 'work-responsive') { workWatchdog.ack(token); return; }
160
166
  const request = pending.get(id);
161
167
  if (!request || request.worker !== current) return;
@@ -164,6 +170,7 @@ function getWorker(production) {
164
170
  if (error?.retireWorker) {
165
171
  void lifecycle.terminate(new Error(error.message)).catch(error => console.error('[prnext] RSC worker termination failed:', error));
166
172
  }
173
+ if(type==='instrumentation-error'){if(typeof value==='string'&&value.length<=1024){request.reportedDigests??=new Set();if(request.reportedDigests.size<32)request.reportedDigests.add(value);}return;}
167
174
  if (request.cancelled && type !== 'settled') return;
168
175
  if (type === 'settled') {
169
176
  pending.delete(id); clearTimeout(request.timeout);
@@ -178,7 +185,7 @@ function getWorker(production) {
178
185
  }
179
186
  if (type === 'start') {
180
187
  if (chunk) request.channel.chunk(chunk, byteOffset, byteLength);
181
- request.resolve({ body: request.channel.body, status: status || 200, rscError, boundaryIndex, headers });
188
+ request.resolve({ body: request.channel.body, status: status || 200, rscError, reportedDigests:request.reportedDigests??=new Set(), boundaryIndex, headers });
182
189
  if (!done) return;
183
190
  }
184
191
  if (type === 'chunk') {
@@ -202,6 +209,7 @@ function getWorker(production) {
202
209
  if (type === 'end' || done) request.channel.end();
203
210
  else if (error) {
204
211
  const failure = new Error(error.message);
212
+ if(error.instrumentationReported===true)failure.instrumentationReported=true;
205
213
  if (error.stack) failure.stack = error.stack;
206
214
  if (error.digest) failure.digest = error.digest;
207
215
  if ([400, 504].includes(error.statusCode)) failure.statusCode = error.statusCode;
@@ -217,7 +225,7 @@ function getWorker(production) {
217
225
  request.reject(new Error('Invalid App Router Flight response buffer'));
218
226
  } else {
219
227
  // ArrayBuffer overload: a view of the transferred allocation, no copy.
220
- request.resolve({ body: Buffer.from(body), status: status || 200, rscError, boundaryIndex, headers, staticMetadata, navigation });
228
+ request.resolve({ body: Buffer.from(body), status: status || 200, rscError, reportedDigests:request.reportedDigests??=new Set(), boundaryIndex, headers, staticMetadata, navigation });
221
229
  }
222
230
  // Keep only settlement bookkeeping after sending the body. The
223
231
  // isolate can still be flushing cache writes after its terminal frame.
@@ -233,6 +241,22 @@ function getWorker(production) {
233
241
  current.unref();
234
242
  return current;
235
243
  }
244
+ export async function prewarmAppRenderer(options){
245
+ flightDecoder(true);
246
+ await preloadAppRoutes([],options);
247
+ }
248
+ export async function preloadAppRoutes(files,options){
249
+ const worker=getWorker(true),id=`preload-${++nextId}`;
250
+ worker.ref();
251
+ try{await new Promise((resolve,reject)=>{
252
+ const clean=()=>{clearTimeout(timer);worker.off('message',message);worker.off('error',error);worker.off('exit',exit);};
253
+ const message=value=>{if(value.type==='preloaded'&&value.id===id){clean();value.error?reject(new Error(value.error)):resolve();}};
254
+ const error=value=>{clean();reject(value);};const exit=()=>error(new Error('RSC worker exited while preloading'));
255
+ const timer=setTimeout(()=>{clean();void lifecycles.get(worker)?.terminate(new Error('RSC preload timed out'));reject(new Error('RSC preload timed out'));},options.timeoutMs);
256
+ worker.on('message',message);worker.once('error',error);worker.once('exit',exit);
257
+ worker.postMessage({type:'preload',id,value:{files,options}});
258
+ });}finally{if(!hasPending(worker)&&!lifecycles.get(worker)?.retiring)worker.unref();}
259
+ }
236
260
 
237
261
  // Stdio workers can retain referenced MessagePorts after rendering (including
238
262
  // diagnostics piped from the RSC isolate). EOF must drain real cache/after()
@@ -240,6 +264,12 @@ function getWorker(production) {
240
264
  // incidental event-loop liveness or terminate unfinished application work.
241
265
  export async function drainAppRuntime() {
242
266
  while (pending.size) await Promise.all([...pending.values()].map(request => request.settled));
267
+ if(process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT||process.env.OTEL_EXPORTER_OTLP_ENDPOINT)await Promise.all([...workers.values()].map(worker=>new Promise(resolve=>{
268
+ const token=randomUUID();let timer;
269
+ const clean=()=>{clearTimeout(timer);worker.off('message',message);worker.off('exit',clean);resolve();};
270
+ const message=value=>{if(value.type==='telemetry-flushed'&&value.token===token)clean();};
271
+ worker.on('message',message);worker.once('exit',clean);timer=setTimeout(clean,3000);worker.postMessage({type:'telemetry-flush',token});
272
+ })));
243
273
  await closeAppRuntime();
244
274
  }
245
275
 
@@ -266,7 +296,8 @@ function requestDeadline(entry, lifecycle, id, hardTimeoutMs, delayMs) {
266
296
  }, delayMs);
267
297
  }
268
298
 
269
- export async function renderFlight(request, { softTimeoutMs, hardTimeoutMs, signal, deferFinalizer } = {}) {
299
+ export async function renderFlight(request, { softTimeoutMs, hardTimeoutMs, signal, deferFinalizer, settleBeforeReturn = false } = {}) {
300
+ if(settleBeforeReturn&&request.stream)throw new Error('Streaming Flight must be consumed before waiting for settlement.');
270
301
  softTimeoutMs ??= request.cacheConfig?.maxDuration ? request.cacheConfig.maxDuration * 1000 : 25_000;
271
302
  hardTimeoutMs ??= request.cacheConfig?.maxDuration ? request.cacheConfig.maxDuration * 1000 : 28_000;
272
303
  const deadline = performance.now() + hardTimeoutMs;
@@ -292,7 +323,11 @@ export async function renderFlight(request, { softTimeoutMs, hardTimeoutMs, sign
292
323
  let settle;
293
324
  const settled = new Promise(resolve => { settle = resolve; });
294
325
  trackRequestWork(settled, signal, { deadline, supervised: true });
295
- const entry = { worker: current, resolve, reject, operation: request.operation, lease, settle, settled };
326
+ // Build callers may close this isolate as soon as their last path returns.
327
+ // Its terminal Flight frame precedes after()/cache completion; keep that
328
+ // path outstanding until the worker acknowledges bounded settlement.
329
+ const resolveResult=settleBeforeReturn?value=>{void settled.then(()=>resolve(value));}:resolve;
330
+ const entry = { worker: current, resolve:resolveResult, reject, operation: request.operation, lease, settle, settled };
296
331
  const timeout = entry.timeout = requestDeadline(entry, lifecycle, id, hardTimeoutMs, Math.max(0, deadline - performance.now()));
297
332
  function cancel(reason) {
298
333
  if (entry.cancelled) return;
@@ -324,7 +359,7 @@ export async function renderFlight(request, { softTimeoutMs, hardTimeoutMs, sign
324
359
  pending.set(id, entry);
325
360
  try {
326
361
  const wire = { ...request, renderTimeoutMs: Math.min(softTimeoutMs, Math.max(1, deadline - performance.now())) };
327
- if (request.action && deferFinalizer) {
362
+ if (request.cacheConfig?.runtime !== 'edge' && deferFinalizer) {
328
363
  wire.waitForResponse = true;
329
364
  deferFinalizer(() => { if (pending.has(id)) current.postMessage({ id, type: 'after-response' }); });
330
365
  }
@@ -383,15 +418,17 @@ export async function decodeFlight(body, clientModules = {}, distDir, { producti
383
418
  return decoder.createFromNodeStream(body instanceof ReadableStream ? Readable.fromWeb(body) : Readable.from([body]), serverConsumerManifest);
384
419
  }
385
420
 
386
- export async function renderHtml(tree, router, { timeoutMs = 25_000, formState = null, staticGeneration = false, nonce, strictMode = true } = {}) {
421
+ export async function renderHtml(tree, router, { timeoutMs = 25_000, formState = null, staticGeneration = false, nonce, strictMode = true, htmlOnly = false, reportedDigests } = {}) {
387
422
  const abort = new AbortController();
388
423
  const timeout = setTimeout(() => abort.abort(appTimeoutError('HTML render', timeoutMs)), timeoutMs);
389
424
  let renderError;
390
425
  try {
391
426
  domRuntime ??= await import('react-dom/server');
392
- const stream = await domRuntime.renderToReadableStream(React.createElement(strictMode ? React.StrictMode : React.Fragment, null, React.createElement(AppRouterProvider, { router, nonce }, appContent(tree))), {
427
+ const element = React.createElement(strictMode ? React.StrictMode : React.Fragment, null, React.createElement(AppRouterProvider, { router, nonce }, appContent(tree)));
428
+ const renderOptions = {
393
429
  nonce,
394
430
  signal: abort.signal, onError(error) {
431
+ if(!abort.signal.aborted){if(reportedDigests?.has(error?.digest))acknowledgeRequestError(error);else reportRequestError(error);}
395
432
  if (isDynamicBailout(error)) return error.digest;
396
433
  // Suspense can recover client SSR failures by emitting its fallback and
397
434
  // retrying in the browser. Static generation must still reject them:
@@ -401,9 +438,15 @@ export async function renderHtml(tree, router, { timeoutMs = 25_000, formState =
401
438
  return appErrorDigest(error);
402
439
  },
403
440
  formState,
404
- });
405
- const reader = stream.getReader();
441
+ };
442
+ let stream;
443
+ if (htmlOnly) {
444
+ staticRuntime ??= await import('react-dom/static');
445
+ stream = (await staticRuntime.prerender(element, renderOptions)).prelude;
446
+ } else if(!useNodeHtml())stream = await domRuntime.renderToReadableStream(element, renderOptions);
447
+ const reader = stream ? stream.getReader() : await nodeHtmlReader(element,renderOptions);
406
448
  const chunks = [];
449
+ const decoder = new TextDecoder();
407
450
  let length = 0;
408
451
  try {
409
452
  for (;;) {
@@ -416,12 +459,15 @@ export async function renderHtml(tree, router, { timeoutMs = 25_000, formState =
416
459
  await reader.cancel(error);
417
460
  throw error;
418
461
  }
419
- chunks.push(Buffer.from(value));
462
+ // Decode each owned chunk once instead of copying every byte into
463
+ // per-chunk Buffers and a second aggregate Buffer before stringifying.
464
+ chunks.push(decoder.decode(value,{stream:true}));
420
465
  }
421
466
  } finally { reader.releaseLock(); }
422
467
  if (abort.signal.aborted) throw abort.signal.reason;
423
468
  if (renderError) throw renderError;
424
- return Buffer.concat(chunks, length).toString('utf8');
469
+ chunks.push(decoder.decode());
470
+ return chunks.join('');
425
471
  } catch (error) {
426
472
  if (abort.signal.aborted && abort.signal.reason?.statusCode === 504) throw abort.signal.reason;
427
473
  throw error;
@@ -440,9 +486,12 @@ export function completeAppHtml(html, flight, route = {}) {
440
486
  }
441
487
  const styles = fontPreloads(route.fonts, route.nonce) + (route.css || []).map(href => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join('');
442
488
  if (styles) html = beforeClosingTag(html, 'head', styles);
443
- const payload = `<script id="__PRNEXT_FLIGHT__" type="application/octet-stream">${flight.toString('base64')}</script>`;
444
- const bootstrap = route.client ? `<script type="module"${route.nonce ? ` nonce="${escapeHtml(route.nonce)}"` : ''} src="${escapeHtml(route.client)}"></script>` : '';
445
- html = beforeClosingTag(html, 'body', `${payload}${bootstrap}`);
489
+ if (route.htmlOnly) assertHtmlOnlyDocument(html, route.pattern);
490
+ else {
491
+ const payload = `<script id="__PRNEXT_FLIGHT__" type="application/octet-stream">${flight.toString('base64')}</script>`;
492
+ const bootstrap = route.client ? `<script type="module"${route.nonce ? ` nonce="${escapeHtml(route.nonce)}"` : ''} src="${escapeHtml(route.client)}"></script>` : '';
493
+ html = beforeClosingTag(html, 'body', `${payload}${bootstrap}`);
494
+ }
446
495
  if (!html.startsWith('<!DOCTYPE html>')) html = `<!DOCTYPE html>${html}`;
447
496
  if (Buffer.byteLength(html) > MAX_RESPONSE_BYTES) throw new Error('Response exceeds the 16 MiB PRNext limit');
448
497
  return html;
@@ -450,13 +499,15 @@ export function completeAppHtml(html, flight, route = {}) {
450
499
 
451
500
  const devStaticChecks = new Map();
452
501
 
502
+ import { differentialFlight } from './flight-delta.mjs';
453
503
  export async function renderAppPage(options) {
454
504
  const response = await renderAppPageInner(options);
455
505
  if (options.method !== 'POST' && response.headers?.location) response.headers.location = addBasePath(response.headers.location, options.manifest?.config?.basePath || '');
456
- return response;
506
+ return differentialFlight(response, options);
457
507
  }
458
508
 
459
509
  async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distDir = path.dirname(path.dirname(modulePath)), url = 'http://localhost/', originalUrl, params = {}, method = 'GET', headers = {}, routingRequestHeaders, routingResolver, body = '', production = process.env.NODE_ENV === 'production', stream = false, signal, deferFinalizer }) {
510
+ if (route.htmlOnly) return { status:409, headers:{'content-type':'text/plain; charset=utf-8','cache-control':'private, no-store','x-prnext-document-navigation':'1'}, body:Buffer.from('This HTML-only page requires its prerendered document.') };
460
511
  let clientModules = manifest.app?.clientModules || emptyManifest;
461
512
  if (route.cacheConfig?.runtime === 'edge') {
462
513
  let edge = !manifest.dev && edgeManifests.get(clientModules);
@@ -470,8 +521,8 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
470
521
  clientModules = edge;
471
522
  }
472
523
  const responseHeaders = { 'cache-control': 'private, no-cache, no-store, max-age=0', vary: 'RSC' };
473
- const request = { modulePath, routePattern: route.pattern, cacheConfig: route.cacheConfig, css: route.css || [], fonts: route.fonts || [], nonce: scriptNonce(headers), basePath: manifest.config?.basePath || '', trailingSlash: manifest?.config?.trailingSlash || false, skipTrailingSlashRedirect: manifest?.config?.skipTrailingSlashRedirect || false, url, originalUrl, params, method, headers, clientModules, production,
474
- distDir, strictMode: manifest.config?.reactStrictMode !== false, cacheComponents: manifest.config?.cacheComponents, cacheLife: manifest.config?.cacheLife, cacheHandlers: manifest.config?.cacheHandlers, cacheHandler: manifest.config?.cacheHandler, cacheMaxMemorySize: manifest.config?.cacheMaxMemorySize, serverActions: manifest.config?.serverActions, previewModeId: manifest.previewModeId, actions: manifest.app?.actions || emptyManifest, actionKey: manifest.app?.actionKey };
524
+ const request = { modulePath, routePattern: route.pattern, cacheConfig: route.cacheConfig, css: route.css || [], cssScopes: route.cssScopes || [], fonts: route.fonts || [], nonce: scriptNonce(headers), basePath: manifest.config?.basePath || '', trailingSlash: manifest?.config?.trailingSlash || false, skipTrailingSlashRedirect: manifest?.config?.skipTrailingSlashRedirect || false, url, originalUrl, params, method, headers, clientModules, production,
525
+ distDir, traceparent:currentTraceparent(), instrumentation:manifest.instrumentation, strictMode: manifest.config?.reactStrictMode !== false, cacheComponents: manifest.config?.cacheComponents, navigationCache: manifest.config?.navigationCache, authorizationSecret: production ? manifest.app?.actionKey : developmentAuthorizationSecret, partialPrefetching: manifest.config?.partialPrefetching, cacheLife: manifest.config?.cacheLife, cacheHandlers: manifest.config?.cacheHandlers, cacheHandler: manifest.config?.cacheHandler, cacheMaxMemorySize: manifest.config?.cacheMaxMemorySize, serverActions: manifest.config?.serverActions, previewModeId: manifest.previewModeId, actions: manifest.app?.actions || emptyManifest, actionKey: manifest.app?.actionKey };
475
526
  if (route.parallel) {
476
527
  request.routingResolver = routingResolver;
477
528
  request.skipProxyUrlNormalize = manifest.config?.skipProxyUrlNormalize;
@@ -503,6 +554,26 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
503
554
  }
504
555
  }
505
556
  const header = name => Object.entries(headers).find(([key]) => key.toLowerCase() === name)?.[1];
557
+ if (request.partialPrefetching && manifest.app?.actionKey) {
558
+ // Preview identity includes authorization, but exposes no cookie material or
559
+ // guessable credential hash. A changed login cannot reveal an old preview.
560
+ responseHeaders['x-prnext-ppr-id'] = 'stage:' + createHmac('sha256', manifest.app.actionKey)
561
+ .update(JSON.stringify([manifest.cacheId, route.id, originalUrl || url, header('cookie') || '', header('authorization') || ''])).digest('hex');
562
+ }
563
+ const prefetchStage = header('x-prnext-prefetch-stage');
564
+ if (request.partialPrefetching && method === 'GET' && header('rsc') === '1' && header('x-prnext-prefetch') === '1' && ['shell', 'prefetch'].includes(prefetchStage)) {
565
+ const requirement = route.cacheConfig?.ensureStatic;
566
+ const staticOnly = ['prefetch', 'navigation'].includes(requirement) || prefetchStage === 'shell' && requirement === 'shell';
567
+ if (!staticOnly) {
568
+ const rendered = await renderFlight({ ...request, navigationStage: prefetchStage }, { signal, deferFinalizer });
569
+ if (rendered.status !== 200 || rendered.navigation || !responseHeaders['x-prnext-ppr-id']) return { status: 204, headers: responseHeaders, body: Buffer.alloc(0) };
570
+ const value = { version: 1, id: responseHeaders['x-prnext-ppr-id'], flight: rendered.body.toString('base64'), keys: [], unknownParams: false,
571
+ router: { pathname: new URL(originalUrl || url).pathname, search: new URL(originalUrl || url).search, params, basePath: request.basePath } };
572
+ const bytes = Buffer.from(JSON.stringify(value));
573
+ if (bytes.length > PARTIAL_PREFETCH_BYTES) return { status: 204, headers: responseHeaders, body: Buffer.alloc(0) };
574
+ return { status: 200, headers: { ...responseHeaders, 'content-type': PARTIAL_PREFETCH_TYPE }, body: bytes };
575
+ }
576
+ }
506
577
  if (method === 'POST') {
507
578
  const id = header('next-action');
508
579
  if (id && !Object.hasOwn(request.actions, id)) return { status: 404, headers: responseHeaders, body: Buffer.from('Unknown Server Action') };
@@ -514,19 +585,20 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
514
585
  request.action = { id, contentType: header('content-type'), body: bytes };
515
586
  }
516
587
  if (!manifest.dev && request.cacheComponents && (route.ppr || route.pprFallback) && ['GET', 'HEAD'].includes(method) &&
588
+ !(request.partialPrefetching && header('rsc') === '1' && header('x-prnext-prefetch') !== '1' && !['shell', 'prefetch', 'navigation'].includes(route.cacheConfig?.ensureStatic)) &&
517
589
  !request.nonce && !(route.parallel && header('x-prnext-router-state')) && !isDraftRequest(request)) {
518
590
  const pathname = staticPath(route.pattern, new URL(url).pathname).path;
519
591
  const generic = route.pprGeneric?.find(item => Object.entries(item.params).every(([name, value]) => JSON.stringify(params[name]) === JSON.stringify(value)));
520
592
  const filename = (!originalUrl ? route.ppr?.[pathname] : undefined) || generic?.file;
521
593
  if (filename || route.pprFallback) {
522
594
  const artifact = await partialArtifact({ request, route, manifest, filename, identity: filename === generic?.file ? filename : `${pathname}:${originalUrl ? new URL(originalUrl).pathname : ''}`, signal,
523
- regenerate: async build => (await import('./app-static.mjs')).prerenderAppRoute({ modulePath, route, manifest, distDir, production,
595
+ regenerate: async build => (await import('./app-static.mjs')).prerenderAppRoute({ modulePath, route, manifest, distDir, production, renderMode:'isr',
524
596
  path: build?.generic?.path || pathname, originalUrl: build?.generic ? undefined : originalUrl,
525
597
  params: build?.generic?.params || params, partialParams: build?.generic?.unknown }),
526
598
  });
527
- if (!artifact.dynamic) return renderPartialAppPage({ request, route, artifact, responseHeaders, stream, signal, rsc: header('rsc') === '1', prefetch: header('x-prnext-prefetch') === '1', knownPrefetch: header('x-prnext-prefetch-known'),
599
+ if (!artifact.dynamic) return renderPartialAppPage({ request, route, artifact, responseHeaders, stream, signal, deferFinalizer, rsc: header('rsc') === '1', prefetch: header('x-prnext-prefetch') === '1', knownPrefetch: header('x-prnext-prefetch-known'),
528
600
  concrete: artifact.generic ? (continuationSignal => partialArtifact({ request, route, manifest, identity: `concrete:${pathname}:${originalUrl ? new URL(originalUrl).pathname : ''}`, signal: continuationSignal,
529
- regenerate: async () => (await import('./app-static.mjs')).prerenderAppRoute({ modulePath, route, manifest, distDir, production, path: pathname, originalUrl, params, seed: artifact }),
601
+ regenerate: async () => (await import('./app-static.mjs')).prerenderAppRoute({ modulePath, route, manifest, distDir, production, renderMode:'isr', path: pathname, originalUrl, params, seed: artifact }),
530
602
  })) : undefined });
531
603
  }
532
604
  }
@@ -576,7 +648,7 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
576
648
  let html, model, recovery = false;
577
649
  try {
578
650
  model = await decodeFlight(result.body, clientModules, distDir, { production });
579
- html = await renderHtml(model.tree, model.router, { timeoutMs: route.cacheConfig?.maxDuration ? route.cacheConfig.maxDuration * 1000 : undefined, formState: model.formState, nonce: request.nonce, strictMode: manifest.config?.reactStrictMode !== false });
651
+ html = await renderHtml(model.tree, model.router, { timeoutMs: route.cacheConfig?.maxDuration ? route.cacheConfig.maxDuration * 1000 : undefined, formState: model.formState, reportedDigests:result.reportedDigests, nonce: request.nonce, strictMode: manifest.config?.reactStrictMode !== false });
580
652
  } catch (error) {
581
653
  if (error?.statusCode === 504) throw error;
582
654
  const navigation = navigationResponse(error);
@@ -589,14 +661,14 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
589
661
  recovery = true;
590
662
  }
591
663
  const { body: flight, status } = result;
592
- html = completeAppHtml(html, flight, { ...route, nonce: request.nonce, ...(recovery ? { css: [], fonts: [] } : {}) });
664
+ html = completeAppHtml(html, flight, { ...route, css: model?.css ?? route.css, nonce: request.nonce, ...(recovery ? { css: [], fonts: [] } : {}) });
593
665
  // A Flight error inside Suspense need not fail the HTML shell. Only shell
594
666
  // failure selects 500 for an ordinary GET; action/navigation statuses remain.
595
667
  const htmlStatus = result.rscError && status === 500 && ['GET', 'HEAD'].includes(method) ? 200 : status;
596
668
  return { status: recovery ? route.parallel && status === 404 ? 404 : 500 : htmlStatus, headers: { ...responseHeaders, 'content-type': 'text/html; charset=utf-8' }, body: Buffer.from(html) };
597
669
  }
598
670
 
599
- async function renderPartialAppPage({ request, route, artifact, responseHeaders, stream, signal, rsc, concrete, prefetch, knownPrefetch }) {
671
+ async function renderPartialAppPage({ request, route, artifact, responseHeaders, stream, signal, deferFinalizer, rsc, concrete, prefetch, knownPrefetch }) {
600
672
  const keys = [...partialKeyMap(artifact.keyScopes, request.params, removeBasePath(new URL(request.url).pathname, request.basePath))];
601
673
  const partialRequest = { ...request, partialFlight: artifact.flight, ...(keys.length ? { partialKeys: keys } : {}) };
602
674
  const headers = { ...artifact.headers, ...responseHeaders, 'x-prnext-prerender': artifact.complete ? 'complete' : 'partial', 'content-type': rsc ? 'text/x-component; charset=utf-8' : 'text/html; charset=utf-8' };
@@ -619,7 +691,7 @@ async function renderPartialAppPage({ request, route, artifact, responseHeaders,
619
691
  return { status: resolved.status || 200, headers: resolved.headers || {}, body: streaming
620
692
  ? new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } }) : bytes };
621
693
  }
622
- return renderFlight({ ...partialRequest, partialFlight: resolved.flight || artifact.flight, stream: streaming }, { signal: continuationSignal });
694
+ return renderFlight({ ...partialRequest, partialFlight: resolved.flight || artifact.flight, stream: streaming }, { signal: continuationSignal, deferFinalizer });
623
695
  }
624
696
  if (rsc) {
625
697
  const result = await continueFlight(signal, stream);
@@ -7,6 +7,7 @@ import { pathToFileURL } from 'node:url';
7
7
  import { currentRequest, runRequestContext } from '../compat/headers.cjs';
8
8
  import { staticParams } from '../compat/static-generation.cjs';
9
9
  import { appRoutePlan } from './app-route-plan.mjs';
10
+ import { navigationAuthorization } from '../compat/navigation-authorization.cjs';
10
11
 
11
12
  import { matchAppPattern, createAppMatcher, readRouterState } from './app-routing-state.mjs';
12
13
  export { matchAppPattern, readRouterState, advancedRoutingSources } from './app-routing-state.mjs';
@@ -51,6 +52,9 @@ function selectedSegments(chain, params) {
51
52
 
52
53
  /** Parallel slots retain their original Flight children on soft navigation. */
53
54
  export function createAdvancedTree(entry, request, searchParams) {
55
+ function ScopedLayoutSlot(props) {
56
+ return React.createElement(entry.LayoutSlot, { ...props, authorizationKey: navigationAuthorization(currentRequest(), request.authorizationSecret || request.actionKey, request.navigationCache?.authCookies) });
57
+ }
54
58
  const pathname = removeBasePath(new URL(request.url).pathname, request.basePath || '');
55
59
  const matchCurrent = createAppMatcher(pathname);
56
60
  const old = readRouterState(request), slots = {}, activeParams = { ...request.params };
@@ -166,7 +170,7 @@ export function createAdvancedTree(entry, request, searchParams) {
166
170
  }
167
171
  }
168
172
  function slot(node, owner, name) {
169
- const key = registerKey(`${scope(owner)}::${name}`, owner, `${owner.path}::`, `::${name}`, ['id']);
173
+ const key = registerKey(`${scope(owner)}::${name}`, owner, `${owner.path}::`, `::${name}`, ['id', 'activityKey', 'authorizationKey']);
170
174
  const previous = old?.slots[key];
171
175
  let selected = rebuild && previous ? savedBranch(node, previous) : firstCandidate(node);
172
176
  if (rebuild && previous && !selected) { invalid = true; return { tree: null, segments: [] }; }
@@ -202,7 +206,10 @@ export function createAdvancedTree(entry, request, searchParams) {
202
206
  metadataSelections.set(saved.node.path, saved);
203
207
  if (name === 'children' && owner === root) rootPage = saved;
204
208
  }
205
- return { tree: React.createElement(entry.LayoutSlot, { key, id: key, preserve: true }), segments: null };
209
+ let tree = React.createElement(ScopedLayoutSlot, { key, id: key, preserve: true, activityKey: previous.branch });
210
+ const context = request.routingContexts?.get(previous.url);
211
+ if (context && request.renderRoutingBranch) tree = request.renderRoutingBranch(tree, context);
212
+ return { tree, segments: null };
206
213
  }
207
214
  if (!selected) {
208
215
  // Find the nearest default whose route prefix contains the destination.
@@ -224,13 +231,16 @@ export function createAdvancedTree(entry, request, searchParams) {
224
231
  slots[key] = { branch: selected.node.path + (selected.default ? '#default' : ''), url: selectedURL, source: sourceURL(owner, previous), accessUrl: selected.accessUrl || accessURL(selectedURL, previous) };
225
232
  if (name === 'children' && owner === root) rootPage = selected;
226
233
  let selectedTree = renderPath(selected.chain, selected, 0, name === 'children' && owner === node);
227
- const context = request.routingContexts?.get(selectedURL);
228
- if (context && request.renderRoutingBranch) selectedTree = request.renderRoutingBranch(selectedTree, context);
229
234
  if(request.instantValidation && name!=='children'){
230
235
  const instant=selected.node.config?.page?.instant ?? selected.chain.findLast(item=>item.config?.layout?.instant!==undefined)?.config.layout.instant;
231
236
  if(instantEnabled(instant,request.production)) instantTrees.push({path:(owner.path || '/')+' @'+name,tree:selectedTree});
232
237
  }
233
- return { tree: React.createElement(entry.LayoutSlot, { key, id: key, preserve: false }, selectedTree), segments: selected.default ? [] : selectedSegments(selected.chain.slice(node === owner ? 1 : 0), selected.params) };
238
+ const retainedLayout = selected.chain.find(item => item !== owner && item.files.layout);
239
+ const activityKey = retainedLayout ? scope(retainedLayout) : JSON.stringify([slots[key].branch, slots[key].url]);
240
+ let tree = React.createElement(ScopedLayoutSlot, { key, id: key, preserve: false, activityKey }, selectedTree);
241
+ const context = request.routingContexts?.get(selectedURL);
242
+ if (context && request.renderRoutingBranch) tree = request.renderRoutingBranch(tree, context);
243
+ return { tree, segments: selected.default ? [] : selectedSegments(selected.chain.slice(node === owner ? 1 : 0), selected.params) };
234
244
  }
235
245
  function renderPath(chain, selection, index, skipOwner) {
236
246
  const node = chain[index];
@@ -245,7 +255,7 @@ export function createAdvancedTree(entry, request, searchParams) {
245
255
  : React.createElement(Page, { ...props, key: registerKey(`${node.path}:${JSON.stringify(selection.params)}`, node, `${node.path}:`, '', undefined, selection.params) });
246
256
  } else children = null;
247
257
  if (skipOwner) return children;
248
- const childKey = registerKey(`${key}::children`, node, `${node.path}::`, '::children', ['id']);
258
+ const childKey = registerKey(`${key}::children`, node, `${node.path}::`, '::children', ['id', 'activityKey', 'activityPreserve', 'authorizationKey']);
249
259
  const previousChild = old?.slots[childKey];
250
260
  const preserveChild = !!previousChild && !rebuild && (interceptOwners.has(node.path) || (selection.scaffold && index === chain.length - 1));
251
261
  if (rebuild && previousChild && previousChild.branch !== selection.node.path + (selection.default ? '#default' : '')) {
@@ -263,7 +273,10 @@ export function createAdvancedTree(entry, request, searchParams) {
263
273
  const saved = savedBranch(node, previousChild);
264
274
  if (saved) metadataSelections.set(saved.node.path, saved);
265
275
  }
266
- children = React.createElement(entry.LayoutSlot, { key: childKey, id: childKey, preserve: preserveChild }, preserveChild ? null : children);
276
+ const nextLayout = chain.slice(index + 1).some(child => child.files.layout);
277
+ const activityKey = nextLayout ? `${chain[index + 1].path}:${JSON.stringify(scopedParams(chain[index + 1]))}` : JSON.stringify([slots[childKey].branch, slots[childKey].url]);
278
+ const activityPreserve = !!(rebuild && previousChild && previousChild.branch !== selection.node.path + (selection.default ? '#default' : ''));
279
+ children = React.createElement(ScopedLayoutSlot, { key: childKey, id: childKey, preserve: preserveChild, activityKey, activityPreserve }, preserveChild ? null : children);
267
280
  const selected = { children: selection.default ? [] : selectedSegments(chain.slice(index + 1), selection.params) };
268
281
  if (preserveChild) selected.children = null;
269
282
  const parallel = {};
@@ -279,8 +292,8 @@ export function createAdvancedTree(entry, request, searchParams) {
279
292
  }
280
293
  function wrap(node, children, parallel, selected, key, params, templateSegments, selectionNode) {
281
294
  if (node.files.loading) children = React.createElement(React.Suspense, { fallback: React.createElement(component(node.files.loading, 'loading')) }, children);
282
- if (node.files.error) children = React.createElement(entry.ErrorBoundary, { key: registerKey(`error:${key}`, node, `error:${node.path}::`), errorComponent: component(node.files.error, 'error'), resetKey: request.renderKey }, children);
283
- if (node.files.notFound || node === root) children = React.createElement(entry.NavigationBoundary, { key: registerKey(`navigation:${key}`, node, `navigation:${node.path}::`), resetKey: request.renderKey,
295
+ if (node.files.error) children = React.createElement(entry.ErrorBoundary, { key: registerKey(`error:${key}`, node, `error:${node.path}::`, '', ['resetKey']), errorComponent: component(node.files.error, 'error'), resetKey: request.renderKey }, children);
296
+ if (node.files.notFound || node === root) children = React.createElement(entry.NavigationBoundary, { key: registerKey(`navigation:${key}`, node, `navigation:${node.path}::`, '', ['resetKey']), resetKey: request.renderKey,
284
297
  notFound: node.files.notFound ? React.createElement(component(node.files.notFound, 'not-found')) : React.createElement('h1', null, '404: This page could not be found.') }, children);
285
298
  if (node.files.template) children = React.createElement(component(node.files.template, 'template'), { key: registerKey(selected.children?.join('/') || key, selectionNode || node, `${node.path}::`, '', undefined, scopedParams(node), templateSegments) }, children);
286
299
  if (node.files.layout) {
@@ -1,3 +1,4 @@
1
+ import {currentTraceparent} from './trace.mjs';
1
2
  import path from 'node:path';
2
3
  import { addBasePath, removeBasePath } from '../compat/paths.cjs';
3
4
  import { renderFlight, decodeFlight, renderHtml, completeAppHtml } from './app-render.mjs';
@@ -12,12 +13,13 @@ export function requestOptions(options) {
12
13
  const canonical = new URL('http://prnext.local');
13
14
  canonical.pathname = new URL(options.path || options.url || '/', canonical).pathname;
14
15
  return { modulePath: options.modulePath, distDir: options.distDir || path.dirname(path.dirname(options.modulePath)),
15
- routePattern: route.pattern, cacheConfig: route.cacheConfig || {}, css: route.css || [], fonts: route.fonts || [], production: options.production ?? true,
16
+ ...(options.renderMode==='isr'?{runtimeInstrumentation:true,instrumentation:options.manifest?.instrumentation,traceparent:currentTraceparent(),revalidateReason:options.revalidateReason}:{}),
17
+ routePattern: route.pattern, cacheConfig: route.cacheConfig || {}, css: route.css || [], cssScopes: route.cssScopes || [], fonts: route.fonts || [], production: options.production ?? true,
16
18
  url: canonical.href, originalUrl: options.originalUrl ? new URL(new URL(options.originalUrl).pathname, canonical).href : undefined,
17
19
  partialParams: options.partialParams, basePath: options.manifest?.config?.basePath || '', trailingSlash: options.manifest?.config?.trailingSlash || false, skipTrailingSlashRedirect: options.manifest?.config?.skipTrailingSlashRedirect || false, params: options.params || {}, method: 'GET', headers: {},
18
20
  ...(options.seed ? { partialFlight: options.seed.flight, partialKeys: [...partialKeyMap(options.seed.keyScopes, options.params, removeBasePath(canonical.pathname, options.manifest?.config?.basePath || ''))] } : {}),
19
21
  clientModules: options.manifest?.app?.clientModules || {},
20
- cacheComponents: options.manifest?.config?.cacheComponents, cacheLife: options.manifest?.config?.cacheLife, cacheHandlers: options.manifest?.config?.cacheHandlers, cacheHandler: options.manifest?.config?.cacheHandler, cacheMaxMemorySize: options.manifest?.config?.cacheMaxMemorySize,
22
+ cacheComponents: options.manifest?.config?.cacheComponents, navigationCache: options.manifest?.config?.navigationCache, partialPrefetching: options.manifest?.config?.partialPrefetching, cacheLife: options.manifest?.config?.cacheLife, cacheHandlers: options.manifest?.config?.cacheHandlers, cacheHandler: options.manifest?.config?.cacheHandler, cacheMaxMemorySize: options.manifest?.config?.cacheMaxMemorySize,
21
23
  actions: options.manifest?.app?.actions || {}, actionKey: options.manifest?.app?.actionKey };
22
24
  }
23
25
 
@@ -29,11 +31,12 @@ export async function prerenderAppRoute(options) {
29
31
  const request = requestOptions(options);
30
32
  const mode = request.cacheConfig.dynamic || 'auto';
31
33
  if (request.cacheConfig.runtime === 'edge' || mode === 'force-dynamic' || (request.cacheConfig.revalidate === 0 && mode !== 'force-static')) {
34
+ if (request.cacheConfig.ensureStatic === 'navigation') throw new Error(`Route ${request.routePattern}: ensureStatic='navigation' conflicts with request-time rendering configuration.`);
32
35
  return { dynamic: true, reason: request.cacheConfig.runtime === 'edge' ? 'runtime: edge' : mode === 'force-dynamic' ? 'dynamic: force-dynamic' : 'revalidate: 0' };
33
36
  }
34
37
  try {
35
- const partial = request.cacheComponents === true && mode === 'auto';
36
- const result = await renderFlight({ ...request, staticGeneration: { mode, partial } });
38
+ const partial = request.cacheComponents === true && mode === 'auto' && request.cacheConfig.ensureStatic !== 'navigation';
39
+ const result = await renderFlight({ ...request, staticGeneration: { mode, partial } },{settleBeforeReturn:true});
37
40
  return await runRequestContext({ ...request, phase: 'render', staticGeneration: { mode } }, async () => {
38
41
  const context = currentRequest();
39
42
  try {
@@ -61,21 +64,28 @@ export async function prerenderAppRoute(options) {
61
64
  }
62
65
  html = completeAppHtml(shell.shell, result.body, { ...options.route, css: [], fonts: [] });
63
66
  } else {
64
- html = completeAppHtml(await renderHtml(model.tree, model.router, { timeoutMs: options.route?.cacheConfig?.maxDuration ? options.route.cacheConfig.maxDuration * 1000 : undefined, staticGeneration: true, strictMode: options.manifest?.config?.reactStrictMode !== false }), result.body, options.route);
67
+ html = completeAppHtml(await renderHtml(model.tree, model.router, { timeoutMs: options.route?.cacheConfig?.maxDuration ? options.route.cacheConfig.maxDuration * 1000 : undefined, staticGeneration: true, strictMode: options.manifest?.config?.reactStrictMode !== false, htmlOnly:options.route?.htmlOnly === true }), result.body, { ...options.route, css:model.css ?? options.route?.css });
65
68
  }
66
69
  }
67
- if (staticMetadata(context)?.externalCache) return { dynamic: true, reason: 'External cache handlers require validation on every request' };
68
- return { body: html, flight: result.body, ...staticMetadata(context), generatedAt: Date.now(),
70
+ if (staticMetadata(context)?.externalCache) {
71
+ if (request.cacheConfig.ensureStatic === 'navigation') throw new Error(`Route ${request.routePattern}: external cache validation prevents guaranteed static navigation output.`);
72
+ return { dynamic: true, reason: 'External cache handlers require validation on every request' };
73
+ }
74
+ return { body: html, ...(options.route?.htmlOnly ? {} : { flight:result.body }), ...staticMetadata(context), generatedAt: Date.now(),
69
75
  status: result.status, headers: { ...result.headers, ...(result.headers?.location ? { location: addBasePath(result.headers.location, request.basePath) } : {}), 'content-type': 'text/html; charset=utf-8' } };
70
76
  } finally { await flushCacheWork(context); }
71
77
  });
72
78
  } catch (error) {
73
- if (error.code === 'PRNEXT_DYNAMIC_SERVER_USAGE' && mode === 'auto') return { dynamic: true, reason: error.dynamicReason || error.message };
79
+ if (error.code === 'PRNEXT_DYNAMIC_SERVER_USAGE' && mode === 'auto') {
80
+ if (request.cacheConfig.ensureStatic === 'navigation') throw new Error(`Route ${request.routePattern}: ensureStatic='navigation' requires static server output; ${error.dynamicReason || error.message}.`, { cause: error });
81
+ return { dynamic: true, reason: error.dynamicReason || error.message };
82
+ }
74
83
  throw error;
75
84
  }
76
85
  }
77
86
 
78
87
  export async function renderAppIsrPage(options) {
88
+ if (options.route?.htmlOnly) throw new Error(`htmlOnly route ${options.route.pattern} cannot use ISR or request-time regeneration.`);
79
89
  const rendered = await prerenderAppRoute(options);
80
90
  if (rendered.dynamic) return { status: 200, headers: {}, body: [],
81
91
  isr: { dynamic: true, revalidate: 0, htmlLength: 0, dataLength: 0, tags: [], paths: [] } };
@@ -1,3 +1,4 @@
1
+ import {reportRequestError,acknowledgeRequestError} from './instrumentation.mjs';
1
2
  import { htmlEnvelope } from './html-envelope.mjs';
2
3
  import React from 'react';
3
4
  import { fontPreloads } from './font-preload.mjs';
@@ -12,55 +13,9 @@ import { addBasePath } from '../compat/paths.cjs';
12
13
  import { renderAppRecoveryShell } from './app-recovery.mjs';
13
14
  import { partialModel } from './app-partial-model.mjs';
14
15
  import { streamSelector } from './stream-select.mjs';
16
+ import {nodeHtmlReader,useNodeHtml} from './node-html-stream.mjs';
15
17
 
16
- // The SSR decoder and HTML bootstrap share each Flight allocation. Unlike
17
- // ReadableStream.tee(), this tap stops reading when the HTML consumer stalls.
18
- // Before a document shell exists, at most the overall 16 MiB response limit can
19
- // accumulate: an async root layout may need the complete model to produce it.
20
- function flightTap(source) {
21
- const reader = source.getReader();
22
- const queue = [];
23
- let bytes = 0;
24
- let done = false;
25
- let failure;
26
- let outputStarted = false;
27
- let wakeData;
28
- let wakeSpace;
29
- const finish = error => {
30
- done = true; failure = error;
31
- wakeData?.(); wakeData = undefined;
32
- wakeSpace?.(); wakeSpace = undefined;
33
- };
34
- const cancel = async reason => { finish(reason); await reader.cancel(reason).catch(() => {}); };
35
- const body = new ReadableStream({
36
- async pull(controller) {
37
- try {
38
- while (!done && outputStarted && bytes >= 128 * 1024) await new Promise(resolve => { wakeSpace = resolve; });
39
- if (done) { if (failure) controller.error(failure); else controller.close(); return; }
40
- const next = await reader.read();
41
- if (next.done) { finish(); controller.close(); return; }
42
- queue.push(next.value); bytes += next.value.byteLength;
43
- wakeData?.(); wakeData = undefined;
44
- controller.enqueue(next.value);
45
- } catch (error) { finish(error); controller.error(error); }
46
- },
47
- cancel,
48
- }, { highWaterMark: 0 });
49
- return {
50
- body, cancel,
51
- startOutput() { outputStarted = true; },
52
- async next() {
53
- while (!queue.length && !done) await new Promise(resolve => { wakeData = resolve; });
54
- if (queue.length) {
55
- const value = queue.shift(); bytes -= value.byteLength;
56
- wakeSpace?.(); wakeSpace = undefined;
57
- return { value, done: false };
58
- }
59
- if (failure) throw failure;
60
- return { done: true };
61
- },
62
- };
63
- }
18
+ import {flightTap} from './flight-tap.mjs';
64
19
 
65
20
  export async function renderProgressiveAppHtml({ result, request, route, responseHeaders, decodeFlight, signal, timeoutMs, partial }) {
66
21
  timeoutMs ??= route?.cacheConfig?.maxDuration ? route.cacheConfig.maxDuration * 1000 : 25_000;
@@ -92,6 +47,7 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
92
47
  onError(error) {
93
48
  if (isDynamicBailout(error)) return error.digest;
94
49
  if (combinedSignal.aborted) return error?.digest;
50
+ if(result.reportedDigests?.has(error?.digest))acknowledgeRequestError(error);else reportRequestError(error);
95
51
  const navigation = navigationResponse(error);
96
52
  if (navigation) {
97
53
  if (committed) control = navigation;
@@ -104,8 +60,9 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
104
60
  const element = React.createElement(request.strictMode !== false ? React.StrictMode : React.Fragment, null, React.createElement(AppRouterProvider, { router: resumedModel.router, nonce: request.nonce }, appContent(resumedModel.tree)));
105
61
  // React owns and mutates its opaque continuation during resume. Parsed
106
62
  // artifacts can be shared, but each HTML request needs its own continuation.
107
- const html = partial ? await resume(element, structuredClone(partial.postponed), renderOptions) : await renderToReadableStream(element, renderOptions);
108
- reader = html.getReader();
63
+ reader = !partial&&request.cacheConfig?.runtime!=='edge'&&useNodeHtml()
64
+ ? await nodeHtmlReader(element,renderOptions)
65
+ : (partial ? await resume(element, structuredClone(partial.postponed), renderOptions) : await renderToReadableStream(element, renderOptions)).getReader();
109
66
  const decoder = new TextDecoder();
110
67
  let prefix = partial?.shell || '';
111
68
  let htmlDone = false;
@@ -116,13 +73,13 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
116
73
  const next = await reader.read();
117
74
  if (next.done) { htmlDone = true; prefix += decoder.decode(); break; }
118
75
  prefix += decoder.decode(next.value, { stream: true });
119
- if (Buffer.byteLength(prefix) > MAX_RESPONSE_BYTES) throw new Error('HTML shell exceeds the 16 MiB PRNext limit');
76
+ if (Buffer.byteLength(prefix) > MAX_RESPONSE_BYTES) throw Object.assign(new Error('HTML shell exceeds the 16 MiB PRNext limit'),{code:'PRNEXT_RESPONSE_TOO_LARGE'});
120
77
  }
121
78
  if (renderError) throw renderError;
122
79
  if (!/<html(?:\s|>)/i.test(prefix) || !/<body(?:\s|>)/i.test(prefix) || !/<\/head>/i.test(prefix)) {
123
80
  throw Object.assign(new Error('App Router root layout must render <html>, <head> and <body> elements'), { code: 'PRNEXT_INVALID_ROOT_LAYOUT' });
124
81
  }
125
- const styles = fontPreloads(route.fonts, request.nonce) + (route.css || []).map(href => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join('');
82
+ const styles = fontPreloads(route.fonts, request.nonce) + (model.css ?? route.css ?? []).map(href => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join('');
126
83
  if (styles && !partial) prefix = prefix.replace(/<\/head>/i, `${styles}</head>`);
127
84
  tap.startOutput();
128
85
  committed = true;
@@ -183,7 +140,7 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
183
140
  return { status, headers: { ...responseHeaders, ...result.headers, 'content-type': 'text/html; charset=utf-8' }, body, cancel };
184
141
  } catch (error) {
185
142
  if (!committed && !combinedSignal.aborted && error?.statusCode !== 504 &&
186
- error?.code !== 'PRNEXT_INVALID_ROOT_LAYOUT' && !isDynamicBailout(error) && (!navigationResponse(error) || (route.parallel && navigationResponse(error).status === 404))) {
143
+ error?.code !== 'PRNEXT_INVALID_ROOT_LAYOUT' && error?.code !== 'PRNEXT_RESPONSE_TOO_LARGE' && !isDynamicBailout(error) && (!navigationResponse(error) || (route.parallel && navigationResponse(error).status === 404))) {
187
144
  try {
188
145
  // Next emits an empty document after a shell failure. The original
189
146
  // Flight stream still carries the failing subtree to client boundaries;