@thomas.f/prnext 0.1.19-alpha → 0.1.21-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 (113) hide show
  1. package/build/app.mjs +46 -78
  2. package/build/build-lock.mjs +2 -71
  3. package/build/build-retention.mjs +119 -0
  4. package/build/build-timings.mjs +38 -0
  5. package/build/bundle-runtime.mjs +17 -16
  6. package/build/cache-command.mjs +31 -0
  7. package/build/check.mjs +41 -3
  8. package/build/compilation-cache.mjs +32 -12
  9. package/build/compilation-storage.mjs +190 -0
  10. package/build/compiler.mjs +5 -4
  11. package/build/config.mjs +23 -2
  12. package/build/create.mjs +1 -1
  13. package/build/css-modules.mjs +4 -0
  14. package/build/dev-action-graph.mjs +21 -0
  15. package/build/flight-summary.mjs +64 -0
  16. package/build/font-cache.mjs +9 -6
  17. package/build/font-files.mjs +1 -1
  18. package/build/index.mjs +57 -36
  19. package/build/inspect.mjs +3 -0
  20. package/build/inspection-summary.mjs +7 -2
  21. package/build/native-discovery.mjs +18 -6
  22. package/build/native-ppr.mjs +32 -0
  23. package/build/npm-packages.mjs +2 -2
  24. package/build/og.mjs +121 -1
  25. package/build/parallel-plan.mjs +55 -0
  26. package/build/persistent-cache.mjs +29 -24
  27. package/build/production-check.mjs +34 -0
  28. package/build/react-compiler-report.mjs +31 -8
  29. package/build/react-compiler.mjs +101 -10
  30. package/build/server-config.mjs +35 -0
  31. package/build/source-analysis.mjs +108 -5
  32. package/build/standalone-copy.mjs +48 -0
  33. package/build/standalone.mjs +2 -22
  34. package/build/static-adapter.mjs +7 -3
  35. package/build/transform-cache.mjs +17 -3
  36. package/build/transform.mjs +38 -6
  37. package/build/typescript-dev.mjs +3 -1
  38. package/cli.mjs +27 -5
  39. package/compat/app-layout-context.cjs +7 -3
  40. package/compat/diagnostics.cjs +5 -4
  41. package/compat/flight-delta.cjs +47 -12
  42. package/compat/index.d.ts +24 -1
  43. package/compat/navigation-authorization.cjs +8 -1
  44. package/compat/runtime-sample.cjs +56 -0
  45. package/compat/use-cache.cjs +5 -2
  46. package/docs/ai/compatibility.md +113 -10
  47. package/docs/ai/deployment.md +144 -0
  48. package/docs/ai/quickstart.md +5 -0
  49. package/docs/ai/troubleshooting.md +22 -0
  50. package/package.json +25 -22
  51. package/release/fontkit.mjs +31 -0
  52. package/release/verify.mjs +3 -0
  53. package/runtime/api.mjs +5 -2
  54. package/runtime/app-client.mjs +246 -54
  55. package/runtime/app-partial.mjs +2 -10
  56. package/runtime/app-prefetch.mjs +2 -2
  57. package/runtime/app-render.mjs +42 -13
  58. package/runtime/app-stream-channel.mjs +20 -6
  59. package/runtime/app-stream-html.mjs +30 -11
  60. package/runtime/app-styles.mjs +22 -4
  61. package/runtime/cache-components-rsc.mjs +26 -6
  62. package/runtime/cache-result.mjs +58 -0
  63. package/runtime/compile-cache-retention.mjs +53 -0
  64. package/runtime/compile-cache.mjs +28 -1
  65. package/runtime/dev-client.mjs +32 -8
  66. package/runtime/dev-compiler-broker.mjs +20 -23
  67. package/runtime/dev-gateway.mjs +23 -6
  68. package/runtime/dev-route-selection.mjs +64 -0
  69. package/runtime/dev-runtime-pool.mjs +3 -2
  70. package/runtime/dev-runtime-spare.mjs +1 -1
  71. package/runtime/dev-session-retention.mjs +154 -0
  72. package/runtime/development-flight.mjs +59 -0
  73. package/runtime/development-queue.mjs +33 -0
  74. package/runtime/development.mjs +37 -16
  75. package/runtime/env.mjs +9 -0
  76. package/runtime/flight-delta.mjs +31 -5
  77. package/runtime/flight-diagnostics.mjs +48 -0
  78. package/runtime/flight-tap.mjs +10 -5
  79. package/runtime/html-envelope.mjs +34 -0
  80. package/runtime/loopback-rpc.mjs +92 -0
  81. package/runtime/native-ppr.mjs +88 -0
  82. package/runtime/og/NOTICE.txt +1 -1
  83. package/runtime/og/index.node.mjs +76 -31216
  84. package/runtime/og/upstream.json +115 -0
  85. package/runtime/persistent/client.mjs +5 -3
  86. package/runtime/persistent/common.mjs +7 -18
  87. package/runtime/persistent/daemon.mjs +4 -12
  88. package/runtime/persistent/health.mjs +58 -0
  89. package/runtime/persistent/native-proxy.mjs +1 -1
  90. package/runtime/persistent/watchdog.mjs +12 -27
  91. package/runtime/prewarm.mjs +35 -0
  92. package/runtime/project-lock.mjs +71 -0
  93. package/runtime/request-transport.mjs +112 -32
  94. package/runtime/rsc-worker.mjs +58 -8
  95. package/runtime/storage-path.mjs +36 -0
  96. package/runtime/trace.mjs +44 -4
  97. package/runtime/worker.mjs +37 -17
  98. package/vendor/fontkit/NOTICE.txt +5 -0
  99. package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
  100. package/vendor/fontkit/index.mjs +3 -0
  101. package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
  102. package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
  103. package/vendor/fontkit/licenses/brotli/readme.md +61 -0
  104. package/vendor/fontkit/licenses/clone/LICENSE +18 -0
  105. package/vendor/fontkit/licenses/dfa/README.md +83 -0
  106. package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
  107. package/vendor/fontkit/licenses/fontkit/README.md +266 -0
  108. package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
  109. package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
  110. package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
  111. package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
  112. package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
  113. package/vendor/fontkit/upstream.json +215 -0
