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