@thomas.f/prnext 0.1.17-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 (137) hide show
  1. package/README.md +26 -869
  2. package/build/actions.mjs +1 -1
  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 +81 -26
  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 +31 -8
  15. package/build/config.mjs +30 -1
  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-update.mjs +4 -2
  21. package/build/dev-dependencies.mjs +1 -1
  22. package/build/dev-discovery.mjs +3 -3
  23. package/build/dev.mjs +15 -8
  24. package/build/dynamic.mjs +1 -1
  25. package/build/edge.mjs +3 -3
  26. package/build/fonts.mjs +1 -1
  27. package/build/framework-imports.mjs +15 -5
  28. package/build/html-only-config.mjs +53 -0
  29. package/build/html-only-graph.mjs +67 -0
  30. package/build/index.mjs +59 -26
  31. package/build/instrumentation.mjs +30 -0
  32. package/build/metadata-routes.mjs +1 -1
  33. package/build/migration-audit.mjs +1 -1
  34. package/build/native-type-config.mjs +37 -0
  35. package/build/persistent-cache.mjs +18 -3
  36. package/build/precompress.mjs +1 -1
  37. package/build/prerender-cache.mjs +70 -0
  38. package/build/prerender-config.mjs +29 -0
  39. package/build/prerender-pages.mjs +19 -9
  40. package/build/prerender-pool.mjs +165 -0
  41. package/build/prerender-process-worker.mjs +58 -0
  42. package/build/prerender-worker.mjs +5 -5
  43. package/build/production-builder.mjs +58 -9
  44. package/build/react-compiler-report.mjs +225 -0
  45. package/build/react-compiler.mjs +139 -0
  46. package/build/runtime-contract.mjs +2 -2
  47. package/build/static-export.mjs +4 -4
  48. package/build/styles.mjs +6 -4
  49. package/build/transform.mjs +1 -1
  50. package/build/typed-routes.mjs +4 -0
  51. package/build/typescript-dev.mjs +35 -8
  52. package/build/typescript-native-worker.mjs +55 -0
  53. package/build/typescript-native.mjs +18 -0
  54. package/build/typescript-toolchain.mjs +19 -0
  55. package/build/typescript.mjs +6 -4
  56. package/build/webpack-graph.mjs +6 -3
  57. package/build/worker-memory.mjs +9 -0
  58. package/cli.mjs +60 -4
  59. package/compat/activity-cache.cjs +26 -0
  60. package/compat/after.cjs +6 -4
  61. package/compat/app-layout-context.cjs +52 -2
  62. package/compat/base64.cjs +10 -0
  63. package/compat/cache-handlers.cjs +12 -2
  64. package/compat/cache-wire.cjs +28 -0
  65. package/compat/cache.cjs +2 -1
  66. package/compat/cache.d.cts +4 -0
  67. package/compat/cookies.cjs +21 -5
  68. package/compat/data-cache.cjs +12 -8
  69. package/compat/flight-delta.cjs +150 -0
  70. package/compat/headers.cjs +4 -2
  71. package/compat/incremental-cache.cjs +20 -7
  72. package/compat/index.d.ts +68 -3
  73. package/compat/link.cjs +12 -9
  74. package/compat/navigation-authorization.cjs +9 -0
  75. package/compat/navigation-stages.cjs +19 -0
  76. package/compat/offline-store.cjs +11 -0
  77. package/compat/offline.cjs +5 -0
  78. package/compat/offline.d.cts +2 -0
  79. package/compat/redis-cache.cjs +201 -0
  80. package/compat/redis-cache.d.cts +37 -0
  81. package/compat/server-primitives.cjs +31 -11
  82. package/compat/server.d.cts +15 -2
  83. package/compat/static-generation.cjs +7 -0
  84. package/compat/use-cache.cjs +3 -0
  85. package/docs/ai/compatibility.md +171 -2
  86. package/docs/ai/data.md +165 -5
  87. package/docs/ai/deployment.md +158 -0
  88. package/docs/ai/quickstart.md +29 -4
  89. package/docs/ai/routing.md +82 -0
  90. package/docs/ai/troubleshooting.md +37 -2
  91. package/package.json +26 -7
  92. package/runtime/action-client.mjs +10 -4
  93. package/runtime/action-server.mjs +2 -0
  94. package/runtime/app-client.mjs +212 -65
  95. package/runtime/app-content.mjs +4 -3
  96. package/runtime/app-partial-client.mjs +3 -0
  97. package/runtime/app-partial.mjs +1 -1
  98. package/runtime/app-prefetch-constants.mjs +4 -0
  99. package/runtime/app-prefetch.mjs +2 -3
  100. package/runtime/app-render-key.mjs +5 -0
  101. package/runtime/app-render.mjs +88 -28
  102. package/runtime/app-routing.mjs +22 -9
  103. package/runtime/app-static.mjs +18 -8
  104. package/runtime/app-stream-html.mjs +10 -53
  105. package/runtime/app-style-selection.mjs +34 -0
  106. package/runtime/app-styles.mjs +154 -0
  107. package/runtime/cache-components-rsc.mjs +4 -1
  108. package/runtime/cache-request.mjs +2 -1
  109. package/runtime/deployment-client.mjs +19 -0
  110. package/runtime/dev-client.mjs +7 -2
  111. package/runtime/dev-compiler-broker.mjs +42 -0
  112. package/runtime/dev-indicator.mjs +8 -3
  113. package/runtime/dev-lazy-tool.mjs +16 -0
  114. package/runtime/development.mjs +29 -12
  115. package/runtime/document.mjs +1 -1
  116. package/runtime/edge.mjs +6 -5
  117. package/runtime/fetch-cache.mjs +10 -8
  118. package/runtime/flight-delta.mjs +50 -0
  119. package/runtime/flight-tap.mjs +28 -0
  120. package/runtime/html-only.mjs +27 -0
  121. package/runtime/http.mjs +2 -0
  122. package/runtime/incremental-cache.mjs +25 -4
  123. package/runtime/instrumentation.mjs +49 -0
  124. package/runtime/navigation-recovery.mjs +36 -0
  125. package/runtime/navigation.mjs +7 -0
  126. package/runtime/node-html-stream.mjs +47 -0
  127. package/runtime/pages-client.mjs +14 -7
  128. package/runtime/pages-navigation.mjs +48 -63
  129. package/runtime/prefetch-scheduler.mjs +110 -0
  130. package/runtime/profile.mjs +14 -0
  131. package/runtime/profiles.json +2 -2
  132. package/runtime/render.mjs +3 -0
  133. package/runtime/route-pattern.mjs +44 -0
  134. package/runtime/rsc-worker.mjs +34 -12
  135. package/runtime/trace.mjs +78 -0
  136. package/runtime/watch-project.mjs +19 -15
  137. package/runtime/worker.mjs +70 -26
