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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +27 -868
  2. package/build/actions.mjs +27 -15
  3. package/build/analyze.mjs +180 -24
  4. package/build/app-config.mjs +13 -0
  5. package/build/app-static.mjs +34 -12
  6. package/build/app-styles.mjs +79 -0
  7. package/build/app.mjs +122 -46
  8. package/build/bundle-runtime.mjs +22 -9
  9. package/build/cache-components.mjs +1 -1
  10. package/build/cache-handlers.mjs +1 -1
  11. package/build/compact-css.mjs +53 -0
  12. package/build/compilation-cache.mjs +269 -0
  13. package/build/compilation-store.mjs +142 -0
  14. package/build/compiler.mjs +34 -10
  15. package/build/config.mjs +34 -3
  16. package/build/create.mjs +239 -0
  17. package/build/css-modules.mjs +56 -12
  18. package/build/deployment-assets.mjs +89 -0
  19. package/build/deployment-config.mjs +18 -0
  20. package/build/dev-css-assets.mjs +173 -0
  21. package/build/dev-css-update.mjs +18 -14
  22. package/build/dev-dependencies.mjs +1 -1
  23. package/build/dev-discovery.mjs +3 -3
  24. package/build/dev-lazy-build.mjs +77 -0
  25. package/build/dev.mjs +15 -8
  26. package/build/dynamic.mjs +14 -7
  27. package/build/edge.mjs +3 -3
  28. package/build/fonts.mjs +1 -1
  29. package/build/framework-imports.mjs +15 -5
  30. package/build/html-only-config.mjs +53 -0
  31. package/build/html-only-graph.mjs +67 -0
  32. package/build/incremental.mjs +112 -18
  33. package/build/index.mjs +90 -28
  34. package/build/instrumentation.mjs +30 -0
  35. package/build/lazy-dynamic-imports.mjs +137 -0
  36. package/build/metadata-routes.mjs +1 -1
  37. package/build/migrate.mjs +4 -4
  38. package/build/migration-audit.mjs +19 -31
  39. package/build/migration-imports.mjs +245 -0
  40. package/build/migration-scenarios-runner.mjs +73 -0
  41. package/build/migration-scenarios.mjs +72 -0
  42. package/build/native-discovery.mjs +65 -5
  43. package/build/native-packages.mjs +2 -0
  44. package/build/native-type-config.mjs +37 -0
  45. package/build/npm-packages.mjs +1 -0
  46. package/build/persistent-cache.mjs +18 -3
  47. package/build/precompress.mjs +1 -1
  48. package/build/prerender-cache.mjs +70 -0
  49. package/build/prerender-config.mjs +29 -0
  50. package/build/prerender-pages.mjs +19 -9
  51. package/build/prerender-pool.mjs +165 -0
  52. package/build/prerender-process-worker.mjs +58 -0
  53. package/build/prerender-worker.mjs +5 -5
  54. package/build/production-builder.mjs +59 -10
  55. package/build/react-compiler-report.mjs +225 -0
  56. package/build/react-compiler.mjs +139 -0
  57. package/build/runtime-contract.mjs +2 -2
  58. package/build/standalone.mjs +21 -3
  59. package/build/static-export.mjs +4 -4
  60. package/build/styles.mjs +6 -4
  61. package/build/transform.mjs +1 -1
  62. package/build/typed-routes.mjs +4 -0
  63. package/build/typescript-dev.mjs +35 -8
  64. package/build/typescript-native-worker.mjs +55 -0
  65. package/build/typescript-native.mjs +18 -0
  66. package/build/typescript-toolchain.mjs +19 -0
  67. package/build/typescript.mjs +6 -4
  68. package/build/webpack-graph.mjs +19 -6
  69. package/build/worker-memory.mjs +9 -0
  70. package/cli.mjs +70 -5
  71. package/compat/activity-cache.cjs +26 -0
  72. package/compat/after.cjs +6 -4
  73. package/compat/app-dynamic.cjs +12 -1
  74. package/compat/app-layout-context.cjs +52 -2
  75. package/compat/base64.cjs +10 -0
  76. package/compat/cache-handlers.cjs +15 -3
  77. package/compat/cache-wire.cjs +28 -0
  78. package/compat/cache.cjs +2 -1
  79. package/compat/cache.d.cts +4 -0
  80. package/compat/cookies.cjs +21 -5
  81. package/compat/data-cache.cjs +12 -8
  82. package/compat/flight-delta.cjs +152 -0
  83. package/compat/headers.cjs +4 -2
  84. package/compat/incremental-cache.cjs +20 -7
  85. package/compat/index.d.ts +69 -4
  86. package/compat/link.cjs +13 -10
  87. package/compat/navigation-authorization.cjs +9 -0
  88. package/compat/navigation-stages.cjs +19 -0
  89. package/compat/offline-store.cjs +11 -0
  90. package/compat/offline.cjs +5 -0
  91. package/compat/offline.d.cts +2 -0
  92. package/compat/redis-cache.cjs +201 -0
  93. package/compat/redis-cache.d.cts +37 -0
  94. package/compat/server-primitives.cjs +31 -11
  95. package/compat/server.d.cts +15 -2
  96. package/compat/static-generation.cjs +7 -0
  97. package/compat/use-cache.cjs +3 -0
  98. package/docs/ai/compatibility.md +248 -2
  99. package/docs/ai/data.md +165 -5
  100. package/docs/ai/deployment.md +158 -0
  101. package/docs/ai/quickstart.md +29 -4
  102. package/docs/ai/routing.md +93 -0
  103. package/docs/ai/troubleshooting.md +116 -2
  104. package/package.json +40 -9
  105. package/runtime/action-client.mjs +10 -4
  106. package/runtime/action-server.mjs +2 -0
  107. package/runtime/app-client.mjs +316 -92
  108. package/runtime/app-content.mjs +4 -3
  109. package/runtime/app-partial-client.mjs +3 -0
  110. package/runtime/app-partial.mjs +1 -1
  111. package/runtime/app-prefetch-constants.mjs +4 -0
  112. package/runtime/app-prefetch.mjs +2 -3
  113. package/runtime/app-render-key.mjs +5 -0
  114. package/runtime/app-render.mjs +88 -28
  115. package/runtime/app-routing.mjs +22 -9
  116. package/runtime/app-static.mjs +18 -8
  117. package/runtime/app-stream-html.mjs +10 -53
  118. package/runtime/app-style-selection.mjs +34 -0
  119. package/runtime/app-styles.mjs +154 -0
  120. package/runtime/atomic-file.mjs +27 -0
  121. package/runtime/cache-components-rsc.mjs +7 -2
  122. package/runtime/cache-request.mjs +2 -1
  123. package/runtime/deployment-client.mjs +19 -0
  124. package/runtime/dev-client.mjs +13 -3
  125. package/runtime/dev-compiler-broker.mjs +42 -0
  126. package/runtime/dev-file-changes.mjs +123 -8
  127. package/runtime/dev-indicator.mjs +8 -3
  128. package/runtime/dev-lazy-imports.mjs +121 -0
  129. package/runtime/dev-lazy-staging.mjs +14 -0
  130. package/runtime/dev-lazy-tool.mjs +16 -0
  131. package/runtime/dev-style-retention.mjs +15 -0
  132. package/runtime/development.mjs +83 -25
  133. package/runtime/document.mjs +1 -1
  134. package/runtime/edge.mjs +6 -5
  135. package/runtime/fetch-cache.mjs +10 -8
  136. package/runtime/flight-delta.mjs +74 -0
  137. package/runtime/flight-tap.mjs +28 -0
  138. package/runtime/fs.mjs +15 -8
  139. package/runtime/html-only.mjs +27 -0
  140. package/runtime/http.mjs +2 -0
  141. package/runtime/incremental-cache.mjs +25 -4
  142. package/runtime/instrumentation.mjs +49 -0
  143. package/runtime/lazy-import-client.mjs +38 -0
  144. package/runtime/navigation-recovery.mjs +36 -0
  145. package/runtime/navigation.mjs +7 -0
  146. package/runtime/node-html-stream.mjs +47 -0
  147. package/runtime/pages-client.mjs +14 -7
  148. package/runtime/pages-navigation.mjs +48 -63
  149. package/runtime/prefetch-scheduler.mjs +110 -0
  150. package/runtime/profile.mjs +14 -0
  151. package/runtime/profiles.json +2 -2
  152. package/runtime/render.mjs +3 -0
  153. package/runtime/route-pattern.mjs +44 -0
  154. package/runtime/rsc-worker.mjs +34 -12
  155. package/runtime/trace.mjs +78 -0
  156. package/runtime/watch-project.mjs +19 -15
  157. package/runtime/worker.mjs +70 -26
  158. package/vendor/nft/LICENSE +7 -0
  159. package/vendor/nft/README.md +16 -0
  160. package/vendor/nft/out/analyze.d.ts +8 -0
  161. package/vendor/nft/out/analyze.js +1216 -0
  162. package/vendor/nft/out/analyze.js.map +1 -0
  163. package/vendor/nft/out/cli.d.ts +2 -0
  164. package/vendor/nft/out/cli.js +94 -0
  165. package/vendor/nft/out/cli.js.map +1 -0
  166. package/vendor/nft/out/fs.d.ts +22 -0
  167. package/vendor/nft/out/fs.js +106 -0
  168. package/vendor/nft/out/fs.js.map +1 -0
  169. package/vendor/nft/out/index.d.ts +4 -0
  170. package/vendor/nft/out/index.js +25 -0
  171. package/vendor/nft/out/index.js.map +1 -0
  172. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  173. package/vendor/nft/out/node-file-trace.js +380 -0
  174. package/vendor/nft/out/node-file-trace.js.map +1 -0
  175. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  176. package/vendor/nft/out/resolve-dependency.js +427 -0
  177. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  178. package/vendor/nft/out/types.d.ts +66 -0
  179. package/vendor/nft/out/types.js +2 -0
  180. package/vendor/nft/out/types.js.map +1 -0
  181. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  182. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  183. package/vendor/nft/out/utils/binary-locators.js +88 -0
  184. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  185. package/vendor/nft/out/utils/get-package-base.js +30 -0
  186. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  187. package/vendor/nft/out/utils/interop-require.js +24 -0
  188. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  189. package/vendor/nft/out/utils/node-version.js +10 -0
  190. package/vendor/nft/out/utils/node-version.js.map +1 -0
  191. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  192. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  193. package/vendor/nft/out/utils/special-cases.js +347 -0
  194. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  195. package/vendor/nft/out/utils/static-eval.js +579 -0
  196. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  197. package/vendor/nft/out/utils/types.js +2 -0
  198. package/vendor/nft/out/utils/types.js.map +1 -0
  199. package/vendor/nft/out/utils/wrappers.js +659 -0
  200. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  201. package/vendor/nft/package.json +4 -0
  202. package/vendor/nft/upstream.json +51 -0
