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