@@ -0,0 +1,4 @@
1
+ // Shared wire limits stay available without loading the optional PPR decoder.
2
+ export const PARTIAL_PREFETCH_TYPE = 'application/x-prnext-ppr+json';
3
+ export const PARTIAL_PREFETCH_BYTES = 768 * 1024;
4
+ export const PARTIAL_PREFETCH_CACHE_BYTES = 2 * 1024 * 1024;
@@ -1,8 +1,7 @@
1
1
  // Prefetch contains only prerendered segments. Request-dependent holes are
2
2
  // resolved by a fresh, authenticated navigation; they never enter this cache.
3
- export const PARTIAL_PREFETCH_TYPE = 'application/x-prnext-ppr+json';
4
- export const PARTIAL_PREFETCH_BYTES = 768 * 1024;
5
- export const PARTIAL_PREFETCH_CACHE_BYTES = 2 * 1024 * 1024;
3
+ import { PARTIAL_PREFETCH_BYTES } from './app-prefetch-constants.mjs';
4
+ export { PARTIAL_PREFETCH_TYPE, PARTIAL_PREFETCH_BYTES, PARTIAL_PREFETCH_CACHE_BYTES } from './app-prefetch-constants.mjs';
6
5
 
7
6
  export async function readPartialPrefetch(response) {
8
7
  const reader = response.body.getReader();
@@ -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,10 @@ 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
+ }
236
248
  export async function preloadAppRoutes(files,options){
237
249
  const worker=getWorker(true),id=`preload-${++nextId}`;
238
250
  worker.ref();
@@ -252,6 +264,12 @@ export async function preloadAppRoutes(files,options){
252
264
  // incidental event-loop liveness or terminate unfinished application work.
253
265
  export async function drainAppRuntime() {
254
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
+ })));
255
273
  await closeAppRuntime();
256
274
  }
257
275
 
@@ -278,7 +296,8 @@ function requestDeadline(entry, lifecycle, id, hardTimeoutMs, delayMs) {
278
296
  }, delayMs);
279
297
  }
280
298
 
281
- 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.');
282
301
  softTimeoutMs ??= request.cacheConfig?.maxDuration ? request.cacheConfig.maxDuration * 1000 : 25_000;
283
302
  hardTimeoutMs ??= request.cacheConfig?.maxDuration ? request.cacheConfig.maxDuration * 1000 : 28_000;