@@ -1,7 +1,7 @@
1
+ import { deploymentHeaders, assertDeploymentResponse } from './deployment-client.mjs';
1
2
  import React, { startTransition, useLayoutEffect } from 'react';
2
3
  import { preload } from 'react-dom';
3
- import { partialModel } from './app-partial-model.mjs';
4
- import { PARTIAL_PREFETCH_TYPE, PARTIAL_PREFETCH_CACHE_BYTES, readPartialPrefetch, partialFlightStream } from './app-prefetch.mjs';
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';
7
7
  import { AppRouterProvider } from '../compat/app-context.cjs';
@@ -13,6 +13,14 @@ import { flightResponseURL, publicNavigationURL, isApplicationURL } from './clie
13
13
  import { normalizeBasePath, removeBasePath } from '../compat/paths.cjs';
14
14
  import GlobalErrorBoundary, { DefaultGlobalError, GlobalErrorTrigger } from '../compat/app-global-error-boundary.cjs';
15
15
  import { loadBeforeInteractive } from '../compat/script-loader.cjs';
16
+ import { createPrefetchScheduler, prefetchCacheVictim } from './prefetch-scheduler.mjs';
17
+ import { appStyleManager } from './app-styles.mjs';
18
+ import { retainAppStyles } from './app-style-selection.mjs';
19
+ import { isHtmlDocumentRoute } from './route-pattern.mjs';
20
+ import {recoverNavigation} from './navigation-recovery.mjs';
21
+ import { createActivityCache } from '../compat/activity-cache.cjs';
22
+ import { createFlightTransport, TYPE as FLIGHT_DELTA_TYPE } from '../compat/flight-delta.cjs';
23
+ import {decodeBase64} from '../compat/base64.cjs';
16
24
 
17
25
  const PREFETCH_LIMIT = 8;
18
26
  const PREFETCH_LIFETIME = 30_000;
