@thomas.f/prnext 0.1.18-alpha → 0.1.20-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 (162) hide show
  1. package/README.md +2 -0
  2. package/build/actions.mjs +26 -14
  3. package/build/app.mjs +90 -101
  4. package/build/build-lock.mjs +2 -71
  5. package/build/build-retention.mjs +119 -0
  6. package/build/bundle-runtime.mjs +9 -8
  7. package/build/cache-command.mjs +31 -0
  8. package/build/check.mjs +41 -3
  9. package/build/compilation-cache.mjs +32 -12
  10. package/build/compilation-storage.mjs +190 -0
  11. package/build/compiler.mjs +8 -7
  12. package/build/config.mjs +25 -2
  13. package/build/create.mjs +1 -1
  14. package/build/css-modules.mjs +4 -0
  15. package/build/dev-action-graph.mjs +21 -0
  16. package/build/dev-css-assets.mjs +173 -0
  17. package/build/dev-css-update.mjs +14 -12
  18. package/build/dev-lazy-build.mjs +77 -0
  19. package/build/dynamic.mjs +13 -6
  20. package/build/font-cache.mjs +9 -6
  21. package/build/font-files.mjs +1 -1
  22. package/build/incremental.mjs +112 -18
  23. package/build/index.mjs +61 -15
  24. package/build/lazy-dynamic-imports.mjs +137 -0
  25. package/build/migrate.mjs +4 -4
  26. package/build/migration-audit.mjs +19 -31
  27. package/build/migration-imports.mjs +245 -0
  28. package/build/migration-scenarios-runner.mjs +73 -0
  29. package/build/migration-scenarios.mjs +72 -0
  30. package/build/native-discovery.mjs +65 -5
  31. package/build/native-packages.mjs +2 -0
  32. package/build/native-ppr.mjs +32 -0
  33. package/build/npm-packages.mjs +1 -0
  34. package/build/og.mjs +121 -1
  35. package/build/parallel-plan.mjs +55 -0
  36. package/build/persistent-cache.mjs +29 -24
  37. package/build/production-builder.mjs +1 -1
  38. package/build/production-check.mjs +34 -0
  39. package/build/react-compiler-report.mjs +31 -8
  40. package/build/react-compiler.mjs +101 -10
  41. package/build/server-config.mjs +35 -0
  42. package/build/source-analysis.mjs +54 -4
  43. package/build/standalone.mjs +21 -3
  44. package/build/static-adapter.mjs +7 -3
  45. package/build/transform.mjs +38 -6
  46. package/build/typescript-dev.mjs +3 -1
  47. package/build/webpack-graph.mjs +13 -3
  48. package/cli.mjs +24 -6
  49. package/compat/app-dynamic.cjs +12 -1
  50. package/compat/app-layout-context.cjs +7 -3
  51. package/compat/cache-handlers.cjs +3 -1
  52. package/compat/flight-delta.cjs +50 -13
  53. package/compat/index.d.ts +24 -1
  54. package/compat/link.cjs +2 -2
  55. package/compat/navigation-authorization.cjs +8 -1
  56. package/docs/ai/compatibility.md +187 -7
  57. package/docs/ai/deployment.md +141 -0
  58. package/docs/ai/quickstart.md +5 -0
  59. package/docs/ai/routing.md +11 -0
  60. package/docs/ai/troubleshooting.md +79 -0
  61. package/package.json +26 -11
  62. package/release/fontkit.mjs +31 -0
  63. package/release/verify.mjs +3 -0
  64. package/runtime/api.mjs +5 -2
  65. package/runtime/app-client.mjs +254 -53
  66. package/runtime/app-partial.mjs +2 -10
  67. package/runtime/app-prefetch.mjs +2 -2
  68. package/runtime/app-render.mjs +11 -7
  69. package/runtime/app-stream-channel.mjs +3 -2
  70. package/runtime/app-styles.mjs +22 -4
  71. package/runtime/atomic-file.mjs +27 -0
  72. package/runtime/cache-components-rsc.mjs +3 -1
  73. package/runtime/compile-cache-retention.mjs +53 -0
  74. package/runtime/compile-cache.mjs +28 -1
  75. package/runtime/dev-client.mjs +13 -5
  76. package/runtime/dev-file-changes.mjs +123 -8
  77. package/runtime/dev-gateway.mjs +3 -2
  78. package/runtime/dev-lazy-imports.mjs +121 -0
  79. package/runtime/dev-lazy-staging.mjs +14 -0
  80. package/runtime/dev-route-selection.mjs +64 -0
  81. package/runtime/dev-runtime-pool.mjs +3 -2
  82. package/runtime/dev-runtime-spare.mjs +1 -1
  83. package/runtime/dev-session-retention.mjs +154 -0
  84. package/runtime/dev-style-retention.mjs +15 -0
  85. package/runtime/development.mjs +86 -26
  86. package/runtime/env.mjs +9 -0
  87. package/runtime/flight-delta.mjs +59 -9
  88. package/runtime/fs.mjs +15 -8
  89. package/runtime/lazy-import-client.mjs +38 -0
  90. package/runtime/native-ppr.mjs +35 -0
  91. package/runtime/og/NOTICE.txt +1 -1
  92. package/runtime/og/index.node.mjs +76 -31216
  93. package/runtime/og/upstream.json +115 -0
  94. package/runtime/persistent/client.mjs +4 -2
  95. package/runtime/persistent/daemon.mjs +3 -11
  96. package/runtime/persistent/health.mjs +58 -0
  97. package/runtime/project-lock.mjs +71 -0
  98. package/runtime/request-transport.mjs +112 -32
  99. package/runtime/storage-path.mjs +36 -0
  100. package/runtime/trace.mjs +44 -4
  101. package/runtime/worker.mjs +9 -4
  102. package/vendor/fontkit/NOTICE.txt +5 -0
  103. package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
  104. package/vendor/fontkit/index.mjs +3 -0
  105. package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
  106. package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
  107. package/vendor/fontkit/licenses/brotli/readme.md +61 -0
  108. package/vendor/fontkit/licenses/clone/LICENSE +18 -0
  109. package/vendor/fontkit/licenses/dfa/README.md +83 -0
  110. package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
  111. package/vendor/fontkit/licenses/fontkit/README.md +266 -0
  112. package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
  113. package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
  114. package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
  115. package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
  116. package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
  117. package/vendor/fontkit/upstream.json +215 -0
  118. package/vendor/nft/LICENSE +7 -0
  119. package/vendor/nft/README.md +16 -0
  120. package/vendor/nft/out/analyze.d.ts +8 -0
  121. package/vendor/nft/out/analyze.js +1216 -0
  122. package/vendor/nft/out/analyze.js.map +1 -0
  123. package/vendor/nft/out/cli.d.ts +2 -0
  124. package/vendor/nft/out/cli.js +94 -0
  125. package/vendor/nft/out/cli.js.map +1 -0
  126. package/vendor/nft/out/fs.d.ts +22 -0
  127. package/vendor/nft/out/fs.js +106 -0
  128. package/vendor/nft/out/fs.js.map +1 -0
  129. package/vendor/nft/out/index.d.ts +4 -0
  130. package/vendor/nft/out/index.js +25 -0
  131. package/vendor/nft/out/index.js.map +1 -0
  132. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  133. package/vendor/nft/out/node-file-trace.js +380 -0
  134. package/vendor/nft/out/node-file-trace.js.map +1 -0
  135. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  136. package/vendor/nft/out/resolve-dependency.js +427 -0
  137. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  138. package/vendor/nft/out/types.d.ts +66 -0
  139. package/vendor/nft/out/types.js +2 -0
  140. package/vendor/nft/out/types.js.map +1 -0
  141. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  142. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  143. package/vendor/nft/out/utils/binary-locators.js +88 -0
  144. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  145. package/vendor/nft/out/utils/get-package-base.js +30 -0
  146. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  147. package/vendor/nft/out/utils/interop-require.js +24 -0
  148. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  149. package/vendor/nft/out/utils/node-version.js +10 -0
  150. package/vendor/nft/out/utils/node-version.js.map +1 -0
  151. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  152. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  153. package/vendor/nft/out/utils/special-cases.js +347 -0
  154. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  155. package/vendor/nft/out/utils/static-eval.js +579 -0
  156. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  157. package/vendor/nft/out/utils/types.js +2 -0
  158. package/vendor/nft/out/utils/types.js.map +1 -0
  159. package/vendor/nft/out/utils/wrappers.js +659 -0
  160. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  161. package/vendor/nft/package.json +4 -0
  162. package/vendor/nft/upstream.json +51 -0