284
303
  const deadline = performance.now() + hardTimeoutMs;
@@ -304,7 +323,11 @@ export async function renderFlight(request, { softTimeoutMs, hardTimeoutMs, sign
304
323
  let settle;
305
324
  const settled = new Promise(resolve => { settle = resolve; });
306
325
  trackRequestWork(settled, signal, { deadline, supervised: true });
307
- 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 };
308
331
  const timeout = entry.timeout = requestDeadline(entry, lifecycle, id, hardTimeoutMs, Math.max(0, deadline - performance.now()));
309
332
  function cancel(reason) {
310
333
  if (entry.cancelled) return;
@@ -336,7 +359,7 @@ export async function renderFlight(request, { softTimeoutMs, hardTimeoutMs, sign
336
359
  pending.set(id, entry);
337
360
  try {
338
361
  const wire = { ...request, renderTimeoutMs: Math.min(softTimeoutMs, Math.max(1, deadline - performance.now())) };
339
- if (request.action && deferFinalizer) {
362
+ if (request.cacheConfig?.runtime !== 'edge' && deferFinalizer) {
340
363
  wire.waitForResponse = true;
341
364
  deferFinalizer(() => { if (pending.has(id)) current.postMessage({ id, type: 'after-response' }); });
342
365
  }
@@ -395,15 +418,17 @@ export async function decodeFlight(body, clientModules = {}, distDir, { producti
395
418
  return decoder.createFromNodeStream(body instanceof ReadableStream ? Readable.fromWeb(body) : Readable.from([body]), serverConsumerManifest);
396
419
  }
397
420
 
398
- 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 } = {}) {
399
422
  const abort = new AbortController();
400
423
  const timeout = setTimeout(() => abort.abort(appTimeoutError('HTML render', timeoutMs)), timeoutMs);
401
424
  let renderError;
402
425
  try {
403
426
  domRuntime ??= await import('react-dom/server');
404
- 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 = {
405
429
  nonce,
406
430
  signal: abort.signal, onError(error) {
431
+ if(!abort.signal.aborted){if(reportedDigests?.has(error?.digest))acknowledgeRequestError(error);else reportRequestError(error);}
407
432
  if (isDynamicBailout(error)) return error.digest;
408
433
  // Suspense can recover client SSR failures by emitting its fallback and
409
434
  // retrying in the browser. Static generation must still reject them:
@@ -413,9 +438,15 @@ export async function renderHtml(tree, router, { timeoutMs = 25_000, formState =
413
438
  return appErrorDigest(error);
414
439
  },
415
440
  formState,
416
- });
417
- 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);
418
448
  const chunks = [];
449
+ const decoder = new TextDecoder();
419
450
  let length = 0;
420
451
  try {
421
452
  for (;;) {
@@ -428,12 +459,15 @@ export async function renderHtml(tree, router, { timeoutMs = 25_000, formState =
428
459
  await reader.cancel(error);
429
460
  throw error;
430
461
  }
431
- 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}));
432
465
  }
433
466
  } finally { reader.releaseLock(); }
434
467
  if (abort.signal.aborted) throw abort.signal.reason;
435
468
  if (renderError) throw renderError;
436
- return Buffer.concat(chunks, length).toString('utf8');
469
+ chunks.push(decoder.decode());
470
+ return chunks.join('');
437
471
  } catch (error) {
438
472
  if (abort.signal.aborted && abort.signal.reason?.statusCode === 504) throw abort.signal.reason;
439
473
  throw error;
@@ -452,9 +486,12 @@ export function completeAppHtml(html, flight, route = {}) {
452
486
  }
453
487
  const styles = fontPreloads(route.fonts, route.nonce) + (route.css || []).map(href => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join('');
454
488
  if (styles) html = beforeClosingTag(html, 'head', styles);
455
- const payload = `<script id="__PRNEXT_FLIGHT__" type="application/octet-stream">${flight.toString('base64')}</script>`;
456
- const bootstrap = route.client ? `<script type="module"${route.nonce ? ` nonce="${escapeHtml(route.nonce)}"` : ''} src="${escapeHtml(route.client)}"></script>` : '';
457
- 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
+ }
458
495
  if (!html.startsWith('<!DOCTYPE html>')) html = `<!DOCTYPE html>${html}`;
459
496
  if (Buffer.byteLength(html) > MAX_RESPONSE_BYTES) throw new Error('Response exceeds the 16 MiB PRNext limit');
460
497
  return html;
@@ -462,13 +499,15 @@ export function completeAppHtml(html, flight, route = {}) {
462
499
 
463
500
  const devStaticChecks = new Map();
464
501
 
502
+ import { differentialFlight } from './flight-delta.mjs';
465
503
  export async function renderAppPage(options) {
466
504
  const response = await renderAppPageInner(options);
467
505
  if (options.method !== 'POST' && response.headers?.location) response.headers.location = addBasePath(response.headers.location, options.manifest?.config?.basePath || '');
468
- return response;
506
+ return differentialFlight(response, options);
469
507
  }
470
508
 
471
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.') };
472
511
  let clientModules = manifest.app?.clientModules || emptyManifest;
473
512
  if (route.cacheConfig?.runtime === 'edge') {
474
513
  let edge = !manifest.dev && edgeManifests.get(clientModules);
@@ -482,8 +521,8 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
482
521
  clientModules = edge;
483
522
  }
484
523
  const responseHeaders = { 'cache-control': 'private, no-cache, no-store, max-age=0', vary: 'RSC' };
485
- 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,
486
- 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 };
487
526
  if (route.parallel) {
488
527
  request.routingResolver = routingResolver;
489
528
  request.skipProxyUrlNormalize = manifest.config?.skipProxyUrlNormalize;
@@ -515,6 +554,26 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
515
554
  }
516
555
  }
517
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
+ }
518
577
  if (method === 'POST') {
519
578
  const id = header('next-action');
520
579
  if (id && !Object.hasOwn(request.actions, id)) return { status: 404, headers: responseHeaders, body: Buffer.from('Unknown Server Action') };
@@ -526,19 +585,20 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
526
585
  request.action = { id, contentType: header('content-type'), body: bytes };
527
586
  }
528
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)) &&
529
589
  !request.nonce && !(route.parallel && header('x-prnext-router-state')) && !isDraftRequest(request)) {
530
590
  const pathname = staticPath(route.pattern, new URL(url).pathname).path;
531
591
  const generic = route.pprGeneric?.find(item => Object.entries(item.params).every(([name, value]) => JSON.stringify(params[name]) === JSON.stringify(value)));
532
592
  const filename = (!originalUrl ? route.ppr?.[pathname] : undefined) || generic?.file;
533
593
  if (filename || route.pprFallback) {
534
594
  const artifact = await partialArtifact({ request, route, manifest, filename, identity: filename === generic?.file ? filename : `${pathname}:${originalUrl ? new URL(originalUrl).pathname : ''}`, signal,
535
- 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',
536
596
  path: build?.generic?.path || pathname, originalUrl: build?.generic ? undefined : originalUrl,
537
597
  params: build?.generic?.params || params, partialParams: build?.generic?.unknown }),
538
598
  });