@@ -31,7 +39,9 @@ function loadClientModule(source) {
31
39
  // supplies ESM import functions so React and application contexts stay shared.
32
40
  export function installFlightModuleLoader(clientModules) {
33
41
  const loaded = new Map();
42
+ const loadedVersions = new Map();
34
43
  const pending = new Map();
44
+ let version = 0;
35
45
  const previousRequire = globalThis.__webpack_require__;
36
46
  const previousLoad = globalThis.__webpack_chunk_load__;
37
47
  globalThis.__webpack_require__ = id => {
@@ -44,32 +54,42 @@ export function installFlightModuleLoader(clientModules) {
44
54
  return typeof source === 'string' ? source : source?.browserModule || String(id);
45
55
  };
46
56
  globalThis.__webpack_get_script_filename__ = id => globalThis.__webpack_require__.u(id);
47
- globalThis.__webpack_chunk_load__ = id => {
48
- if (loaded.has(id)) return Promise.resolve();
57
+ function loadChunk(id) {
49
58
  if (pending.has(id)) return pending.get(id);
59
+ if (loaded.has(id) && loadedVersions.get(id) === version) return Promise.resolve();
50
60
  if (!Object.hasOwn(clientModules, id)) {
51
61
  if (previousLoad) return previousLoad(id);
52
62
  return Promise.reject(new Error(`Unknown PRNext client module ${id}`));
53
63
  }
54
64
  const source = clientModules[id];
65
+ const requestedVersion = version;
66
+ const current = () => {
67
+ if (!Object.hasOwn(clientModules, id)) throw new Error(`Unknown PRNext client module ${id}`);
68
+ return loadChunk(id);
69
+ };
55
70
  const promise = Promise.resolve().then(() => loadClientModule(source))
56
- .then(module => { loaded.set(id, module); pending.delete(id); }, error => {
57
- pending.delete(id);
71
+ .then(module => {
72
+ if (requestedVersion !== version) return current();
73
+ loaded.set(id, module); loadedVersions.set(id, version);
74
+ }, error => {
75
+ if (requestedVersion !== version) return current();
76
+ loaded.delete(id); loadedVersions.delete(id);
58
77
  throw error;
78
+ }).finally(() => {
79
+ if (pending.get(id) === promise) pending.delete(id);
59
80
  });
60
81
  pending.set(id, promise);
61
82
  return promise;
62
- };
83
+ }
84
+ globalThis.__webpack_chunk_load__ = loadChunk;
63
85
  return {
64
86
  async updateModules(nextModules) {
65
- const ids = [...loaded.keys()].filter(id => Object.hasOwn(nextModules, id));
87
+ const ids = [...new Set([...loaded.keys(), ...pending.keys()])].filter(id => Object.hasOwn(nextModules, id));
88
+ version++;
66
89
  clientModules = nextModules;
67
- await Promise.all(ids.map(async id => {
68
- const source = clientModules[id];
69
- const module = await loadClientModule(source);
70
- loaded.set(id, module);
71
- }));
72
- for (const id of loaded.keys()) if (!Object.hasOwn(nextModules, id)) loaded.delete(id);
90
+ pending.clear();
91
+ for (const id of loaded.keys()) if (!Object.hasOwn(nextModules, id)) { loaded.delete(id); loadedVersions.delete(id); }
92
+ await Promise.all(ids.map(loadChunk));
73
93
  },
74
94
  };
75
95
  }
@@ -88,8 +108,7 @@ function embeddedFlight() {
88
108
  document.removeEventListener('DOMContentLoaded', onComplete);
89
109
  controller.close();
90
110
  } else {
91
- const binary = atob(value);
92
- controller.enqueue(Uint8Array.from(binary, character => character.charCodeAt(0)));
111
+ controller.enqueue(decodeBase64(value));
93
112
  }
94
113
  };
95
114
  onComplete = () => {
@@ -108,28 +127,27 @@ function embeddedFlight() {
108
127
  }
109
128
  const element = document.getElementById('__PRNEXT_FLIGHT__');
110
129
  if (!element) throw new Error('PRNext App Router Flight payload is missing');
111
- const binary = atob(element.textContent.trim());
112
- const bytes = Uint8Array.from(binary, character => character.charCodeAt(0));
130
+ const bytes = decodeBase64(element.textContent.trim());
113
131
  element.remove();
114
132
  return new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } });
115
133
  }
116
134
 
117
135
  function NormalStyles({ css, fonts }) {
118
- const [styles, setStyles] = React.useState([]);
119
136
  useLayoutEffect(() => {
120
137
  for (const font of fonts || []) preload(font.href, { as: 'font', type: font.type, crossOrigin: 'anonymous' });
121
- // Fatal global renders never commit this branch. Wait for a successful
122
- // normal/local-error mount, then restore sheets omitted from its empty SSR
123
- // document. Existing plain SSR links are already loaded and need no copy.
124
- const loaded = new Set([...document.querySelectorAll('link[rel="stylesheet"]')].map(node => node.href));
125
- const missing = (css || []).filter(href => !loaded.has(new URL(href, window.location.href).href));
126
- if (missing.length) setStyles(current => [...new Set([...current, ...missing])]);
138
+ const styles = appStyleManager(document);
139
+ styles.commit(css || []);
140
+ return () => styles.release();
127
141
  }, [css, fonts]);
128
- return styles.map(href => React.createElement('link', { key: href, href, rel: 'stylesheet', precedence: 'prnext-app' }));
142
+ return null;
129
143
  }
130
144
 