@@ -26,6 +26,7 @@ const PREFETCH_LIMIT = 8;
26
26
  const PREFETCH_LIFETIME = 30_000;
27
27
  let application;
28
28
  let strictMode = true;
29
+ const navigationStreams = new WeakMap();
29
30
 
30
31
  function loadClientModule(source) {
31
32
  if (typeof source === 'function') return source();
@@ -39,7 +40,9 @@ function loadClientModule(source) {
39
40
  // supplies ESM import functions so React and application contexts stay shared.
40
41
  export function installFlightModuleLoader(clientModules) {
41
42
  const loaded = new Map();
43
+ const loadedVersions = new Map();
42
44
  const pending = new Map();
45
+ let version = 0;
43
46
  const previousRequire = globalThis.__webpack_require__;
44
47
  const previousLoad = globalThis.__webpack_chunk_load__;
45
48
  globalThis.__webpack_require__ = id => {
@@ -52,32 +55,42 @@ export function installFlightModuleLoader(clientModules) {
52
55
  return typeof source === 'string' ? source : source?.browserModule || String(id);
53
56
  };
54
57
  globalThis.__webpack_get_script_filename__ = id => globalThis.__webpack_require__.u(id);
55
- globalThis.__webpack_chunk_load__ = id => {
56
- if (loaded.has(id)) return Promise.resolve();
58
+ function loadChunk(id) {
57
59
  if (pending.has(id)) return pending.get(id);
60
+ if (loaded.has(id) && loadedVersions.get(id) === version) return Promise.resolve();
58
61
  if (!Object.hasOwn(clientModules, id)) {
59
62
  if (previousLoad) return previousLoad(id);
60
63
  return Promise.reject(new Error(`Unknown PRNext client module ${id}`));
61
64
  }
62
65
  const source = clientModules[id];
66
+ const requestedVersion = version;
67
+ const current = () => {
68
+ if (!Object.hasOwn(clientModules, id)) throw new Error(`Unknown PRNext client module ${id}`);
69
+ return loadChunk(id);
70
+ };
63
71
  const promise = Promise.resolve().then(() => loadClientModule(source))
64
- .then(module => { loaded.set(id, module); pending.delete(id); }, error => {
65
- pending.delete(id);
72
+ .then(module => {
73
+ if (requestedVersion !== version) return current();
74
+ loaded.set(id, module); loadedVersions.set(id, version);
75
+ }, error => {
76
+ if (requestedVersion !== version) return current();
77
+ loaded.delete(id); loadedVersions.delete(id);
66
78
  throw error;
79
+ }).finally(() => {
80
+ if (pending.get(id) === promise) pending.delete(id);
67
81
  });
68
82
  pending.set(id, promise);
69
83
  return promise;
70
- };
84
+ }
85
+ globalThis.__webpack_chunk_load__ = loadChunk;
71
86
  return {
72
87
  async updateModules(nextModules) {
73
- const ids = [...loaded.keys()].filter(id => Object.hasOwn(nextModules, id));
88
+ const ids = [...new Set([...loaded.keys(), ...pending.keys()])].filter(id => Object.hasOwn(nextModules, id));
89
+ version++;
74
90
  clientModules = nextModules;
75
- await Promise.all(ids.map(async id => {
76
- const source = clientModules[id];
77
- const module = await loadClientModule(source);
78
- loaded.set(id, module);
79
- }));
80
- for (const id of loaded.keys()) if (!Object.hasOwn(nextModules, id)) loaded.delete(id);
91
+ pending.clear();
92
+ for (const id of loaded.keys()) if (!Object.hasOwn(nextModules, id)) { loaded.delete(id); loadedVersions.delete(id); }
93
+ await Promise.all(ids.map(loadChunk));
81
94
  },
82
95
  };
83
96
  }
@@ -132,10 +145,11 @@ function NormalStyles({ css, fonts }) {
132
145
 
133
146
  function AppRoot({ model, controller, committed, url, globalErrorComponent, globalErrorCss, retentionEpoch, retentionAuthorizationEpoch }) {
134
147
  const [hydrated, setHydrated] = React.useState(false);
148
+ const flight = navigationStreams.get(model);
135
149
  const cache = React.useRef(null);
136
150
  if (!cache.current) cache.current = createActivityCache({ limit: controller._navigationCache?.maxEntries, ttl: controller._navigationCache?.ttlMs });
137
151
  const activityCache = React.useMemo(() => ({ cache: cache.current, epoch: retentionEpoch, authorizationEpoch: retentionAuthorizationEpoch,
138
- authorizationKey: model.authorizationKey, strictAuthorization: controller._navigationCache?.authCookies !== undefined }), [retentionEpoch, retentionAuthorizationEpoch, model.authorizationKey]);
152
+ authorizationKey: model.authorizationKey, strictAuthorization: controller._navigationCache?.authCookies !== undefined, flight }), [retentionEpoch, retentionAuthorizationEpoch, model.authorizationKey, flight]);
139
153
  const previewDynamic = React.useCallback(() => { throw model.partialPreview.suspended; }, [model.partialPreview]);
140
154
  useLayoutEffect(() => {
141
155
  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' }));
@@ -238,12 +252,18 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
238
252
  if (initialRewrite) initialModel.rewrite = initialRewrite;
239
253
  let currentUrl = new URL(window.location.href);
240
254
  let currentRouting = initialModel.routing;
255
+ // A pending destination may already own the history entry while React still
256
+ // shows the previous view. Rolling back its render must not rewrite that
257
+ // entry with the previous view's routing state on the next scroll event.
258
+ let historyRouting = currentRouting;
241
259
  let currentModel = initialModel;
260
+ let committedView = { model: initialModel, routing: currentRouting, url: currentUrl.href, retentionEpoch: 0, authorizationEpoch: 0 };
261
+ let pendingView, pendingNavigation;
242
262
  const historyFocus = new Map();
243
263
  let currentKey = window.history.state?.__prnext?.key || 0;
244
264
  let nextKey = currentKey + 1;
245
265
  let navigation = 0;
246
- let activeRequest, finishNavigation, pageHidden = false, notifyingPrefetch = false;
266
+ let activeRequest, finishNavigation, finishCommit, documentTimer, pageHidden = false, notifyingPrefetch = false, documentFallback = false;
247
267
  const prefetches = new Map(), scheduler = createPrefetchScheduler();
248
268
  const partialSegments = (typeof __PRNEXT_CACHE_COMPONENTS__ === 'undefined' ? cacheComponents : __PRNEXT_CACHE_COMPONENTS__) ? new Map() : null;
249
269
  let segmentBytes = 0;
@@ -254,12 +274,102 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
254
274
  function saveScroll() {
255
275
  // popstate has already changed location; never overwrite its destination.
256
276
  if (window.location.href === currentUrl.href) {
257
- window.history.replaceState(historyState(currentKey, [window.scrollX, window.scrollY], currentRouting), '', currentUrl.href);
277
+ window.history.replaceState(historyState(currentKey, [window.scrollX, window.scrollY], historyRouting), '', currentUrl.href);
258
278
  if (typeof WeakRef !== 'undefined' && document.activeElement && document.activeElement !== document.body) historyFocus.set(currentKey, new WeakRef(document.activeElement));
259
279
  while (historyFocus.size > 16) historyFocus.delete(historyFocus.keys().next().value);
260
280
  }
261
281
  }
262
282
 
283
+ function renderView(model, url, { token = navigation, canonical = true, kind = 'navigation', committed } = {}) {
284
+ const flight = navigationStreams.get(model), view = { model, token, kind, committed: false };
285
+ pendingView = view;
286
+ const promote = () => {
287
+ if (token !== navigation || pendingView !== view || !view.committed || flight?.complete === false) return;
288
+ pendingView = undefined;
289
+ if (flight) flight.committed = undefined;
290
+ // A committed loading shell can still contain rows owned by a live
291
+ // fetch. Keep the last fully received view available before aborting it.
292
+ // Authorized PPR previews likewise never become the canonical model.
293
+ if (canonical) committedView = { model, routing: model.routing, url, retentionEpoch: controller._retentionEpoch, authorizationEpoch: controller._authorizationEpoch };
294
+ };
295
+ if (flight) flight.committed = promote;
296
+ startTransition(() => root.render(appView({ model, controller, url, globalErrorComponent, globalErrorCss,
297
+ committed: () => {
298
+ if (token !== navigation || pendingView !== view) return;
299
+ view.committed = true;
300
+ promote();
301
+ committed?.();
302
+ },
303
+ })));
304
+ }
305
+
306
+ function abandonPendingView() {
307
+ if (!pendingView) return true;
308
+ const flight = navigationStreams.get(pendingView.model);
309
+ const strictAuthorization = controller._navigationCache?.authCookies !== undefined;
310
+ if (flight) flight.committed = undefined;
311
+ if (pendingView.kind === 'action' && committedView.authorizationEpoch === controller._authorizationEpoch &&
312
+ (!strictAuthorization || committedView.model.authorizationKey === currentModel.authorizationKey)) {
313
+ // An action's received result can invoke a returned action before React
314
+ // commits its UI. This response is never owned/aborted by activeRequest;
315
+ // adopt its model instead of rolling back the prior mutation's epoch.
316
+ pendingView = undefined;
317
+ committedView = { model: currentModel, routing: currentRouting, url: currentUrl.href, retentionEpoch: controller._retentionEpoch, authorizationEpoch: controller._authorizationEpoch };
318
+ return true;
319
+ }
320
+ // A new authorized shell or explicit invalidation can make the last full
321
+ // model private to an obsolete session. Reauthorize through a document
322
+ // request instead of briefly restoring that model while C is pending.
323
+ if ((strictAuthorization && committedView.model.authorizationKey !== currentModel.authorizationKey) ||
324
+ committedView.retentionEpoch !== controller._retentionEpoch ||
325
+ committedView.authorizationEpoch !== controller._authorizationEpoch) return false;
326
+ pendingView = undefined;
327
+ currentModel = committedView.model;
328
+ currentRouting = committedView.routing;
329
+ // React can already be reading a lazy row after createFromFetch resolves
330
+ // the root model. Replace that suspended render before abort rejects its
331
+ // remaining rows; it must not poison the retained layout or page cache.
332
+ root.render(appView({ model: committedView.model, controller, url: committedView.url, globalErrorComponent, globalErrorCss }));
333
+ return true;
334
+ }
335
+
336
+ function fallbackDocument(url, replace = true, defer = false) {
337
+ // The invalidated document stays blocked until its replacement arrives.
338
+ // Repeated synchronous actions must neither dispatch nor start reloads.
339
+ if (documentFallback) return;
340
+ documentFallback = true;
341
+ navigation++;
342
+ activeRequest?.abort();
343
+ appStyleManager(document).cancel();
344
+ finishNavigation?.(); finishNavigation = undefined;
345
+ invalidatePrefetches();
346
+ flightTransport.clear();
347
+ if (controller._navigationCache?.authCookies !== undefined) document.documentElement.style.visibility = 'hidden';
348
+ const request = () => {
349
+ documentTimer = undefined;
350
+ if (pageHidden) return;
351
+ if (replace) window.location.replace(url);
352
+ else window.location.assign(url);
353
+ };
354
+ // An executed action must settle its promise chain before document
355
+ // replacement suspends the old realm. Its POST guard is already latched.
356
+ if (defer) documentTimer = setTimeout(request, 0);
357
+ else request();
358
+ }
359
+
360
+ function requiresAuthorizedDocument(model) {
361
+ if (controller._navigationCache?.authCookies === undefined) return false;
362
+ const identity = model?.authorizationKey;
363
+ // Activity entries are only part of the retained tree: shared client
364
+ // layouts and server layout-cache props can also belong to the old user.
365
+ // An unidentified static model cannot prove that HttpOnly cookies stayed
366
+ // the same. Cross this boundary before adopting any cached layout state.
367
+ return typeof identity !== 'string' || !identity || identity !== currentModel.authorizationKey ||
368
+ committedView.authorizationEpoch !== controller._authorizationEpoch;
369
+ }
370
+
371
+ const actionReloadError = () => new Error('The session changed while this page was loading. Reload before repeating the action.');
372
+
263
373
  const flightTransport = createFlightTransport();
264
374
  async function requestFlight(url, signal, routing = currentRouting, onPartial, {skipEnsure=false,fullFlight=false}={}) {
265
375
  if(process.env.NODE_ENV==='development'&&!routing?.refresh&&!skipEnsure)await globalThis.__PRNEXT_DEV__?.ensureRoute?.(url.href);
@@ -282,12 +392,14 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
282
392
  }
283
393
  await onPartial?.(response, destination);
284
394
  let model;
285
- try { model = assertModel(await createFromFetch(Promise.resolve(exported?response:await flightTransport.response(response,base)), flightOptions)); }
395
+ const flight = { complete: false };
396
+ try { model = assertModel(await createFromFetch(Promise.resolve(await flightTransport.response(response,base,{onEnd(){flight.complete = true;flight.committed?.();}})), flightOptions)); }
286
397
  catch(error) {
287
398
  if(!delta||fullFlight||signal?.aborted)throw error;
288
399
  flightTransport.clear();
289
400
  return requestFlight(url,signal,routing,onPartial,{skipEnsure:true,fullFlight:true});
290
401
  }
402
+ navigationStreams.set(model, flight);
291
403
  model.router.basePath = basePath;
292
404
  const rewrite = readRewriteHeader(response);
293
405
  if (rewrite) model.rewrite = rewrite;
@@ -364,12 +476,20 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
364
476
 
365
477
  function callServer(id, args) {
366
478
  if (!root) return Promise.reject(new Error('Server Actions cannot run during initial rendering'));
479
+ if (pageHidden || documentFallback) return Promise.reject(actionReloadError());
367
480
  const token = ++navigation;
481
+ finishCommit?.();
368
482
  const target = new URL(window.location.href);
483
+ saveScroll();
484
+ const restored = abandonPendingView();
369
485
  activeRequest?.abort();
370
486
  appStyleManager(document).cancel();
371
487
  finishNavigation?.(); finishNavigation = undefined;
372
488
  activeRequest = undefined;
489
+ if (!restored) {
490
+ fallbackDocument(target.href);
491
+ return Promise.reject(actionReloadError());
492
+ }
373
493
  purgeRetainedPages();
374
494
  // Mutations are dispatched in order, including Set-Cookie effects required
375
495
  // by a subsequent action. Capture their route priority when invoked: a
@@ -380,14 +500,29 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
380
500
  }
381
501
 
382
502
  async function dispatchAction(id, args, token, target) {
503
+ if (pageHidden || documentFallback) throw actionReloadError();
383
504
  const release = scheduler.navigation();
384
505
  try { return await performAction(id, args, token, target); } finally { release(); }
385
506
  }
386
507
 
508
+ async function waitForDevelopmentUpdate() {
509
+ // A generation updates the Flight loader before React Refresh registers
510
+ // the replacement component families. An already-running action may finish
511
+ // in that interval; rendering its model immediately remounts those client
512
+ // components and loses layout state. Wait only on development bookkeeping,
513
+ // never replay the mutation or cancel a newer user navigation.
514
+ let development = globalThis.__PRNEXT_DEV__;
515
+ while (development?.applying && development.applied) {
516
+ await development.applied;
517
+ development = globalThis.__PRNEXT_DEV__;
518
+ }
519
+ }
520
+
387
521
  async function performAction(id, args, token, target) {
388
522
  invalidatePrefetches();
389
523
  const temporaryReferences = createTemporaryReferenceSet();
390
524
  const body = await encodeReply(args, { temporaryReferences });
525
+ if (pageHidden || documentFallback) throw actionReloadError();
391
526
  const response = await fetch(target.href, {
392
527
  method: 'POST',
393
528
  headers: { ...deploymentHeaders(deploymentId), 'Next-Action': id, RSC: '1', Accept: 'text/x-component', ...(currentRouting ? { 'x-prnext-router-state': encodeURIComponent(JSON.stringify({ ...currentRouting, refresh: true })) } : {}) },
@@ -404,15 +539,37 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
404
539
  throw new Error(`Server Action request failed (${response.status})`);
405
540
  }
406
541
  const model = await createFromFetch(Promise.resolve(response), { ...flightOptions, temporaryReferences });
542
+ const outcome = () => {
543
+ if (model?.actionError) {
544
+ const error = new Error(model.actionError.message || 'The Server Action failed');
545
+ if (model.actionError.digest !== undefined) error.digest = model.actionError.digest;
546
+ throw error;
547
+ }
548
+ return model?.actionResult;
549
+ };
550
+ if (process.env.NODE_ENV === 'development') await waitForDevelopmentUpdate();
407
551
  controller._retentionEpoch++;
552
+ const validOutcome = model && typeof model === 'object' && (Object.hasOwn(model, 'tree') || Object.hasOwn(model, 'actionResult') || model.actionError || model.actionRedirect || model.routingInvalid);
553
+ if (!validOutcome) throw new Error('The Server Action response did not contain a result');
554
+ if (Object.hasOwn(model, 'tree') && !model.routingInvalid) assertModel(model);
555
+ if (requiresAuthorizedDocument(model)) {
556
+ // A queued action or newer navigation can supersede the render token,
557
+ // but cannot make an observed account change safe to ignore. Complete
558
+ // this one POST and block the queue before any subsequent mutation.
559
+ const latest = pendingNavigation?.token === navigation ? pendingNavigation : undefined;
560
+ const redirected = token === navigation && model.actionRedirect ? navigationUrl(model.actionRedirect.url) : undefined;
561
+ const destination = latest?.url || redirected?.href || window.location.href;
562
+ fallbackDocument(destination, latest ? latest.replace : redirected ? model.actionRedirect.type === 'replace' : true, true);
563
+ return outcome();
564
+ }
408
565
  if (model?.routingInvalid || (model?.rootLayout && currentModel.rootLayout && model.rootLayout !== currentModel.rootLayout)) {
409
- if (token === navigation) window.location.replace(target.href);
410
- return model.actionResult;
566
+ if (token === navigation) fallbackDocument(target.href);
567
+ return outcome();
411
568
  }
412
569
  if (model?.actionRedirect) {
413
570
  const destination = navigationUrl(model.actionRedirect.url);
414
571
  if (token === navigation) await navigate(destination, { replace: model.actionRedirect.type === 'replace' });
415
- return model.actionResult;
572
+ return outcome();
416
573
  }
417
574
  if (model && Object.hasOwn(model, 'tree') && token === navigation) {
418
575
  assertModel(model);
@@ -421,42 +578,48 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
421
578
  if (rewrite) model.rewrite = rewrite;
422
579
  prepareLayoutCache(model, currentModel);
423
580
  try { await appStyleManager(document).prepare(model.globalError ? [] : model.css || []); }
424
- catch (error) { if (token !== navigation) return model.actionResult; throw error; }
425
- if (token !== navigation) return model.actionResult;
581
+ catch (error) { if (token !== navigation) return outcome(); throw error; }
582
+ if (process.env.NODE_ENV === 'development') await waitForDevelopmentUpdate();
583
+ if (token !== navigation) return outcome();
426
584
  currentRouting = model.routing;
427
585
  currentModel = model;
586
+ historyRouting = currentRouting;
428
587
  appStyleManager(document).handoff(model.globalError ? [] : model.css || []);
429
- startTransition(() => root.render(appView({ model, controller, url: target.href, globalErrorComponent, globalErrorCss })));
430
- }
431
- if (model?.actionError) {
432
- const error = new Error(model.actionError.message || 'The Server Action failed');
433
- if (model.actionError.digest !== undefined) error.digest = model.actionError.digest;
434
- throw error;
588
+ renderView(model, target.href, { token, kind: 'action' });
435
589
  }
590
+ if (model?.actionError) return outcome();
436
591
  // Flight's JSON reviver removes object fields encoded as $undefined. A
437
592
  // successful void action therefore has an updated tree but no own result.
438
593
  if (!model || typeof model !== 'object' || (!Object.hasOwn(model, 'actionResult') && !Object.hasOwn(model, 'tree'))) {
439
594
  throw new Error('The Server Action response did not contain a result');
440
595
  }
441
- return model.actionResult;
596
+ return outcome();
442
597
  }
443
598
 
444
599
  async function navigate(href, { replace = false, scroll = true, pop = false, savedScroll, savedRouting, refresh = false } = {}) {
445
- if (pageHidden) return;
600
+ if (pageHidden || documentFallback) return;
446
601
  const target = navigationUrl(href);
447
602
  if (!isApplicationURL(target, window.location.origin, basePath) || isHtmlDocumentRoute(htmlOnlyRoutes, removeBasePath(target.pathname, basePath))) {
448
- if (replace) window.location.replace(target.href);
449
- else window.location.assign(target.href);
603
+ fallbackDocument(target.href, replace);
450
604
  return;
451
605
  }
452
606
  const token = ++navigation;
607
+ const intent = { token, url: target.href, replace: replace || pop || refresh };
608
+ pendingNavigation = intent;
609
+ finishCommit?.();
610
+ saveScroll();
611
+ const restored = abandonPendingView();
453
612
  activeRequest?.abort();
454
613
  appStyleManager(document).cancel();
455
614
  finishNavigation?.(); finishNavigation = undefined;
456
615
  activeRequest = undefined;
457
- saveScroll();
616
+ if (!restored) {
617
+ fallbackDocument(target.href, replace || pop || refresh);
618
+ return;
619
+ }
458
620
 
459
621
  function updateHistory(destination) {
622
+ historyRouting = currentRouting;
460
623
  if (!pop && !refresh) {
461
624
  if (!replace) currentKey = nextKey++;
462
625
  const position = scroll ? [0, 0] : [window.scrollX, window.scrollY];
@@ -470,9 +633,12 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
470
633
  currentUrl = destination;
471
634
  }
472
635
 
473
- if (!refresh && target.pathname === currentUrl.pathname && target.search === currentUrl.search && target.hash !== currentUrl.hash) {
636
+ const committedUrl = new URL(committedView.url);
637
+ if (!refresh && target.pathname === currentUrl.pathname && target.search === currentUrl.search && target.hash !== currentUrl.hash &&
638
+ target.pathname === committedUrl.pathname && target.search === committedUrl.search) {
474
639
  updateHistory(target);
475
640
  if (scroll) scrollToTarget(target, savedScroll);
641
+ if (pendingNavigation === intent) pendingNavigation = undefined;
476
642
  return;
477
643
  }
478
644
 
@@ -493,20 +659,20 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
493
659
  try { result = await prefetched.promise; }
494
660
  catch (error) { if (token !== navigation || (prefetched.abort.signal.aborted && !prefetched.failed)) throw error; result = null; }
495
661
  if (result && !result.partial) {
496
- if (result.model.authorizationKey) {
497
- // A full private prefetch warmed modules/data, not authorization.
498
- // HttpOnly cookies may have changed since it was produced.
499
- const abort = new AbortController(); activeRequest = abort;
500
- result = await navigateFlight(target, abort.signal, pop && savedRouting ? { ...savedRouting, restore: true } : currentRouting);
501
- } else {
502
- result = { ...result, url: new URL(result.url.href) };
503
- if (!result.url.hash) result.url.hash = target.hash;
504
- }
662
+ // Full prefetch warms modules/data, not a future authorization decision.
663
+ // Static Flight has no authorizationKey, but its proxy checks can still
664
+ // change when an HttpOnly cookie is revoked before this click.
665
+ const abort = new AbortController(); activeRequest = abort;
666
+ result = await navigateFlight(target, abort.signal, pop && savedRouting ? { ...savedRouting, restore: true } : currentRouting);
505
667
  } else {
506
668
  const preview = result;
507
669
  const abort = new AbortController();
508
670
  activeRequest = abort;
509
671
  result = await navigateFlight(target, abort.signal, currentRouting, async (response, destination) => {
672
+ // A cached shell (including any cached authorization key) cannot
673
+ // establish this request's live session identity. Strict account
674
+ // isolation waits for the freshly authorized Flight model instead.
675
+ if (controller._navigationCache?.authCookies !== undefined) return;
510
676
  if (!preview || token !== navigation || response.headers.get('x-prnext-ppr-id') !== preview.id ||
511
677
  (preview.model.rootLayout && currentModel.rootLayout && preview.model.rootLayout !== currentModel.rootLayout)) return;
512
678
  // Auth, middleware and rewrites ran again. A changed target or a
@@ -517,7 +683,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
517
683
  if (token !== navigation || abort.signal.aborted) return;
518
684
  updateHistory(destination); historyUpdated = true;
519
685
  appStyleManager(document).handoff(preview.model.css || []);
520
- startTransition(() => root.render(appView({ model: preview.model, controller, url: destination.href, globalErrorComponent, globalErrorCss })));
686
+ renderView(preview.model, destination.href, { token, canonical: false });
521
687
  });
522
688
  }
523
689
  } else {
@@ -531,11 +697,17 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
531
697
  if (token !== navigation) return;
532
698
  if (!isApplicationURL(result.url, window.location.origin, basePath)) throw new Error('The navigation destination requires a document request');
533
699
  if (result.model.rootLayout && currentModel.rootLayout && result.model.rootLayout !== currentModel.rootLayout) throw new Error('A different root layout requires a document request');
700
+ if (requiresAuthorizedDocument(result.model)) {
701
+ activeRequest?.abort();
702
+ fallbackDocument(result.url.href, replace || pop || refresh);
703
+ return;
704
+ }
534
705
  prepareLayoutCache(result.model, currentModel);
535
706
  await appStyleManager(document).prepare(result.model.globalError ? [] : result.model.css || [], activeRequest?.signal);
536
707
  if (token !== navigation) return;
537
708
  currentRouting = result.model.routing;
538
709
  currentModel = result.model;
710
+ historyRouting = currentRouting;
539
711
  if (currentRouting) invalidatePrefetches();
540
712
  if (!historyUpdated) updateHistory(result.url);
541
713
  const committed = () => {
@@ -546,8 +718,12 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
546
718
  }
547
719
  };
548
720
  appStyleManager(document).handoff(result.model.globalError ? [] : result.model.css || []);
549
- if(process.env.NODE_ENV==='development'&&refresh)await new Promise(resolve=>startTransition(()=>root.render(appView({model:result.model,controller,committed:()=>{committed();resolve();},url:result.url.href,globalErrorComponent,globalErrorCss}))));
550
- else startTransition(() => root.render(appView({ model: result.model, controller, committed, url: result.url.href, globalErrorComponent, globalErrorCss })));
721
+ if(process.env.NODE_ENV==='development'&&refresh)await new Promise(resolve=>{
722
+ const finish=()=>{if(finishCommit===finish)finishCommit=undefined;resolve();};
723
+ finishCommit=finish;
724
+ renderView(result.model, result.url.href, { token, committed: () => { committed(); finish(); } });
725
+ });
726
+ else renderView(result.model, result.url.href, { token, committed });
551
727
  } catch (error) {
552
728
  if (token !== navigation || error?.name === 'AbortError') return;
553
729
  if(error?.code==='PRNEXT_NAVIGATION_NETWORK'){
@@ -556,18 +732,29 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
556
732
  }
557
733
  // Pages Router routes, public files, and unavailable Flight endpoints use
558
734
  // a normal document navigation, including the browser's redirect handling.
559
- if (replace || pop || refresh) window.location.replace(target.href);
560
- else window.location.assign(target.href);
735
+ fallbackDocument(target.href, replace || pop || refresh);
561
736
  } finally {
737
+ if (pendingNavigation === intent) pendingNavigation = undefined;
562
738
  if (finishNavigation === releaseNavigation) finishNavigation = undefined;
563
739
  releaseNavigation();
564
740
  }
565
741
  }
566
742
 
567
743
  function purgeRetainedPages() {
744
+ if (pageHidden || documentFallback) return false;
745
+ saveScroll();
746
+ if (!abandonPendingView()) {
747
+ activeRequest?.abort();
748
+ fallbackDocument(currentUrl.href);
749
+ return false;
750
+ }
568
751
  controller._retentionEpoch++;
569
752
  historyFocus.clear();
570
- startTransition(() => root?.render(appView({ model: currentModel, controller, url: currentUrl.href, globalErrorComponent, globalErrorCss })));
753
+ if (committedView.model === currentModel && committedView.authorizationEpoch === controller._authorizationEpoch) committedView = { ...committedView, retentionEpoch: controller._retentionEpoch };
754
+ // This only updates retention for the already committed model. It must
755
+ // not become a new pending Flight view or a development commit barrier.
756
+ startTransition(() => root.render(appView({ model: currentModel, controller, url: committedView.url, globalErrorComponent, globalErrorCss })));
757
+ return true;
571
758
  }
572
759
 
573
760
  const controller = {
@@ -579,13 +766,13 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
579
766
  replace: (href, options) => { void navigate(navigationUrl(href), { ...options, replace: true }); },
580
767
  refresh: () => {
581
768
  invalidatePrefetches();
582
- purgeRetainedPages();
769
+ if (!purgeRetainedPages()) return;
583
770
  void navigate(new URL(window.location.href), { refresh: true });
584
771
  },
585
772
  back: () => { saveScroll(); window.history.back(); },
586
773
  forward: () => { saveScroll(); window.history.forward(); },
587
774
  prefetch(href, options = {}) {
588
- if (pageHidden) return;
775
+ if (pageHidden || documentFallback) return;
589
776
  const target = navigationUrl(href);
590
777
  if (!isApplicationURL(target, window.location.origin, basePath) || isHtmlDocumentRoute(htmlOnlyRoutes, removeBasePath(target.pathname, basePath))) return;
591
778
  target.hash = '';
@@ -650,8 +837,22 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
650
837
  window.addEventListener('popstate', event => {
651
838
  void navigate(new URL(window.location.href), { pop: true, savedScroll: event.state?.__prnext?.scroll, savedRouting: event.state?.__prnext?.routing });
652
839
  });
653
- window.addEventListener('pagehide', () => { saveScroll(); pageHidden = true; navigation++; activeRequest?.abort(); appStyleManager(document).cancel(); finishNavigation?.(); finishNavigation = undefined; invalidatePrefetches(); });
654
- window.addEventListener('pageshow', event => { pageHidden = false; if (event.persisted) controller.refresh(); });
840
+ window.addEventListener('pagehide', () => {
841
+ saveScroll(); pageHidden = true; navigation++; finishCommit?.(); abandonPendingView();
842
+ clearTimeout(documentTimer); documentTimer = undefined;
843
+ activeRequest?.abort(); appStyleManager(document).cancel(); finishNavigation?.(); finishNavigation = undefined; invalidatePrefetches();
844
+ // A cached document may contain an account's shared layout state. Keep it
845
+ // hidden if restored from BFCache until the next document authorizes it.
846
+ if (controller._navigationCache?.authCookies !== undefined) document.documentElement.style.visibility = 'hidden';
847
+ });
848
+ window.addEventListener('pageshow', event => {
849
+ pageHidden = false;
850
+ if (!event.persisted) return;
851
+ if (controller._navigationCache?.authCookies !== undefined) {
852
+ documentFallback = false;
853
+ fallbackDocument(window.location.href);
854
+ } else controller.refresh();
855
+ });
655
856
  window.addEventListener('prnext:invalidate-navigation', () => { controller._authorizationEpoch++; controller.refresh(); });
656
857
  let scrollFrame;
657
858
  window.addEventListener('scroll', () => {
@@ -672,7 +873,7 @@ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, base
672
873
  devStyles: routes => developmentStyles(currentModel, routes).css || [],
673
874
  devPrepareStyles: routes => appStyleManager(document).prepare(developmentStyles(currentModel, routes).css || []),
674
875
  devStylesApplied: (routes, { commit = true } = {}) => {
675
- for (const model of [currentModel]) {
876
+ for (const model of new Set([currentModel, committedView.model, pendingView?.model].filter(Boolean))) {
676
877
  const next = developmentStyles(model, routes);
677
878
  model.css = next.css; model.cssScopes = next.cssScopes;
678
879
  }
@@ -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
  }