539
- 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'),
540
600
  concrete: artifact.generic ? (continuationSignal => partialArtifact({ request, route, manifest, identity: `concrete:${pathname}:${originalUrl ? new URL(originalUrl).pathname : ''}`, signal: continuationSignal,
541
- 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 }),
542
602
  })) : undefined });
543
603
  }
544
604
  }
@@ -588,7 +648,7 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
588
648
  let html, model, recovery = false;
589
649
  try {
590
650
  model = await decodeFlight(result.body, clientModules, distDir, { production });
591
- 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 });
592
652
  } catch (error) {
593
653
  if (error?.statusCode === 504) throw error;
594
654
  const navigation = navigationResponse(error);
@@ -601,14 +661,14 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
601
661
  recovery = true;
602
662
  }
603
663
  const { body: flight, status } = result;
604
- 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: [] } : {}) });
605
665
  // A Flight error inside Suspense need not fail the HTML shell. Only shell
606
666
  // failure selects 500 for an ordinary GET; action/navigation statuses remain.
607
667
  const htmlStatus = result.rscError && status === 500 && ['GET', 'HEAD'].includes(method) ? 200 : status;
608
668
  return { status: recovery ? route.parallel && status === 404 ? 404 : 500 : htmlStatus, headers: { ...responseHeaders, 'content-type': 'text/html; charset=utf-8' }, body: Buffer.from(html) };
609
669
  }
610
670
 
611
- 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 }) {
612
672
  const keys = [...partialKeyMap(artifact.keyScopes, request.params, removeBasePath(new URL(request.url).pathname, request.basePath))];
613
673
  const partialRequest = { ...request, partialFlight: artifact.flight, ...(keys.length ? { partialKeys: keys } : {}) };
614
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' };
@@ -631,7 +691,7 @@ async function renderPartialAppPage({ request, route, artifact, responseHeaders,
631
691
  return { status: resolved.status || 200, headers: resolved.headers || {}, body: streaming
632
692
  ? new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } }) : bytes };
633
693
  }
634
- 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 });
635
695
  }
636
696
  if (rsc) {
637
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;