131
- function AppRoot({ model, controller, committed, url, globalErrorComponent, globalErrorCss }) {
145
+ function AppRoot({ model, controller, committed, url, globalErrorComponent, globalErrorCss, retentionEpoch, retentionAuthorizationEpoch }) {
132
146
  const [hydrated, setHydrated] = React.useState(false);
147
+ const cache = React.useRef(null);
148
+ if (!cache.current) cache.current = createActivityCache({ limit: controller._navigationCache?.maxEntries, ttl: controller._navigationCache?.ttlMs });
149
+ const activityCache = React.useMemo(() => ({ cache: cache.current, epoch: retentionEpoch, authorizationEpoch: retentionAuthorizationEpoch,
150
+ authorizationKey: model.authorizationKey, strictAuthorization: controller._navigationCache?.authCookies !== undefined }), [retentionEpoch, retentionAuthorizationEpoch, model.authorizationKey]);
133
151
  const previewDynamic = React.useCallback(() => { throw model.partialPreview.suspended; }, [model.partialPreview]);
134
152
  useLayoutEffect(() => {
135
153
  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' }));
@@ -145,10 +163,11 @@ function AppRoot({ model, controller, committed, url, globalErrorComponent, glob
145
163
  return React.createElement(AppRouterProvider, { router, controller, ...(model.partialPreview ? { prerenderSearch: previewDynamic, ...(model.partialPreview.unknownParams ? { prerenderParams: previewDynamic } : {}) } : {}) },
146
164
  React.createElement(GlobalErrorBoundary, { component: globalErrorComponent, css: globalErrorCss, resetKey: model },
147
165
  model.globalError ? React.createElement(GlobalErrorTrigger, { error: model.globalError })
148
- : appContent(model.tree, { layoutCache: model.layoutCache || null, styles: React.createElement(NormalStyles, { css: model.css, fonts: model.fonts }) })));
166
+ : appContent(model.tree, { layoutCache: model.layoutCache || null, activityCache, styles: React.createElement(NormalStyles, { css: model.css, fonts: model.fonts }) })));
149
167
  }
150
168
 
151
169
  function prepareLayoutCache(model, previous) {
170
+ retainAppStyles(model, previous);
152
171
  if (!model.routing || model.layoutCache) return model;
153
172
  const active = new Set(Object.keys(model.routing.slots));
154
173
  model.layoutCache = {
@@ -166,7 +185,7 @@ function AppView(props) {
166
185
  }
167
186
 
168
187
  function appView(props) {
169
- return React.createElement(strictMode ? React.StrictMode : React.Fragment, null, React.createElement(AppView, props));
188
+ return React.createElement(strictMode ? React.StrictMode : React.Fragment, null, React.createElement(AppView, { ...props, retentionEpoch: props.controller?._retentionEpoch || 0, retentionAuthorizationEpoch: props.controller?._authorizationEpoch || 0 }));
170
189
  }
171
190
 
172
191
  function historyState(key, scroll, routing) {
@@ -179,7 +198,7 @@ function scrollToTarget(url, saved) {
179
198
  if (url.hash) {
180
199
  let name = url.hash.slice(1);
181
200
  try { name = decodeURIComponent(name); } catch { /* A literal malformed escape can be an element ID. */ }
182
- const anchor = document.getElementById(name) || document.getElementsByName(name)[0];
201
+ const anchor = [...document.querySelectorAll('[id], [name]')].find(element => (element.id === name || element.getAttribute('name') === name) && element.getClientRects().length);
183
202
  if (anchor) { anchor.scrollIntoView(); return; }
184
203
  }
185
204
  window.scrollTo(0, 0);
@@ -191,7 +210,7 @@ function assertModel(model) {
191
210
  return model;
192
211
  }
193
212
 
194
- async function startApp({ clientModules = {}, basePath = '', trailingSlash = false, skipTrailingSlashRedirect = false, globalErrorId, globalErrorCss = [], strictMode: enabledStrictMode = true, cacheComponents = false } = {}) {
213
+ async function startApp({ deploymentId, htmlOnlyRoutes, clientModules = {}, basePath = '', trailingSlash = false, skipTrailingSlashRedirect = false, globalErrorId, globalErrorCss = [], strictMode: enabledStrictMode = true, cacheComponents = false, partialPrefetching = false, navigationCache } = {}) {
195
214
  strictMode = enabledStrictMode;
196
215
  await loadBeforeInteractive();
197
216
  basePath = normalizeBasePath(basePath);
@@ -201,8 +220,18 @@ async function startApp({ clientModules = {}, basePath = '', trailingSlash = fal
201
220
  await globalThis.__webpack_chunk_load__(globalErrorId);
202
221
  return { default: globalThis.__webpack_require__(globalErrorId).default };
203
222
  }) : DefaultGlobalError;
223
+ // The production browser graph fixes this feature at build time. The
224
+ // fallback also keeps direct, unbundled runtime consumers functional.
225
+ const partialRuntime = typeof __PRNEXT_CACHE_COMPONENTS__ === 'undefined'
226
+ ? cacheComponents ? import('./app-partial-client.mjs') : null
227
+ : __PRNEXT_CACHE_COMPONENTS__ ? import('./app-partial-client.mjs') : null;
204
228
  // The Flight adapter reads __webpack_require__.u during module evaluation.
205
- const flightClient = await import('react-server-dom-webpack/client.browser');
229
+ // Both imports start concurrently and receive rejection handlers in this
230
+ // turn. A rejected PPR chunk must not wait unhandled for a slow Flight chunk.
231
+ const [flightClient, partial] = await Promise.all([import('react-server-dom-webpack/client.browser'), partialRuntime]);
232
+ // Ordinary App routes do not need the resumable PPR model. Keep it out of
233
+ // their initial module graph; Cache Components still loads it before use.
234
+ const { partialModel, readPartialPrefetch, partialFlightStream } = partial || {};
206
235
  const { createFromFetch, createFromReadableStream, createServerReference, createTemporaryReferenceSet, encodeReply } = flightClient.default || flightClient;
207
236
  let root;
208
237
  let actionQueue = Promise.resolve();
@@ -221,61 +250,111 @@ async function startApp({ clientModules = {}, basePath = '', trailingSlash = fal
221
250
  if (initialRewrite) initialModel.rewrite = initialRewrite;
222
251
  let currentUrl = new URL(window.location.href);
223
252
  let currentRouting = initialModel.routing;
253
+ // A pending destination may already own the history entry while React still
254
+ // shows the previous view. Rolling back its render must not rewrite that
255
+ // entry with the previous view's routing state on the next scroll event.
256
+ let historyRouting = currentRouting;
224
257
  let currentModel = initialModel;
225
- const historyModels = new Map();
258
+ let committedView = { model: initialModel, routing: currentRouting, url: currentUrl.href };
259
+ let pendingView;
260
+ const historyFocus = new Map();
226
261
  let currentKey = window.history.state?.__prnext?.key || 0;
227
262
  let nextKey = currentKey + 1;
228
263
  let navigation = 0;
229
- let activeRequest;
230
- const prefetches = new Map();
231
- const partialSegments = new Map();
264
+ let activeRequest, finishNavigation, finishCommit, pageHidden = false, notifyingPrefetch = false;
265
+ const prefetches = new Map(), scheduler = createPrefetchScheduler();
266
+ const partialSegments = (typeof __PRNEXT_CACHE_COMPONENTS__ === 'undefined' ? cacheComponents : __PRNEXT_CACHE_COMPONENTS__) ? new Map() : null;
232
267
  let segmentBytes = 0;
233
268
 
234
- if (currentRouting) historyModels.set(currentKey, initialModel);
235
269
  window.history.replaceState(historyState(currentKey, [window.scrollX, window.scrollY], currentRouting), '', currentUrl.href);
236
270
  window.history.scrollRestoration = 'manual';
237
271
 
238
272
  function saveScroll() {
239
273
  // popstate has already changed location; never overwrite its destination.
240
274
  if (window.location.href === currentUrl.href) {
241
- window.history.replaceState(historyState(currentKey, [window.scrollX, window.scrollY], currentRouting), '', currentUrl.href);
275
+ window.history.replaceState(historyState(currentKey, [window.scrollX, window.scrollY], historyRouting), '', currentUrl.href);
276
+ if (typeof WeakRef !== 'undefined' && document.activeElement && document.activeElement !== document.body) historyFocus.set(currentKey, new WeakRef(document.activeElement));
277
+ while (historyFocus.size > 16) historyFocus.delete(historyFocus.keys().next().value);
242
278
  }
243
279
  }
244
280
 
245
- async function requestFlight(url, signal, routing = currentRouting, onPartial, {skipEnsure=false}={}) {
281
+ function renderView(model, url, { token = navigation, canonical = true, committed } = {}) {
282
+ const view = { model, token };
283
+ pendingView = view;
284
+ startTransition(() => root.render(appView({ model, controller, url, globalErrorComponent, globalErrorCss,
285
+ committed: () => {
286
+ 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 };
291
+ committed?.();
292
+ },
293
+ })));
294
+ }
295
+
296
+ function abandonPendingView() {
297
+ if (!pendingView) return;
298
+ pendingView = undefined;
299
+ currentModel = committedView.model;
300
+ currentRouting = committedView.routing;
301
+ // React can already be reading a lazy row after createFromFetch resolves
302
+ // the root model. Replace that suspended render before abort rejects its
303
+ // 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 }));
305
+ }
306
+
307
+ const flightTransport = createFlightTransport();
308
+ async function requestFlight(url, signal, routing = currentRouting, onPartial, {skipEnsure=false,fullFlight=false}={}) {
246
309
  if(process.env.NODE_ENV==='development'&&!routing?.refresh&&!skipEnsure)await globalThis.__PRNEXT_DEV__?.ensureRoute?.(url.href);
247
310
  const exported = window.__PRNEXT_STATIC_EXPORT__ === true;
248
- const target = exported ? new URL(basePath + '/_prnext/flight' + (url.pathname.slice(basePath.length).replace(/\/$/, '') || '') + '/index.txt', url) : url;
311
+ const base = !exported && !fullFlight ? flightTransport.snapshot() : undefined;
312
+ const target = exported ? new URL(basePath + '/_prnext/flight' + (deploymentId ? '/' + encodeURIComponent(deploymentId) : '') + (url.pathname.slice(basePath.length).replace(/\/$/, '') || '') + '/index.txt', url) : url;
249
313
  const response = await fetch(target.href, {
250
- headers: { RSC: '1', Accept: 'text/x-component', ...(routing ? { 'x-prnext-router-state': encodeURIComponent(JSON.stringify(routing)) } : {}) },
314
+ headers: { ...deploymentHeaders(deploymentId), RSC: '1', Accept: 'text/x-component', ...(!exported&&!fullFlight?{'x-prnext-flight-accept':'v1',...(base?{'x-prnext-flight-base':base.token}:{})}:{}), ...(routing ? { 'x-prnext-router-state': encodeURIComponent(JSON.stringify(routing)) } : {}) },
251
315
  credentials: 'same-origin',
252
316
  signal,
253
317
  });
318
+ if (!exported) await assertDeploymentResponse(response, deploymentId);
254
319
  let destination;
255
320
  try { destination = exported ? url : flightResponseURL(response, url); }
256
321
  catch (error) { await response.body?.cancel().catch(() => {}); throw error; }
257
- if (exported ? !response.ok : !response.headers.get('content-type')?.toLowerCase().startsWith('text/x-component')) {
322
+ const delta = response.headers.get('content-type')?.startsWith(FLIGHT_DELTA_TYPE);
323
+ if (exported ? !response.ok : !(delta||response.headers.get('content-type')?.toLowerCase().startsWith('text/x-component'))) {
258
324
  await response.body?.cancel();
259
325
  throw new Error('The navigation destination requires a document request');
260
326
  }
261
- onPartial?.(response, destination);
262
- const model = assertModel(await createFromFetch(Promise.resolve(response), flightOptions));
327
+ await onPartial?.(response, destination);
328
+ let model;
329
+ try { model = assertModel(await createFromFetch(Promise.resolve(exported?response:await flightTransport.response(response,base)), flightOptions)); }
330
+ catch(error) {
331
+ if(!delta||fullFlight||signal?.aborted)throw error;
332
+ flightTransport.clear();
333
+ return requestFlight(url,signal,routing,onPartial,{skipEnsure:true,fullFlight:true});
334
+ }
263
335
  model.router.basePath = basePath;
264
336
  const rewrite = readRewriteHeader(response);
265
337
  if (rewrite) model.rewrite = rewrite;
266
338
  return { model, url: destination };
267
339
  }
268
340
 
269
- async function requestPartial(url, signal) {
341
+ const navigateFlight=(url,signal,...args)=>recoverNavigation(recoverySignal=>requestFlight(url,recoverySignal,...args),{signal});
342
+
343
+ const requestPartial = (typeof __PRNEXT_CACHE_COMPONENTS__ === 'undefined' ? cacheComponents : __PRNEXT_CACHE_COMPONENTS__) ? async function requestPartial(url, signal, full = false) {
270
344
  const response = await fetch(url.href, { credentials: 'same-origin', signal,
271
- headers: { RSC: '1', Accept: PARTIAL_PREFETCH_TYPE, 'x-prnext-prefetch': '1',
345
+ headers: { ...deploymentHeaders(deploymentId), RSC: '1', Accept: PARTIAL_PREFETCH_TYPE, 'x-prnext-prefetch': '1',
346
+ ...(partialPrefetching ? { 'x-prnext-prefetch-stage': full ? 'prefetch' : 'shell' } : {}),
272
347
  ...(partialSegments.size ? { 'x-prnext-prefetch-known': [...partialSegments.keys()].join(',') } : {}) } });
273
- const destination = flightResponseURL(response, url);
348
+ await assertDeploymentResponse(response, deploymentId);
349
+ let destination;
350
+ try { destination = flightResponseURL(response, url); }
351
+ catch (error) { await response.body?.cancel().catch(() => {}); throw error; }
274
352
  if (!response.headers.get('content-type')?.startsWith(PARTIAL_PREFETCH_TYPE)) {
275
353
  await response.body?.cancel();
276
354
  return null;
277
355
  }
278
356
  const value = await readPartialPrefetch(response);
357
+ if (signal.aborted) return null;
279
358
  let segment = partialSegments.get(value.id);
280
359
  if (!segment && value.flight) {
281
360
  const bytes = value.flight.length;
@@ -294,11 +373,13 @@ async function startApp({ clientModules = {}, basePath = '', trailingSlash = fal
294
373
  // No visitor data is decoded here. Explicit postponed errors become pending
295
374
  // boundaries until the navigation's independently authorized Flight arrives.
296
375
  const suspended = new Promise(() => {});
297
- const model = partialModel(assertModel(await segment.model), { keyMap: new Map(value.keys), suspended });
376
+ const decoded = assertModel(await segment.model);
377
+ if (signal.aborted) return null;
378
+ const model = partialModel(decoded, { keyMap: new Map(value.keys), suspended });
298
379
  model.router = { ...value.router, basePath };
299
380
  model.partialPreview = { unknownParams: value.unknownParams, suspended };
300
381
  return { model, id: value.id, url: destination, partial: true };
301
- }
382
+ } : null;
302
383
 
303
384
  function dropPrefetch(key, invalidate = false) {
304
385
  const entry = prefetches.get(key);
@@ -307,21 +388,36 @@ async function startApp({ clientModules = {}, basePath = '', trailingSlash = fal
307
388
  clearTimeout(entry.timer);
308
389
  if (invalidate) {
309
390
  entry.abort.abort();
310
- entry.onInvalidate?.();
391
+ const callbacks = entry.invalidations; entry.invalidations = undefined;
392
+ // Callbacks may rearm prefetch. Deliver after cache/queue mutations so a
393
+ // replacement cannot be evicted inside the same invalidation transaction.
394
+ if (callbacks?.size) queueMicrotask(() => {
395
+ for (const callback of callbacks) {
396
+ notifyingPrefetch = true;
397
+ try { callback(); } catch (error) { console.error(error); }
398
+ finally { notifyingPrefetch = false; }
399
+ }
400
+ });
311
401
  }
312
402
  }
313
403
 
314
404
  function invalidatePrefetches() {
315
- partialSegments.clear(); segmentBytes = 0;
316
- for (const key of prefetches.keys()) dropPrefetch(key, true);
405
+ if (typeof __PRNEXT_CACHE_COMPONENTS__ === 'undefined' ? cacheComponents : __PRNEXT_CACHE_COMPONENTS__) { partialSegments.clear(); segmentBytes = 0; }
406
+ for (const [key, entry] of [...prefetches]) if (prefetches.get(key) === entry) dropPrefetch(key, true);
317
407
  }
318
408
 
319
409
  function callServer(id, args) {
320
410
  if (!root) return Promise.reject(new Error('Server Actions cannot run during initial rendering'));
321
411
  const token = ++navigation;
412
+ finishCommit?.();
322
413
  const target = new URL(window.location.href);
414
+ saveScroll();
415
+ abandonPendingView();
323
416
  activeRequest?.abort();
417
+ appStyleManager(document).cancel();
418
+ finishNavigation?.(); finishNavigation = undefined;
324
419
  activeRequest = undefined;
420
+ purgeRetainedPages();
325
421
  // Mutations are dispatched in order, including Set-Cookie effects required
326
422
  // by a subsequent action. Capture their route priority when invoked: a
327
423
  // queued action must not cancel a newer user navigation when it starts.
@@ -331,12 +427,30 @@ async function startApp({ clientModules = {}, basePath = '', trailingSlash = fal
331
427
  }
332
428
 
333
429
  async function dispatchAction(id, args, token, target) {
430
+ const release = scheduler.navigation();
431
+ try { return await performAction(id, args, token, target); } finally { release(); }
432
+ }
433
+
434
+ async function waitForDevelopmentUpdate() {
435
+ // 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.
440
+ let development = globalThis.__PRNEXT_DEV__;
441
+ while (development?.applying && development.applied) {
442
+ await development.applied;
443
+ development = globalThis.__PRNEXT_DEV__;
444
+ }
445
+ }
446
+
447
+ async function performAction(id, args, token, target) {
334
448
  invalidatePrefetches();
335
449
  const temporaryReferences = createTemporaryReferenceSet();
336
450
  const body = await encodeReply(args, { temporaryReferences });
337
451
  const response = await fetch(target.href, {
338
452
  method: 'POST',
339
- headers: { 'Next-Action': id, RSC: '1', Accept: 'text/x-component', ...(currentRouting ? { 'x-prnext-router-state': encodeURIComponent(JSON.stringify({ ...currentRouting, refresh: true })) } : {}) },
453
+ headers: { ...deploymentHeaders(deploymentId), 'Next-Action': id, RSC: '1', Accept: 'text/x-component', ...(currentRouting ? { 'x-prnext-router-state': encodeURIComponent(JSON.stringify({ ...currentRouting, refresh: true })) } : {}) },
340
454
  credentials: 'same-origin',
341
455
  // RPC redirects arrive as Flight data. Never replay a mutation through an
342
456
  // unexpected HTTP 307/308 or redirect an authenticated POST off origin.
@@ -344,19 +458,30 @@ async function startApp({ clientModules = {}, basePath = '', trailingSlash = fal
344
458
  body,
345
459
  });
346
460
  invalidatePrefetches();
461
+ await assertDeploymentResponse(response, deploymentId);
347
462
  if (!response.headers.get('content-type')?.toLowerCase().startsWith('text/x-component')) {
348
463
  await response.body?.cancel();
349
464
  throw new Error(`Server Action request failed (${response.status})`);
350
465
  }
351
466
  const model = await createFromFetch(Promise.resolve(response), { ...flightOptions, temporaryReferences });
467
+ const outcome = () => {
468
+ if (model?.actionError) {
469
+ const error = new Error(model.actionError.message || 'The Server Action failed');
470
+ if (model.actionError.digest !== undefined) error.digest = model.actionError.digest;
471
+ throw error;
472
+ }
473
+ return model?.actionResult;
474
+ };
475
+ if (process.env.NODE_ENV === 'development') await waitForDevelopmentUpdate();
476
+ controller._retentionEpoch++;
352
477
  if (model?.routingInvalid || (model?.rootLayout && currentModel.rootLayout && model.rootLayout !== currentModel.rootLayout)) {
353
478
  if (token === navigation) window.location.replace(target.href);
354
- return model.actionResult;
479
+ return outcome();
355
480
  }
356
481
  if (model?.actionRedirect) {
357
482
  const destination = navigationUrl(model.actionRedirect.url);
358
483
  if (token === navigation) await navigate(destination, { replace: model.actionRedirect.type === 'replace' });
359
- return model.actionResult;
484
+ return outcome();
360
485
  }
361
486
  if (model && Object.hasOwn(model, 'tree') && token === navigation) {
362
487
  assertModel(model);
@@ -364,37 +489,44 @@ async function startApp({ clientModules = {}, basePath = '', trailingSlash = fal
364
489
  const rewrite = readRewriteHeader(response);
365
490
  if (rewrite) model.rewrite = rewrite;
366
491
  prepareLayoutCache(model, currentModel);
492
+ try { await appStyleManager(document).prepare(model.globalError ? [] : model.css || []); }
493
+ catch (error) { if (token !== navigation) return outcome(); throw error; }
494
+ if (process.env.NODE_ENV === 'development') await waitForDevelopmentUpdate();
495
+ if (token !== navigation) return outcome();
367
496
  currentRouting = model.routing;
368
497
  currentModel = model;
369
- if (currentRouting) historyModels.set(currentKey, model);
370
- startTransition(() => root.render(appView({ model, controller, url: target.href, globalErrorComponent, globalErrorCss })));
371
- }
372
- if (model?.actionError) {
373
- const error = new Error(model.actionError.message || 'The Server Action failed');
374
- if (model.actionError.digest !== undefined) error.digest = model.actionError.digest;
375
- throw error;
498
+ historyRouting = currentRouting;
499
+ appStyleManager(document).handoff(model.globalError ? [] : model.css || []);
500
+ renderView(model, target.href, { token });
376
501
  }
502
+ if (model?.actionError) return outcome();
377
503
  // Flight's JSON reviver removes object fields encoded as $undefined. A
378
504
  // successful void action therefore has an updated tree but no own result.
379
505
  if (!model || typeof model !== 'object' || (!Object.hasOwn(model, 'actionResult') && !Object.hasOwn(model, 'tree'))) {
380
506
  throw new Error('The Server Action response did not contain a result');
381
507
  }
382
- return model.actionResult;
508
+ return outcome();
383
509
  }
384
510
 
385
511
  async function navigate(href, { replace = false, scroll = true, pop = false, savedScroll, savedRouting, refresh = false } = {}) {
512
+ if (pageHidden) return;
386
513
  const target = navigationUrl(href);
387
- if (!isApplicationURL(target, window.location.origin, basePath)) {
514
+ if (!isApplicationURL(target, window.location.origin, basePath) || isHtmlDocumentRoute(htmlOnlyRoutes, removeBasePath(target.pathname, basePath))) {
388
515
  if (replace) window.location.replace(target.href);
389
516
  else window.location.assign(target.href);
390
517
  return;
391
518
  }
392
519
  const token = ++navigation;
520
+ finishCommit?.();
521
+ saveScroll();
522
+ abandonPendingView();
393
523
  activeRequest?.abort();
524
+ appStyleManager(document).cancel();
525
+ finishNavigation?.(); finishNavigation = undefined;
394
526
  activeRequest = undefined;
395
- saveScroll();
396
527
 
397
528
  function updateHistory(destination) {
529
+ historyRouting = currentRouting;
398
530
  if (!pop && !refresh) {
399
531
  if (!replace) currentKey = nextKey++;
400
532
  const position = scroll ? [0, 0] : [window.scrollX, window.scrollY];
@@ -408,38 +540,52 @@ async function startApp({ clientModules = {}, basePath = '', trailingSlash = fal
408
540
  currentUrl = destination;
409
541
  }
410
542
 
411
- if (!refresh && target.pathname === currentUrl.pathname && target.search === currentUrl.search && target.hash !== currentUrl.hash) {
543
+ const committedUrl = new URL(committedView.url);
544
+ if (!refresh && target.pathname === currentUrl.pathname && target.search === currentUrl.search && target.hash !== currentUrl.hash &&
545
+ target.pathname === committedUrl.pathname && target.search === committedUrl.search) {
412
546
  updateHistory(target);
413
547
  if (scroll) scrollToTarget(target, savedScroll);
414
548
  return;
415
549
  }
416
550
 
551
+ const cacheKey = `${target.origin}${target.pathname}${target.search}`;
552
+ const candidate = !refresh && prefetches.get(cacheKey);
553
+ const prefetched = candidate && !candidate.abort.signal.aborted ? candidate : undefined;
554
+ if (candidate && !prefetched) dropPrefetch(cacheKey, true);
555
+ const releaseNavigation = scheduler.navigation(prefetched?.task);
556
+ finishNavigation = releaseNavigation;
417
557
  try {
418
- const cacheKey = `${target.origin}${target.pathname}${target.search}`;
419
- const prefetched = !refresh && prefetches.get(cacheKey);
420
558
  let result;
421
559
  let historyUpdated = false;
422
- const historyModel = pop && savedRouting && historyModels.get(window.history.state?.__prnext?.key);
423
- if (historyModel) result = { model: historyModel, url: target };
424
- else if (prefetched) {
560
+ // Always authorize history navigation again. Mounted branches can retain
561
+ // UI state while fresh Flight verifies cookies, middleware and rewrites.
562
+ if (prefetched) {
425
563
  activeRequest = prefetched.abort;
426
564
  dropPrefetch(cacheKey);
427
- result = await prefetched.promise;
565
+ try { result = await prefetched.promise; }
566
+ catch (error) { if (token !== navigation || (prefetched.abort.signal.aborted && !prefetched.failed)) throw error; result = null; }
428
567
  if (result && !result.partial) {
429
- result = { ...result, url: new URL(result.url.href) };
430
- if (!result.url.hash) result.url.hash = target.hash;
568
+ // Full prefetch warms modules/data, not a future authorization decision.
569
+ // Static Flight has no authorizationKey, but its proxy checks can still
570
+ // change when an HttpOnly cookie is revoked before this click.
571
+ const abort = new AbortController(); activeRequest = abort;
572
+ result = await navigateFlight(target, abort.signal, pop && savedRouting ? { ...savedRouting, restore: true } : currentRouting);
431
573
  } else {
432
574
  const preview = result;
433
575
  const abort = new AbortController();
434
576
  activeRequest = abort;
435
- result = await requestFlight(target, abort.signal, currentRouting, (response, destination) => {
577
+ result = await navigateFlight(target, abort.signal, currentRouting, async (response, destination) => {
436
578
  if (!preview || token !== navigation || response.headers.get('x-prnext-ppr-id') !== preview.id ||
437
579
  (preview.model.rootLayout && currentModel.rootLayout && preview.model.rootLayout !== currentModel.rootLayout)) return;
438
580
  // Auth, middleware and rewrites ran again. A changed target or a
439
581
  // redirect cannot reveal an obsolete protected prefetched segment.
440
582
  if (destination.pathname !== preview.url.pathname || destination.search !== preview.url.search) return;
583
+ prepareLayoutCache(preview.model, currentModel);
584
+ await appStyleManager(document).prepare(preview.model.css || [], abort.signal);
585
+ if (token !== navigation || abort.signal.aborted) return;
441
586
  updateHistory(destination); historyUpdated = true;
442
- startTransition(() => root.render(appView({ model: preview.model, controller, url: destination.href, globalErrorComponent, globalErrorCss })));
587
+ appStyleManager(document).handoff(preview.model.css || []);
588
+ renderView(preview.model, destination.href, { token, canonical: false });
443
589
  });
444
590
  }
445
591
  } else {
@@ -448,54 +594,107 @@ async function startApp({ clientModules = {}, basePath = '', trailingSlash = fal
448
594
  // A refresh is already inside the compiler's apply transaction. Calling
449
595
  // ensureRoute from here can wait for that same transaction forever when
450
596
  // this tree has no routing state (the ordinary single-route case).
451
- result = await requestFlight(target, abort.signal, pop && savedRouting ? { ...savedRouting, restore: true } : refresh && currentRouting ? { ...currentRouting, refresh: true } : currentRouting, undefined, {skipEnsure:refresh});
597
+ result = await navigateFlight(target, abort.signal, pop && savedRouting ? { ...savedRouting, restore: true } : refresh && currentRouting ? { ...currentRouting, refresh: true } : currentRouting, undefined, {skipEnsure:refresh});
452
598
  }
453
599
  if (token !== navigation) return;
454
600
  if (!isApplicationURL(result.url, window.location.origin, basePath)) throw new Error('The navigation destination requires a document request');
455
601
  if (result.model.rootLayout && currentModel.rootLayout && result.model.rootLayout !== currentModel.rootLayout) throw new Error('A different root layout requires a document request');
456
602
  prepareLayoutCache(result.model, currentModel);
603
+ await appStyleManager(document).prepare(result.model.globalError ? [] : result.model.css || [], activeRequest?.signal);
604
+ if (token !== navigation) return;
457
605
  currentRouting = result.model.routing;
458
606
  currentModel = result.model;
607
+ historyRouting = currentRouting;
459
608
  if (currentRouting) invalidatePrefetches();
460
609
  if (!historyUpdated) updateHistory(result.url);
461
- if (currentRouting) historyModels.set(currentKey, currentModel);
462
- while (historyModels.size > 16) historyModels.delete(historyModels.keys().next().value);
463
610
  const committed = () => {
464
- if (token === navigation && scroll && !refresh) scrollToTarget(result.url, savedScroll);
611
+ if (token === navigation && scroll && !refresh) {
612
+ scrollToTarget(result.url, savedScroll);
613
+ const focus = pop && historyFocus.get(currentKey)?.deref();
614
+ if (focus?.isConnected && focus.getClientRects().length) focus.focus({ preventScroll: true });
615
+ }
465
616
  };
466
- 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}))));
467
- else startTransition(() => root.render(appView({ model: result.model, controller, committed, url: result.url.href, globalErrorComponent, globalErrorCss })));
617
+ appStyleManager(document).handoff(result.model.globalError ? [] : result.model.css || []);
618
+ if(process.env.NODE_ENV==='development'&&refresh)await new Promise(resolve=>{
619
+ const finish=()=>{if(finishCommit===finish)finishCommit=undefined;resolve();};
620
+ finishCommit=finish;
621
+ renderView(result.model, result.url.href, { token, committed: () => { committed(); finish(); } });
622
+ });
623
+ else renderView(result.model, result.url.href, { token, committed });
468
624
  } catch (error) {
469
625
  if (token !== navigation || error?.name === 'AbortError') return;
626
+ if(error?.code==='PRNEXT_NAVIGATION_NETWORK'){
627
+ window.dispatchEvent(new CustomEvent('prnext:navigation-error',{detail:{message:error.message}}));
628
+ return;
629
+ }
470
630
  // Pages Router routes, public files, and unavailable Flight endpoints use
471
631
  // a normal document navigation, including the browser's redirect handling.
472
632
  if (replace || pop || refresh) window.location.replace(target.href);
473
633
  else window.location.assign(target.href);
634
+ } finally {
635
+ if (finishNavigation === releaseNavigation) finishNavigation = undefined;
636
+ releaseNavigation();
474
637
  }
475
638
  }
476
639
 
640
+ function purgeRetainedPages() {
641
+ saveScroll();
642
+ abandonPendingView();
643
+ controller._retentionEpoch++;
644
+ historyFocus.clear();
645
+ // This only updates retention for the already committed model. It must
646
+ // 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 })));
648
+ }
649
+
477
650
  const controller = {
478
- partialPrefetch: cacheComponents,
651
+ _retentionEpoch: 0,
652
+ _authorizationEpoch: 0,
653
+ _navigationCache: navigationCache,
654
+ partialPrefetch: (typeof __PRNEXT_CACHE_COMPONENTS__ === 'undefined' ? cacheComponents : __PRNEXT_CACHE_COMPONENTS__),
479
655
  push: (href, options) => { void navigate(navigationUrl(href), options); },
480
656
  replace: (href, options) => { void navigate(navigationUrl(href), { ...options, replace: true }); },
481
657
  refresh: () => {
482
658
  invalidatePrefetches();
659
+ purgeRetainedPages();
483
660
  void navigate(new URL(window.location.href), { refresh: true });
484
661
  },
485
662
  back: () => { saveScroll(); window.history.back(); },
486
663
  forward: () => { saveScroll(); window.history.forward(); },
487
664
  prefetch(href, options = {}) {
665
+ if (pageHidden) return;
488
666
  const target = navigationUrl(href);
489
- if (!isApplicationURL(target, window.location.origin, basePath)) return;
667
+ if (!isApplicationURL(target, window.location.origin, basePath) || isHtmlDocumentRoute(htmlOnlyRoutes, removeBasePath(target.pathname, basePath))) return;
490
668
  target.hash = '';
491
669
  const key = target.href;
492
- if (prefetches.has(key)) return;
493
- while (prefetches.size >= PREFETCH_LIMIT) dropPrefetch(prefetches.keys().next().value, true);
670
+ const priority = options.priority === 'viewport' ? 0 : 1;
671
+ const previous = prefetches.get(key);
672
+ if (previous && !previous.abort.signal.aborted && (!options.full || previous.full)) {
673
+ previous.priority = Math.max(previous.priority, priority);
674
+ previous.task.promote(options.priority);
675
+ if (typeof options.onInvalidate === 'function') previous.invalidations.add(options.onInvalidate);
676
+ return;
677
+ }
678
+ if (previous) dropPrefetch(key, true);
679
+ // Self-rearming callbacks may fill free slots, but cannot keep evicting
680
+ // each other forever when more warm routes compete for a bounded cache.
681
+ if (notifyingPrefetch && prefetches.size >= PREFETCH_LIMIT) return;
682
+ const victim = prefetches.size >= PREFETCH_LIMIT ? prefetchCacheVictim(prefetches, priority) : undefined;
683
+ if (prefetches.size >= PREFETCH_LIMIT && victim === undefined) return;
494
684
  const abort = new AbortController();
495
- const entry = { abort, onInvalidate: options.onInvalidate };
496
- entry.promise = cacheComponents && !window.__PRNEXT_STATIC_EXPORT__ ? requestPartial(target, abort.signal) : requestFlight(target, abort.signal);
685
+ const entry = { abort, priority, full: options.full === true, invalidations: new Set(typeof options.onInvalidate === 'function' ? [options.onInvalidate] : []) };
686
+ entry.task = scheduler.schedule(() => (typeof __PRNEXT_CACHE_COMPONENTS__ === 'undefined' ? cacheComponents : __PRNEXT_CACHE_COMPONENTS__) && !window.__PRNEXT_STATIC_EXPORT__
687
+ ? options.full && !partialPrefetching ? requestFlight(target, abort.signal) : requestPartial(target, abort.signal, options.full) : requestFlight(target, abort.signal), { abort, priority: options.priority });
688
+ if (!entry.task) return;
689
+ if (victim !== undefined) dropPrefetch(victim, true);
690
+ entry.promise = entry.task.promise;
497
691
  // A failed speculative fetch must never produce an unhandled rejection.
498
- entry.promise.catch(() => { if (prefetches.get(key) === entry) dropPrefetch(key, true); });
692
+ entry.promise.catch(error => {
693
+ // An intentional cancellation is not a cache miss to retry. In
694
+ // particular pagehide must never create a replacement Flight request.
695
+ entry.failed = !abort.signal.aborted && error?.name !== 'AbortError';
696
+ if (prefetches.get(key) === entry) dropPrefetch(key, true);
697
+ });
499
698
  entry.timer = setTimeout(() => dropPrefetch(key, true), PREFETCH_LIFETIME);
500
699
  prefetches.set(key, entry);
501
700
  },
@@ -528,16 +727,36 @@ async function startApp({ clientModules = {}, basePath = '', trailingSlash = fal
528
727
  window.addEventListener('popstate', event => {
529
728
  void navigate(new URL(window.location.href), { pop: true, savedScroll: event.state?.__prnext?.scroll, savedRouting: event.state?.__prnext?.routing });
530
729
  });
531
- window.addEventListener('pagehide', saveScroll);
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(); });
732
+ window.addEventListener('prnext:invalidate-navigation', () => { controller._authorizationEpoch++; controller.refresh(); });
532
733
  let scrollFrame;
533
734
  window.addEventListener('scroll', () => {
534
735
  if (scrollFrame !== undefined) return;
535
736
  scrollFrame = requestAnimationFrame(() => { scrollFrame = undefined; saveScroll(); });
536
737
  }, { passive: true });
738
+ function developmentStyles(model, routes) {
739
+ const route = routes?.find(route => route.pattern === model.styleRoute);
740
+ if (!route) return model;
741
+ const scopes = routes.flatMap(route => route.cssScopes || []);
742
+ const previous = { routing:model.routing, cssScopes:(model.cssScopes || []).map(scope => scopes.find(current => current.path === scope.path && current.kind === scope.kind) || scope) };
743
+ const next = { ...model, css:route.css, cssScopes:route.cssScopes };
744
+ retainAppStyles(next, previous);
745
+ return next;
746
+ }
537
747
  return { root, router: controller, ...(process.env.NODE_ENV === 'development' ? {
538
748
  devReady,
749
+ devStyles: routes => developmentStyles(currentModel, routes).css || [],
750
+ devPrepareStyles: routes => appStyleManager(document).prepare(developmentStyles(currentModel, routes).css || []),
751
+ devStylesApplied: (routes, { commit = true } = {}) => {
752
+ for (const model of new Set([currentModel, committedView.model, pendingView?.model].filter(Boolean))) {
753
+ const next = developmentStyles(model, routes);
754
+ model.css = next.css; model.cssScopes = next.cssScopes;
755
+ }
756
+ if (commit) appStyleManager(document).commit(currentModel.globalError ? [] : currentModel.css || []);
757
+ },
539
758
  devPrepare: options => moduleLoader.updateModules(options.clientModules),
540
- devCommit: () => {invalidatePrefetches();return navigate(new URL(window.location.href),{refresh:true});},
759
+ devCommit: () => {invalidatePrefetches();purgeRetainedPages();return navigate(new URL(window.location.href),{refresh:true});},
541
760
  } : {}) };
542
761
  }
543
762
 
@@ -545,6 +764,11 @@ export function bootstrapApp(options) {
545
764
  if (process.env.NODE_ENV === 'development' && options.dev && globalThis.__PRNEXT_DEV__) {
546
765
  return globalThis.__PRNEXT_DEV__.bootstrap('app', options, () => startApp(options));
547
766
  }
548
- application ||= startApp(options);
767
+ if (!application) {
768
+ application = startApp(options);
769
+ // Generated entries intentionally do not await startup. Report a failed
770
+ // chunk once, while retaining the original rejection for explicit callers.
771
+ void application.catch(error => console.error('[PRNext] App startup failed:', error));
772
+ }
549
773
  return application;
550
774
  }