@@ -1,6 +1,6 @@
1
1
  import { deploymentHeaders, assertDeploymentResponse } from './deployment-client.mjs';
2
2
  import React, { startTransition, useLayoutEffect } from 'react';
3
- import { preload } from 'react-dom';
3
+ import { preload, flushSync } from 'react-dom';
4
4
  import { PARTIAL_PREFETCH_TYPE, PARTIAL_PREFETCH_CACHE_BYTES } from './app-prefetch-constants.mjs';
5
5
  import { isDynamicBailout } from '../compat/dynamic-bailout.cjs';
6
6
  import { createRoot, hydrateRoot } from 'react-dom/client';
@@ -21,11 +21,13 @@ import {recoverNavigation} from './navigation-recovery.mjs';
21
21
  import { createActivityCache } from '../compat/activity-cache.cjs';
22
22
  import { createFlightTransport, TYPE as FLIGHT_DELTA_TYPE } from '../compat/flight-delta.cjs';
23
23
  import {decodeBase64} from '../compat/base64.cjs';
24
+ import { supersedableDevelopmentFlight, developmentFlightRoot } from './development-flight.mjs';
24
25
 
25
26
  const PREFETCH_LIMIT = 8;
26
27
  const PREFETCH_LIFETIME = 30_000;
27
28
  let application;
28
29
  let strictMode = true;
30
+ const navigationStreams = new WeakMap();
29
31
 
30
32
  function loadClientModule(source) {
31
33
  if (typeof source === 'function') return source();
@@ -144,10 +146,11 @@ function NormalStyles({ css, fonts }) {
144
146
 
145
147
  function AppRoot({ model, controller, committed, url, globalErrorComponent, globalErrorCss, retentionEpoch, retentionAuthorizationEpoch }) {
146
148
  const [hydrated, setHydrated] = React.useState(false);
149
+ const flight = navigationStreams.get(model);
147
150
  const cache = React.useRef(null);
148
151
  if (!cache.current) cache.current = createActivityCache({ limit: controller._navigationCache?.maxEntries, ttl: controller._navigationCache?.ttlMs });
149
152
  const activityCache = React.useMemo(() => ({ cache: cache.current, epoch: retentionEpoch, authorizationEpoch: retentionAuthorizationEpoch,
150
- authorizationKey: model.authorizationKey, strictAuthorization: controller._navigationCache?.authCookies !== undefined }), [retentionEpoch, retentionAuthorizationEpoch, model.authorizationKey]);
153
+ authorizationKey: model.authorizationKey, strictAuthorization: controller._navigationCache?.authCookies !== undefined, flight }), [retentionEpoch, retentionAuthorizationEpoch, model.authorizationKey, flight]);
151
154
  const previewDynamic = React.useCallback(() => { throw model.partialPreview.suspended; }, [model.partialPreview]);
152
155
  useLayoutEffect(() => {
153
156
  if (process.env.NODE_ENV === 'development' && model.instantDiagnostics?.length) globalThis.__PRNEXT_DEV__?.reportError(Object.assign(new Error(model.instantDiagnostics.join('\n')), { digest: 'PRNEXT_INSTANT_BLOCKING' }));
@@ -255,13 +258,17 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
255
258
  // entry with the previous view's routing state on the next scroll event.
256
259
  let historyRouting = currentRouting;
257
260
  let currentModel = initialModel;
258
- let committedView = { model: initialModel, routing: currentRouting, url: currentUrl.href };
259
- let pendingView;
261
+ let committedView = { model: initialModel, routing: currentRouting, url: currentUrl.href, retentionEpoch: 0, authorizationEpoch: 0 };
262
+ // Development navigation can display a newer component generation before its
263
+ // last Flight row arrives. Keep that visible tree separately from the last
264
+ // genuinely complete response; superseding it must not restore older types.
265
+ let developmentView;
266
+ let pendingView, pendingNavigation;
260
267
  const historyFocus = new Map();
261
268
  let currentKey = window.history.state?.__prnext?.key || 0;
262
269
  let nextKey = currentKey + 1;
263
270
  let navigation = 0;
264
- let activeRequest, finishNavigation, finishCommit, pageHidden = false, notifyingPrefetch = false;
271
+ let activeRequest, finishNavigation, finishCommit, documentTimer, pageHidden = false, notifyingPrefetch = false, documentFallback = false;
265
272
  const prefetches = new Map(), scheduler = createPrefetchScheduler();
266
273
  const partialSegments = (typeof __PRNEXT_CACHE_COMPONENTS__ === 'undefined' ? cacheComponents : __PRNEXT_CACHE_COMPONENTS__) ? new Map() : null;
267
274
  let segmentBytes = 0;
@@ -278,34 +285,109 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
278
285
  }
279
286
  }
280
287
 
281
- function renderView(model, url, { token = navigation, canonical = true, committed } = {}) {
282
- const view = { model, token };
288
+ function renderView(model, url, { token = navigation, canonical = true, kind = 'navigation', committed } = {}) {
289
+ const flight = navigationStreams.get(model), view = { model, token, kind, committed: false };
283
290
  pendingView = view;
284
- startTransition(() => root.render(appView({ model, controller, url, globalErrorComponent, globalErrorCss,
291
+ const promote = () => {
292
+ if (token !== navigation || pendingView !== view || !view.committed || flight?.complete === false) return;
293
+ pendingView = undefined;
294
+ if (flight) flight.committed = undefined;
295
+ // A committed loading shell can still contain rows owned by a live
296
+ // fetch. Keep the last fully received view available before aborting it.
297
+ // Authorized PPR previews likewise never become the canonical model.
298
+ if (canonical) {
299
+ committedView = { model, routing: model.routing, url, element, retentionEpoch: controller._retentionEpoch, authorizationEpoch: controller._authorizationEpoch };
300
+ developmentView = undefined;
301
+ }
302
+ };
303
+ if (flight) flight.committed = promote;
304
+ const element = appView({ model, controller, url, globalErrorComponent, globalErrorCss,
285
305
  committed: () => {
286
306
  if (token !== navigation || pendingView !== view) return;
287
- pendingView = undefined;
288
- // A PPR preview is authorized UI, but not the completed route model
289
- // from which a later navigation should inherit its layout cache.
290
- if (canonical) committedView = { model, routing: model.routing, url };
307
+ view.committed = true;
308
+ if (canonical && flight?.development && kind !== 'action') developmentView = { model, routing: model.routing, url, element, retentionEpoch: controller._retentionEpoch, authorizationEpoch: controller._authorizationEpoch };
309
+ promote();
291
310
  committed?.();
292
311
  },
293
- })));
312
+ });
313
+ view.element = element;
314
+ startTransition(() => root.render(element));
294
315
  }
295
316
 
296
317
  function abandonPendingView() {
297
- if (!pendingView) return;
318
+ if (!pendingView) return true;
319
+ const flight = navigationStreams.get(pendingView.model);
320
+ const strictAuthorization = controller._navigationCache?.authCookies !== undefined;
321
+ const retained = developmentView || committedView;
322
+ if (flight) flight.committed = undefined;
323
+ if (pendingView.kind === 'action' && committedView.authorizationEpoch === controller._authorizationEpoch &&
324
+ (!strictAuthorization || committedView.model.authorizationKey === currentModel.authorizationKey)) {
325
+ // An action's received result can invoke a returned action before React
326
+ // commits its UI. This response is never owned/aborted by activeRequest;
327
+ // adopt its model instead of rolling back the prior mutation's epoch.
328
+ committedView = { model: currentModel, routing: currentRouting, url: currentUrl.href, element: pendingView.element, retentionEpoch: controller._retentionEpoch, authorizationEpoch: controller._authorizationEpoch };
329
+ pendingView = undefined;
330
+ developmentView = undefined;
331
+ return true;
332
+ }
333
+ // A new authorized shell or explicit invalidation can make the last full
334
+ // model private to an obsolete session. Reauthorize through a document
335
+ // request instead of briefly restoring that model while C is pending.
336
+ if ((strictAuthorization && retained.model.authorizationKey !== currentModel.authorizationKey) ||
337
+ retained.retentionEpoch !== controller._retentionEpoch ||
338
+ retained.authorizationEpoch !== controller._authorizationEpoch ||
339
+ (retained.model.rootLayout && currentModel.rootLayout && retained.model.rootLayout !== currentModel.rootLayout)) return false;
298
340
  pendingView = undefined;
299
- currentModel = committedView.model;
300
- currentRouting = committedView.routing;
341
+ currentModel = retained.model;
342
+ currentRouting = retained.routing;
301
343
  // React can already be reading a lazy row after createFromFetch resolves
302
344
  // the root model. Replace that suspended render before abort rejects its
303
345
  // remaining rows; it must not poison the retained layout or page cache.
304
- root.render(appView({ model: committedView.model, controller, url: committedView.url, globalErrorComponent, globalErrorCss }));
346
+ // Reuse the exact submitted element. Before a replacement refresh commits,
347
+ // rebuilding the older Flight lazies after Refresh can itself remount them.
348
+ root.render(retained.element || appView({ model: retained.model, controller, url: retained.url, globalErrorComponent, globalErrorCss }));
349
+ return true;
305
350
  }
306
351
 
352
+ function fallbackDocument(url, replace = true, defer = false) {
353
+ // The invalidated document stays blocked until its replacement arrives.
354
+ // Repeated synchronous actions must neither dispatch nor start reloads.
355
+ if (documentFallback) return;
356
+ documentFallback = true;
357
+ navigation++;
358
+ activeRequest?.abort();
359
+ appStyleManager(document).cancel();
360
+ finishNavigation?.(); finishNavigation = undefined;
361
+ invalidatePrefetches();
362
+ flightTransport.clear();
363
+ if (controller._navigationCache?.authCookies !== undefined) document.documentElement.style.visibility = 'hidden';
364
+ const request = () => {
365
+ documentTimer = undefined;
366
+ if (pageHidden) return;
367
+ if (replace) window.location.replace(url);
368
+ else window.location.assign(url);
369
+ };
370
+ // An executed action must settle its promise chain before document
371
+ // replacement suspends the old realm. Its POST guard is already latched.
372
+ if (defer) documentTimer = setTimeout(request, 0);
373
+ else request();
374
+ }
375
+
376
+ function requiresAuthorizedDocument(model) {
377
+ if (controller._navigationCache?.authCookies === undefined) return false;
378
+ const identity = model?.authorizationKey;
379
+ // Activity entries are only part of the retained tree: shared client
380
+ // layouts and server layout-cache props can also belong to the old user.
381
+ // An unidentified static model cannot prove that HttpOnly cookies stayed
382
+ // the same. Cross this boundary before adopting any cached layout state.
383
+ return typeof identity !== 'string' || !identity || identity !== currentModel.authorizationKey ||
384
+ committedView.authorizationEpoch !== controller._authorizationEpoch;
385
+ }
386
+
387
+ const actionReloadError = () => new Error('The session changed while this page was loading. Reload before repeating the action.');
388
+
307
389
  const flightTransport = createFlightTransport();
308
- async function requestFlight(url, signal, routing = currentRouting, onPartial, {skipEnsure=false,fullFlight=false}={}) {
390
+ async function requestFlight(url, signal, routing = currentRouting, onPartial, {skipEnsure=false,fullFlight=false,developmentSupersession}={}) {
309
391
  if(process.env.NODE_ENV==='development'&&!routing?.refresh&&!skipEnsure)await globalThis.__PRNEXT_DEV__?.ensureRoute?.(url.href);
310
392
  const exported = window.__PRNEXT_STATIC_EXPORT__ === true;
311
393
  const base = !exported && !fullFlight ? flightTransport.snapshot() : undefined;
@@ -326,19 +408,30 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
326
408
  }
327
409
  await onPartial?.(response, destination);
328
410
  let model;
329
- try { model = assertModel(await createFromFetch(Promise.resolve(exported?response:await flightTransport.response(response,base)), flightOptions)); }
411
+ const flight = { complete: false, development: process.env.NODE_ENV === 'development' && !!developmentSupersession };
412
+ try {
413
+ const received = await flightTransport.response(response,base,{onEnd(){flight.complete = true;flight.committed?.();}});
414
+ if (process.env.NODE_ENV === 'development' && developmentSupersession) {
415
+ model = assertModel(await developmentFlightRoot(createFromReadableStream(supersedableDevelopmentFlight(received.body, developmentSupersession), { ...flightOptions, unstable_allowPartialStream: true }), signal));
416
+ } else model = assertModel(await createFromFetch(Promise.resolve(received), flightOptions));
417
+ }
330
418
  catch(error) {
331
419
  if(!delta||fullFlight||signal?.aborted)throw error;
332
420
  flightTransport.clear();
333
- return requestFlight(url,signal,routing,onPartial,{skipEnsure:true,fullFlight:true});
421
+ return requestFlight(url,signal,routing,onPartial,{skipEnsure:true,fullFlight:true,developmentSupersession});
334
422
  }
423
+ navigationStreams.set(model, flight);
335
424
  model.router.basePath = basePath;
336
425
  const rewrite = readRewriteHeader(response);
337
426
  if (rewrite) model.rewrite = rewrite;
338
427
  return { model, url: destination };
339
428
  }
340
429
 
341
- const navigateFlight=(url,signal,...args)=>recoverNavigation(recoverySignal=>requestFlight(url,recoverySignal,...args),{signal});
430
+ // Only development navigation GETs may retain a DOM-committed partial tree.
431
+ // Direct prefetch, production navigation and Action POST decoders stay strict.
432
+ const navigateFlight=(url,signal,routing,onPartial,options={})=>recoverNavigation(recoverySignal=>requestFlight(url,recoverySignal,routing,onPartial,{
433
+ ...options, ...(process.env.NODE_ENV === 'development' ? { developmentSupersession: signal } : {}),
434
+ }),{signal});
342
435
 
343
436
  const requestPartial = (typeof __PRNEXT_CACHE_COMPONENTS__ === 'undefined' ? cacheComponents : __PRNEXT_CACHE_COMPONENTS__) ? async function requestPartial(url, signal, full = false) {
344
437
  const response = await fetch(url.href, { credentials: 'same-origin', signal,
@@ -408,15 +501,20 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
408
501
 
409
502
  function callServer(id, args) {
410
503
  if (!root) return Promise.reject(new Error('Server Actions cannot run during initial rendering'));
504
+ if (pageHidden || documentFallback) return Promise.reject(actionReloadError());
411
505
  const token = ++navigation;
412
506
  finishCommit?.();
413
507
  const target = new URL(window.location.href);
414
508
  saveScroll();
415
- abandonPendingView();
509
+ const restored = abandonPendingView();
416
510
  activeRequest?.abort();
417
511
  appStyleManager(document).cancel();
418
512
  finishNavigation?.(); finishNavigation = undefined;
419
513
  activeRequest = undefined;
514
+ if (!restored) {
515
+ fallbackDocument(target.href);
516
+ return Promise.reject(actionReloadError());
517
+ }
420
518
  purgeRetainedPages();
421
519
  // Mutations are dispatched in order, including Set-Cookie effects required
422
520
  // by a subsequent action. Capture their route priority when invoked: a
@@ -427,16 +525,17 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
427
525
  }
428
526
 
429
527
  async function dispatchAction(id, args, token, target) {
528
+ if (pageHidden || documentFallback) throw actionReloadError();
430
529
  const release = scheduler.navigation();
431
530
  try { return await performAction(id, args, token, target); } finally { release(); }
432
531
  }
433
532
 
434
533
  async function waitForDevelopmentUpdate() {
435
534
  // A generation updates the Flight loader before React Refresh registers
436
- // the replacement component families. An already-running action may finish
437
- // in that interval; rendering its model immediately remounts those client
438
- // components and loses layout state. Wait only on development bookkeeping,
439
- // never replay the mutation or cancel a newer user navigation.
535
+ // the replacement component families. An already-running Flight request
536
+ // may finish in that interval; rendering its model immediately can remount
537
+ // those client components and lose layout state. Wait only on development
538
+ // bookkeeping, never replay a request or cancel a newer user navigation.
440
539
  let development = globalThis.__PRNEXT_DEV__;
441
540
  while (development?.applying && development.applied) {
442
541
  await development.applied;
@@ -448,6 +547,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
448
547
  invalidatePrefetches();
449
548
  const temporaryReferences = createTemporaryReferenceSet();
450
549
  const body = await encodeReply(args, { temporaryReferences });
550
+ if (pageHidden || documentFallback) throw actionReloadError();
451
551
  const response = await fetch(target.href, {
452
552
  method: 'POST',
453
553
  headers: { ...deploymentHeaders(deploymentId), 'Next-Action': id, RSC: '1', Accept: 'text/x-component', ...(currentRouting ? { 'x-prnext-router-state': encodeURIComponent(JSON.stringify({ ...currentRouting, refresh: true })) } : {}) },
@@ -474,8 +574,21 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
474
574
  };
475
575
  if (process.env.NODE_ENV === 'development') await waitForDevelopmentUpdate();
476
576
  controller._retentionEpoch++;
577
+ const validOutcome = model && typeof model === 'object' && (Object.hasOwn(model, 'tree') || Object.hasOwn(model, 'actionResult') || model.actionError || model.actionRedirect || model.routingInvalid);
578
+ if (!validOutcome) throw new Error('The Server Action response did not contain a result');
579
+ if (Object.hasOwn(model, 'tree') && !model.routingInvalid) assertModel(model);
580
+ if (requiresAuthorizedDocument(model)) {
581
+ // A queued action or newer navigation can supersede the render token,
582
+ // but cannot make an observed account change safe to ignore. Complete
583
+ // this one POST and block the queue before any subsequent mutation.
584
+ const latest = pendingNavigation?.token === navigation ? pendingNavigation : undefined;
585
+ const redirected = token === navigation && model.actionRedirect ? navigationUrl(model.actionRedirect.url) : undefined;
586
+ const destination = latest?.url || redirected?.href || window.location.href;
587
+ fallbackDocument(destination, latest ? latest.replace : redirected ? model.actionRedirect.type === 'replace' : true, true);
588
+ return outcome();
589
+ }
477
590
  if (model?.routingInvalid || (model?.rootLayout && currentModel.rootLayout && model.rootLayout !== currentModel.rootLayout)) {
478
- if (token === navigation) window.location.replace(target.href);
591
+ if (token === navigation) fallbackDocument(target.href);
479
592
  return outcome();
480
593
  }
481
594
  if (model?.actionRedirect) {
@@ -497,7 +610,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
497
610
  currentModel = model;
498
611
  historyRouting = currentRouting;
499
612
  appStyleManager(document).handoff(model.globalError ? [] : model.css || []);
500
- renderView(model, target.href, { token });
613
+ renderView(model, target.href, { token, kind: 'action' });
501
614
  }
502
615
  if (model?.actionError) return outcome();
503
616
  // Flight's JSON reviver removes object fields encoded as $undefined. A
@@ -508,22 +621,32 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
508
621
  return outcome();
509
622
  }
510
623
 
511
- async function navigate(href, { replace = false, scroll = true, pop = false, savedScroll, savedRouting, refresh = false } = {}) {
512
- if (pageHidden) return;
624
+ async function navigate(href, { replace = false, scroll = true, pop = false, savedScroll, savedRouting, refresh = false, developmentSignal } = {}) {
625
+ if (developmentSignal?.aborted) return false;
626
+ if (pageHidden || documentFallback) return developmentSignal ? false : undefined;
513
627
  const target = navigationUrl(href);
514
628
  if (!isApplicationURL(target, window.location.origin, basePath) || isHtmlDocumentRoute(htmlOnlyRoutes, removeBasePath(target.pathname, basePath))) {
515
- if (replace) window.location.replace(target.href);
516
- else window.location.assign(target.href);
517
- return;
629
+ fallbackDocument(target.href, replace);
630
+ return developmentSignal ? false : undefined;
518
631
  }
519
632
  const token = ++navigation;
633
+ // The compiler generation may remain valid when an ordinary navigation or
634
+ // Action replaces only its pending UI. This is distinct from a DOM commit,
635
+ // an owner cancellation, or a genuine failed request.
636
+ const superseded = () => developmentSignal && token !== navigation && !developmentSignal.aborted && !pageHidden && !documentFallback ? 'superseded' : false;
637
+ const intent = { token, url: target.href, replace: replace || pop || refresh };
638
+ pendingNavigation = intent;
520
639
  finishCommit?.();
521
640
  saveScroll();
522
- abandonPendingView();
641
+ const restored = abandonPendingView();
523
642
  activeRequest?.abort();
524
643
  appStyleManager(document).cancel();
525
644
  finishNavigation?.(); finishNavigation = undefined;
526
645
  activeRequest = undefined;
646
+ if (!restored) {
647
+ fallbackDocument(target.href, replace || pop || refresh);
648
+ return developmentSignal ? false : undefined;
649
+ }
527
650
 
528
651
  function updateHistory(destination) {
529
652
  historyRouting = currentRouting;
@@ -545,7 +668,8 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
545
668
  target.pathname === committedUrl.pathname && target.search === committedUrl.search) {
546
669
  updateHistory(target);
547
670
  if (scroll) scrollToTarget(target, savedScroll);
548
- return;
671
+ if (pendingNavigation === intent) pendingNavigation = undefined;
672
+ return developmentSignal ? false : undefined;
549
673
  }
550
674
 
551
675
  const cacheKey = `${target.origin}${target.pathname}${target.search}`;
@@ -575,6 +699,12 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
575
699
  const abort = new AbortController();
576
700
  activeRequest = abort;
577
701
  result = await navigateFlight(target, abort.signal, currentRouting, async (response, destination) => {
702
+ if (process.env.NODE_ENV === 'development') await waitForDevelopmentUpdate();
703
+ if (token !== navigation || abort.signal.aborted) return;
704
+ // A cached shell (including any cached authorization key) cannot
705
+ // establish this request's live session identity. Strict account
706
+ // isolation waits for the freshly authorized Flight model instead.
707
+ if (controller._navigationCache?.authCookies !== undefined) return;
578
708
  if (!preview || token !== navigation || response.headers.get('x-prnext-ppr-id') !== preview.id ||
579
709
  (preview.model.rootLayout && currentModel.rootLayout && preview.model.rootLayout !== currentModel.rootLayout)) return;
580
710
  // Auth, middleware and rewrites ran again. A changed target or a
@@ -582,6 +712,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
582
712
  if (destination.pathname !== preview.url.pathname || destination.search !== preview.url.search) return;
583
713
  prepareLayoutCache(preview.model, currentModel);
584
714
  await appStyleManager(document).prepare(preview.model.css || [], abort.signal);
715
+ if (process.env.NODE_ENV === 'development') await waitForDevelopmentUpdate();
585
716
  if (token !== navigation || abort.signal.aborted) return;
586
717
  updateHistory(destination); historyUpdated = true;
587
718
  appStyleManager(document).handoff(preview.model.css || []);
@@ -594,14 +725,26 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
594
725
  // A refresh is already inside the compiler's apply transaction. Calling
595
726
  // ensureRoute from here can wait for that same transaction forever when
596
727
  // this tree has no routing state (the ordinary single-route case).
597
- result = await navigateFlight(target, abort.signal, pop && savedRouting ? { ...savedRouting, restore: true } : refresh && currentRouting ? { ...currentRouting, refresh: true } : currentRouting, undefined, {skipEnsure:refresh});
728
+ const signal = developmentSignal ? AbortSignal.any([abort.signal, developmentSignal]) : abort.signal;
729
+ result = await navigateFlight(target, signal, pop && savedRouting ? { ...savedRouting, restore: true } : refresh && currentRouting ? { ...currentRouting, refresh: true } : currentRouting, undefined, {skipEnsure:refresh,developmentSupersession:developmentSignal ? signal : undefined});
598
730
  }
599
- if (token !== navigation) return;
731
+ // Internal refreshes execute inside applyUpdate and must never wait on
732
+ // their own transaction. Ordinary navigations can already be in flight
733
+ // when an on-demand route publishes another client-module generation.
734
+ if (process.env.NODE_ENV === 'development' && !refresh) await waitForDevelopmentUpdate();
735
+ if (token !== navigation || activeRequest?.signal.aborted || developmentSignal?.aborted) return superseded();
600
736
  if (!isApplicationURL(result.url, window.location.origin, basePath)) throw new Error('The navigation destination requires a document request');
601
737
  if (result.model.rootLayout && currentModel.rootLayout && result.model.rootLayout !== currentModel.rootLayout) throw new Error('A different root layout requires a document request');
738
+ if (requiresAuthorizedDocument(result.model)) {
739
+ activeRequest?.abort();
740
+ fallbackDocument(result.url.href, replace || pop || refresh);
741
+ return developmentSignal ? false : undefined;
742
+ }
602
743
  prepareLayoutCache(result.model, currentModel);
603
- await appStyleManager(document).prepare(result.model.globalError ? [] : result.model.css || [], activeRequest?.signal);
604
- if (token !== navigation) return;
744
+ const styleSignal = developmentSignal && activeRequest ? AbortSignal.any([activeRequest.signal, developmentSignal]) : activeRequest?.signal;
745
+ await appStyleManager(document).prepare(result.model.globalError ? [] : result.model.css || [], styleSignal);
746
+ if (process.env.NODE_ENV === 'development' && !refresh) await waitForDevelopmentUpdate();
747
+ if (token !== navigation || activeRequest?.signal.aborted || developmentSignal?.aborted) return superseded();
605
748
  currentRouting = result.model.routing;
606
749
  currentModel = result.model;
607
750
  historyRouting = currentRouting;
@@ -615,36 +758,59 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
615
758
  }
616
759
  };
617
760
  appStyleManager(document).handoff(result.model.globalError ? [] : result.model.css || []);
761
+ let didCommit = false;
618
762
  if(process.env.NODE_ENV==='development'&&refresh)await new Promise(resolve=>{
619
- const finish=()=>{if(finishCommit===finish)finishCommit=undefined;resolve();};
763
+ const finish=()=>{developmentSignal?.removeEventListener('abort', finish);if(finishCommit===finish)finishCommit=undefined;resolve();};
620
764
  finishCommit=finish;
621
- renderView(result.model, result.url.href, { token, committed: () => { committed(); finish(); } });
765
+ developmentSignal?.addEventListener('abort', finish, { once: true });
766
+ if (developmentSignal?.aborted) { finish(); return; }
767
+ renderView(result.model, result.url.href, { token, kind: developmentSignal ? 'development-refresh' : 'navigation', committed: () => { didCommit = true; committed(); finish(); } });
622
768
  });
623
769
  else renderView(result.model, result.url.href, { token, committed });
770
+ if (!developmentSignal) return;
771
+ if (developmentSignal.aborted || pageHidden || documentFallback) return false;
772
+ return didCommit || superseded();
624
773
  } catch (error) {
625
- if (token !== navigation || error?.name === 'AbortError') return;
774
+ if (developmentSignal?.aborted) return false;
626
775
  if(error?.code==='PRNEXT_NAVIGATION_NETWORK'){
776
+ // A deadline failure is never evidence that only the UI was replaced.
777
+ if (token !== navigation) return false;
627
778
  window.dispatchEvent(new CustomEvent('prnext:navigation-error',{detail:{message:error.message}}));
628
- return;
779
+ return developmentSignal ? false : undefined;
629
780
  }
781
+ if (token !== navigation) return error?.name === 'AbortError' ? superseded() : false;
782
+ if (error?.name === 'AbortError') return false;
630
783
  // Pages Router routes, public files, and unavailable Flight endpoints use
631
784
  // a normal document navigation, including the browser's redirect handling.
632
- if (replace || pop || refresh) window.location.replace(target.href);
633
- else window.location.assign(target.href);
785
+ fallbackDocument(target.href, replace || pop || refresh);
786
+ return developmentSignal ? false : undefined;
634
787
  } finally {
788
+ if (pendingNavigation === intent) pendingNavigation = undefined;
635
789
  if (finishNavigation === releaseNavigation) finishNavigation = undefined;
636
790
  releaseNavigation();
637
791
  }
638
792
  }
639
793
 
640
- function purgeRetainedPages() {
794
+ function purgeRetainedPages({ synchronous = false } = {}) {
795
+ if (pageHidden || documentFallback) return false;
641
796
  saveScroll();
642
- abandonPendingView();
797
+ if (!abandonPendingView()) {
798
+ activeRequest?.abort();
799
+ fallbackDocument(currentUrl.href);
800
+ return false;
801
+ }
643
802
  controller._retentionEpoch++;
644
803
  historyFocus.clear();
804
+ if (committedView.model === currentModel && committedView.authorizationEpoch === controller._authorizationEpoch) committedView = { ...committedView, retentionEpoch: controller._retentionEpoch };
805
+ if (developmentView?.model === currentModel && developmentView.authorizationEpoch === controller._authorizationEpoch) developmentView = { ...developmentView, retentionEpoch: controller._retentionEpoch };
645
806
  // This only updates retention for the already committed model. It must
646
807
  // not become a new pending Flight view or a development commit barrier.
647
- startTransition(() => root.render(appView({ model: currentModel, controller, url: committedView.url, globalErrorComponent, globalErrorCss })));
808
+ const element = appView({ model: currentModel, controller, url: (developmentView || committedView).url, globalErrorComponent, globalErrorCss });
809
+ if (committedView.model === currentModel) committedView = { ...committedView, element };
810
+ if (developmentView?.model === currentModel) developmentView = { ...developmentView, element };
811
+ if (synchronous) flushSync(() => root.render(element));
812
+ else startTransition(() => root.render(element));
813
+ return true;
648
814
  }
649
815
 
650
816
  const controller = {
@@ -656,13 +822,13 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
656
822
  replace: (href, options) => { void navigate(navigationUrl(href), { ...options, replace: true }); },
657
823
  refresh: () => {
658
824
  invalidatePrefetches();
659
- purgeRetainedPages();
825
+ if (!purgeRetainedPages()) return;
660
826
  void navigate(new URL(window.location.href), { refresh: true });
661
827
  },
662
828
  back: () => { saveScroll(); window.history.back(); },
663
829
  forward: () => { saveScroll(); window.history.forward(); },
664
830
  prefetch(href, options = {}) {
665
- if (pageHidden) return;
831
+ if (pageHidden || documentFallback) return;
666
832
  const target = navigationUrl(href);
667
833
  if (!isApplicationURL(target, window.location.origin, basePath) || isHtmlDocumentRoute(htmlOnlyRoutes, removeBasePath(target.pathname, basePath))) return;
668
834
  target.hash = '';
@@ -703,6 +869,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
703
869
  let resolveDevReady;
704
870
  const devReady=process.env.NODE_ENV==='development'?new Promise(resolve=>{resolveDevReady=resolve;}):undefined;
705
871
  const initialView = appView({ model: initialModel, controller, globalErrorComponent, globalErrorCss, ...(devReady?{committed:resolveDevReady}:{}) });
872
+ committedView.element = initialView;
706
873
  const rootOptions = {
707
874
  formState: initialModel.formState ?? null,
708
875
  onCaughtError(error) {
@@ -727,8 +894,22 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
727
894
  window.addEventListener('popstate', event => {
728
895
  void navigate(new URL(window.location.href), { pop: true, savedScroll: event.state?.__prnext?.scroll, savedRouting: event.state?.__prnext?.routing });
729
896
  });
730
- window.addEventListener('pagehide', () => { saveScroll(); pageHidden = true; navigation++; finishCommit?.(); abandonPendingView(); activeRequest?.abort(); appStyleManager(document).cancel(); finishNavigation?.(); finishNavigation = undefined; invalidatePrefetches(); });
731
- window.addEventListener('pageshow', event => { pageHidden = false; if (event.persisted) controller.refresh(); });
897
+ window.addEventListener('pagehide', () => {
898
+ saveScroll(); pageHidden = true; navigation++; finishCommit?.(); abandonPendingView();
899
+ clearTimeout(documentTimer); documentTimer = undefined;
900
+ activeRequest?.abort(); appStyleManager(document).cancel(); finishNavigation?.(); finishNavigation = undefined; invalidatePrefetches();
901
+ // A cached document may contain an account's shared layout state. Keep it
902
+ // hidden if restored from BFCache until the next document authorizes it.
903
+ if (controller._navigationCache?.authCookies !== undefined) document.documentElement.style.visibility = 'hidden';
904
+ });
905
+ window.addEventListener('pageshow', event => {
906
+ pageHidden = false;
907
+ if (!event.persisted) return;
908
+ if (controller._navigationCache?.authCookies !== undefined) {
909
+ documentFallback = false;
910
+ fallbackDocument(window.location.href);
911
+ } else controller.refresh();
912
+ });
732
913
  window.addEventListener('prnext:invalidate-navigation', () => { controller._authorizationEpoch++; controller.refresh(); });
733
914
  let scrollFrame;
734
915
  window.addEventListener('scroll', () => {
@@ -747,7 +928,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
747
928
  return { root, router: controller, ...(process.env.NODE_ENV === 'development' ? {
748
929
  devReady,
749
930
  devStyles: routes => developmentStyles(currentModel, routes).css || [],
750
- devPrepareStyles: routes => appStyleManager(document).prepare(developmentStyles(currentModel, routes).css || []),
931
+ devPrepareStyles: (routes, signal) => appStyleManager(document).prepare(developmentStyles(currentModel, routes).css || [], signal),
751
932
  devStylesApplied: (routes, { commit = true } = {}) => {
752
933
  for (const model of new Set([currentModel, committedView.model, pendingView?.model].filter(Boolean))) {
753
934
  const next = developmentStyles(model, routes);
@@ -756,7 +937,18 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
756
937
  if (commit) appStyleManager(document).commit(currentModel.globalError ? [] : currentModel.css || []);
757
938
  },
758
939
  devPrepare: options => moduleLoader.updateModules(options.clientModules),
759
- devCommit: () => {invalidatePrefetches();purgeRetainedPages();return navigate(new URL(window.location.href),{refresh:true});},
940
+ devBeforeRefresh: ({ navigation: routeOpening } = {}) => {
941
+ if (!routeOpening) { invalidatePrefetches(); return purgeRetainedPages({ synchronous: true }); }
942
+ // A cold route's module publication must not invalidate retained pages.
943
+ // Quiesce the old visible tree under its old families before Refresh,
944
+ // without changing epochs, cancelling the click, or committing its URL.
945
+ saveScroll();
946
+ if (!abandonPendingView()) { activeRequest?.abort(); fallbackDocument(currentUrl.href); return false; }
947
+ const retained = developmentView || committedView;
948
+ flushSync(() => root.render(retained.element));
949
+ return true;
950
+ },
951
+ devCommit: (_options, { signal } = {}) => navigate(new URL(window.location.href), { refresh: true, developmentSignal: signal }),
760
952
  } : {}) };
761
953
  }
762
954
 
@@ -8,9 +8,10 @@ import { appContent } from './app-content.mjs';
8
8
  import { runRequestContext, currentRequest } from '../compat/headers.cjs';
9
9
  import { cachedValue, cacheGeneration, flushCacheWork } from '../compat/data-cache.cjs';
10
10
  import { partialModel, preparePartialModel } from './app-partial-model.mjs';
11
- import { MAX_RESPONSE_BYTES, escapeHtml } from './http.mjs';
11
+ import { escapeHtml, MAX_RESPONSE_BYTES } from './http.mjs';
12
12
  import { fontPreloads } from './font-preload.mjs';
13
13
  import { PartialArtifactCache } from './partial-artifact-cache.mjs';
14
+ export {partialShell} from './native-ppr.mjs';
14
15
 
15
16
  const artifacts = new Map();
16
17
  const parsedArtifacts = new PartialArtifactCache();
@@ -32,7 +33,6 @@ async function readArtifact(file) {
32
33
  }
33
34
  return value;
34
35
  }
35
-
36
36
  async function bytes(stream) {
37
37
  const chunks = [];
38
38
  let length = 0;
@@ -120,11 +120,3 @@ export async function partialArtifact({ request, route, manifest, filename, iden
120
120
  } finally { await flushCacheWork(context); }
121
121
  });
122
122
  }
123
-
124
- export function partialShell(artifact, route) {
125
- const bootstrap = '<script>self.__PRNEXT_FLIGHT_STREAM__=self.__PRNEXT_FLIGHT_STREAM__||[];</script>' +
126
- (route.client ? `<script type="module" async src="${escapeHtml(route.client)}"></script>` : '');
127
- const end = artifact.shell.lastIndexOf('</body>');
128
- if (end < 0 || !/<\/body><\/html>$/i.test(artifact.shell)) throw new Error('Invalid partial prerender document');
129
- return artifact.shell.slice(0, end) + bootstrap + artifact.shell.slice(end);
130
- }
@@ -1,6 +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
3
  import { PARTIAL_PREFETCH_BYTES } from './app-prefetch-constants.mjs';
4
+ import { decodeBase64 } from '../compat/base64.cjs';
4
5
  export { PARTIAL_PREFETCH_TYPE, PARTIAL_PREFETCH_BYTES, PARTIAL_PREFETCH_CACHE_BYTES } from './app-prefetch-constants.mjs';
5
6
 
6
7
  export async function readPartialPrefetch(response) {
@@ -27,7 +28,6 @@ export async function readPartialPrefetch(response) {
27
28
  }
28
29
 
29
30
  export function partialFlightStream(base64) {
30
- const binary = atob(base64);
31
- const bytes = Uint8Array.from(binary, character => character.charCodeAt(0));
31
+ const bytes = decodeBase64(base64);
32
32
  return new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } });
33
33
  }