@wular/pnext 0.1.0 → 0.1.1
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/package.json
CHANGED
package/reference/navigation.md
CHANGED
|
@@ -45,11 +45,11 @@ Prefetch warms the target page and its assets. Set the mode per link with the `p
|
|
|
45
45
|
|
|
46
46
|
The app-wide default can be set with the `prefetch` field in `pnext.config.ts`, described in [Config](./config.md).
|
|
47
47
|
|
|
48
|
-
Requests use low network priority. At most four run at once, though the hover-intent lane allows up to twelve. The core fallback expiry is five minutes.
|
|
48
|
+
Requests use low network priority. At most four run at once, though the hover-intent lane allows up to twelve. The core fallback expiry is five minutes. Development prefetches like production, except under `compat.next`, which follows Next and does not prefetch in development.
|
|
49
49
|
|
|
50
50
|
## Soft navigation
|
|
51
51
|
|
|
52
|
-
Link clicks and router pushes swap the page in place instead of reloading the document, so shared chunks and CSS are never re-downloaded.
|
|
52
|
+
Link clicks and router pushes swap the page in place instead of reloading the document, so shared chunks and CSS are never re-downloaded. A new page starts at the top, and back and forward stay soft and restore scroll. When a link shows a dynamic page again from the router cache, the page refreshes in the background and the fresh render replaces it in place. Cross-origin targets, non-HTML responses, and fetch failures fall back to a full page load.
|
|
53
53
|
|
|
54
54
|
## Redirects and not found
|
|
55
55
|
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
exportDocumentFetcher,
|
|
22
22
|
loadingShellPredictionPolicy,
|
|
23
|
+
prefetchStaleTimePolicy,
|
|
23
24
|
prefetchStaleTimeMs,
|
|
24
25
|
revalidationPrefetchDelayMs,
|
|
25
26
|
segmentCachePolicy,
|
|
@@ -30,7 +31,9 @@ import {
|
|
|
30
31
|
emitLocationChange,
|
|
31
32
|
emitNavigationCommit,
|
|
32
33
|
emitNavigationStart,
|
|
34
|
+
navigationScrollAction,
|
|
33
35
|
scheduleNavigationScroll,
|
|
36
|
+
setNavigationScrollAction,
|
|
34
37
|
withSilentLocationChange,
|
|
35
38
|
} from './events'
|
|
36
39
|
import type {
|
|
@@ -39,6 +42,7 @@ import type {
|
|
|
39
42
|
EntryModule,
|
|
40
43
|
LinkPrefetchMode,
|
|
41
44
|
LoadingShellPrediction,
|
|
45
|
+
NavigationScrollAction,
|
|
42
46
|
PrefetchedPage,
|
|
43
47
|
PrefetchOptions,
|
|
44
48
|
SegmentCacheHit,
|
|
@@ -57,14 +61,17 @@ export interface BrowserRouteState {
|
|
|
57
61
|
runtimePrefetch?: boolean
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
const ROUTE_STATE_PREFIX = 'window.__PNEXT_ROUTE__='
|
|
65
|
+
|
|
60
66
|
function documentRouteState(doc: Document): BrowserRouteState | undefined {
|
|
61
|
-
const prefix = 'window.__PNEXT_ROUTE__='
|
|
62
67
|
const source = [...doc.scripts]
|
|
63
68
|
.map(script => script.textContent ?? '')
|
|
64
|
-
.find(text => text.startsWith(
|
|
69
|
+
.find(text => text.startsWith(ROUTE_STATE_PREFIX))
|
|
65
70
|
if (!source) return undefined
|
|
66
71
|
try {
|
|
67
|
-
return JSON.parse(
|
|
72
|
+
return JSON.parse(
|
|
73
|
+
source.slice(ROUTE_STATE_PREFIX.length).replace(/;\s*$/, ''),
|
|
74
|
+
) as BrowserRouteState
|
|
68
75
|
} catch {
|
|
69
76
|
return undefined
|
|
70
77
|
}
|
|
@@ -320,6 +327,8 @@ function materializeStreamedSegments(doc: Document) {
|
|
|
320
327
|
for (const chunk of doc.querySelectorAll<HTMLElement>(
|
|
321
328
|
'div[hidden][data-pnext-stream], template[data-pnext-stream]',
|
|
322
329
|
)) {
|
|
330
|
+
// Its promotion script has nothing left to do, and re-running it would only trip a script CSP.
|
|
331
|
+
if (chunk.nextElementSibling?.tagName === 'SCRIPT') chunk.nextElementSibling.remove()
|
|
323
332
|
const id = chunk.getAttribute('data-pnext-stream')
|
|
324
333
|
const suspense = id
|
|
325
334
|
? doc.querySelector(`pnext-suspense[data-pnext-suspense="${CSS.escape(id)}"]`)
|
|
@@ -1053,6 +1062,11 @@ function swapBody(
|
|
|
1053
1062
|
continue
|
|
1054
1063
|
}
|
|
1055
1064
|
if (node.hasAttribute('data-pnext-dev')) continue
|
|
1065
|
+
// Callers install the route state from the document; re-running it only trips a script CSP.
|
|
1066
|
+
if (node.text.startsWith(ROUTE_STATE_PREFIX)) {
|
|
1067
|
+
fragment.append(document.importNode(node, true))
|
|
1068
|
+
continue
|
|
1069
|
+
}
|
|
1056
1070
|
// Parser-created scripts are inert; rebuild them so props, route
|
|
1057
1071
|
// state, and streamed-chunk patches execute in document order when the
|
|
1058
1072
|
// fragment lands in the live document.
|
|
@@ -1092,7 +1106,7 @@ function swapBody(
|
|
|
1092
1106
|
cursor = next
|
|
1093
1107
|
}
|
|
1094
1108
|
}
|
|
1095
|
-
if (paintHold)
|
|
1109
|
+
if (paintHold) placeNavigationPaintHold(paintHold)
|
|
1096
1110
|
// Track the entry of what is now on screen (dropped when the incoming route
|
|
1097
1111
|
// has none, so a stale entry is never attributed to it).
|
|
1098
1112
|
if (entrySrc) document.documentElement.setAttribute(ENTRY_SCRIPT_ATTRIBUTE, entrySrc)
|
|
@@ -1102,6 +1116,8 @@ function swapBody(
|
|
|
1102
1116
|
/** Keep the last complete screen painted while client roots mount into the committed document. */
|
|
1103
1117
|
type NavigationPaintHold = HTMLElement & {
|
|
1104
1118
|
__pnextObserver?: MutationObserver
|
|
1119
|
+
/** The window scroll when the copy was taken. */
|
|
1120
|
+
__pnextScroll?: [number, number]
|
|
1105
1121
|
}
|
|
1106
1122
|
|
|
1107
1123
|
function removeNavigationPaintHold(hold: NavigationPaintHold) {
|
|
@@ -1124,12 +1140,17 @@ function createNavigationPaintHold(): NavigationPaintHold | null {
|
|
|
1124
1140
|
element.textContent?.trim(),
|
|
1125
1141
|
)
|
|
1126
1142
|
if (roots.length === 0) return null
|
|
1127
|
-
const hold = document.createElement('div')
|
|
1143
|
+
const hold: NavigationPaintHold = document.createElement('div')
|
|
1128
1144
|
hold.setAttribute('data-pnext-navigation-paint-hold', '')
|
|
1129
1145
|
hold.setAttribute('aria-hidden', 'true')
|
|
1130
1146
|
hold.style.cssText =
|
|
1131
1147
|
'position:fixed;inset:0;z-index:2147483646;overflow:hidden;pointer-events:none;background:Canvas;visibility:visible!important'
|
|
1132
|
-
hold.
|
|
1148
|
+
hold.__pnextScroll = [window.scrollX, window.scrollY]
|
|
1149
|
+
// A <body> shell over the whole hold: body styles lay out and paint the copy as the page was.
|
|
1150
|
+
const body = document.body.cloneNode() as HTMLElement
|
|
1151
|
+
body.style.minHeight = '100%'
|
|
1152
|
+
body.append(...roots.map(root => root.cloneNode(true)))
|
|
1153
|
+
hold.append(body)
|
|
1133
1154
|
// The copy is paint-only: route mount scans and id lookups must never treat it as live UI.
|
|
1134
1155
|
for (const node of hold.querySelectorAll('[data-pnext-client]'))
|
|
1135
1156
|
node.removeAttribute('data-pnext-client')
|
|
@@ -1159,19 +1180,25 @@ function attachNavigationPaintHold(
|
|
|
1159
1180
|
// The opaque fixed clone covers the viewport while the committed tree mounts.
|
|
1160
1181
|
// Keep that live tree visible to focus management and selector observers;
|
|
1161
1182
|
// hiding <body> also hides every real destination element from browser gates.
|
|
1162
|
-
|
|
1183
|
+
placeNavigationPaintHold(hold)
|
|
1163
1184
|
hold.__pnextObserver ??= new MutationObserver(() => {
|
|
1164
1185
|
if (sequence !== navigationSequence) {
|
|
1165
1186
|
return removeNavigationPaintHold(hold)
|
|
1166
1187
|
}
|
|
1167
|
-
if (!hold.isConnected)
|
|
1188
|
+
if (!hold.isConnected) placeNavigationPaintHold(hold)
|
|
1168
1189
|
})
|
|
1169
1190
|
hold.__pnextObserver.observe(document.documentElement, { childList: true, subtree: true })
|
|
1170
1191
|
const scrollRoot = hold.querySelector<HTMLElement>('[data-scroll-root]')
|
|
1171
1192
|
if (scrollRoot) scrollRoot.scrollTop = scrollTop
|
|
1172
1193
|
}
|
|
1173
1194
|
|
|
1174
|
-
|
|
1195
|
+
/** Append the hold scrolled as the departing page was; appending resets an element's scroll. */
|
|
1196
|
+
function placeNavigationPaintHold(hold: NavigationPaintHold) {
|
|
1197
|
+
document.body.append(hold)
|
|
1198
|
+
if (hold.__pnextScroll) hold.scrollTo?.(...hold.__pnextScroll)
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
async function releaseNavigationPaintHold(hold: NavigationPaintHold | null, reveal?: () => void) {
|
|
1175
1202
|
if (!hold) return
|
|
1176
1203
|
// mountRoute, client effects and the navigation commit have completed before
|
|
1177
1204
|
// this runs. Keep the departing frame through the next rendering opportunity,
|
|
@@ -1179,6 +1206,7 @@ async function releaseNavigationPaintHold(hold: NavigationPaintHold | null) {
|
|
|
1179
1206
|
if (window.requestAnimationFrame) {
|
|
1180
1207
|
await new Promise<void>(resolve => window.requestAnimationFrame(() => resolve()))
|
|
1181
1208
|
}
|
|
1209
|
+
reveal?.()
|
|
1182
1210
|
removeNavigationPaintHold(hold)
|
|
1183
1211
|
}
|
|
1184
1212
|
|
|
@@ -1510,6 +1538,11 @@ function isDevDocument() {
|
|
|
1510
1538
|
return Boolean(document.querySelector('script[data-pnext-dev]'))
|
|
1511
1539
|
}
|
|
1512
1540
|
|
|
1541
|
+
// Next's dev router neither prefetches nor paints loading states; core dev navigates like production.
|
|
1542
|
+
function nextDevDocument() {
|
|
1543
|
+
return prefetchStaleTimePolicy !== undefined && isDevDocument()
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1513
1546
|
// `replace` is also set for history TRAVERSALS (options.pop): assign() would
|
|
1514
1547
|
// push a new entry over the popped one and truncate the forward stack, so a
|
|
1515
1548
|
// bailout during back/forward must replace the current entry instead.
|
|
@@ -1541,6 +1574,24 @@ function saveScrollPosition() {
|
|
|
1541
1574
|
}
|
|
1542
1575
|
}
|
|
1543
1576
|
|
|
1577
|
+
// Defined by compat.next builds, whose router policies replace core's scroll and refresh ones.
|
|
1578
|
+
declare const __PNEXT_NEXT_ROUTER__: boolean | undefined
|
|
1579
|
+
|
|
1580
|
+
// Core's scroll policy when compat installs none: a new page starts at the top, a traversal
|
|
1581
|
+
// returns to where its entry was left.
|
|
1582
|
+
const coreNavigationScroll: NavigationScrollAction = (url, { pop, scroll }) => {
|
|
1583
|
+
const state = historyState()
|
|
1584
|
+
const [x = 0, y = 0] = pop
|
|
1585
|
+
? (entryScroll.get(state.__pnextEntry) ?? (state.__pnextScroll as number[] | undefined) ?? [])
|
|
1586
|
+
: []
|
|
1587
|
+
const target = !pop && url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1)))
|
|
1588
|
+
if (target) target.scrollIntoView()
|
|
1589
|
+
else if (pop || scroll !== false) window.scrollTo(x, y)
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// Where each entry was left, pops included, which cannot write the state of the entry they leave.
|
|
1593
|
+
const entryScroll = new Map<unknown, number[]>()
|
|
1594
|
+
|
|
1544
1595
|
function storeNavState() {
|
|
1545
1596
|
try {
|
|
1546
1597
|
history.replaceState(
|
|
@@ -2855,9 +2906,7 @@ export function prefetchRoute(
|
|
|
2855
2906
|
href: string,
|
|
2856
2907
|
options: PrefetchOptions = {},
|
|
2857
2908
|
): Promise<PrefetchedPage | null> {
|
|
2858
|
-
|
|
2859
|
-
// would hammer the dev server for little gain. Navigation still fetches.
|
|
2860
|
-
if (isDevDocument()) return Promise.resolve(null)
|
|
2909
|
+
if (nextDevDocument()) return Promise.resolve(null)
|
|
2861
2910
|
if (isBotUserAgent()) return Promise.resolve(null)
|
|
2862
2911
|
// `strict` is handled at the facade, the only entry point that can be handed
|
|
2863
2912
|
// an unparseable href; everything reaching here already resolved once.
|
|
@@ -3997,7 +4046,6 @@ export function showLoadingShell(
|
|
|
3997
4046
|
) {
|
|
3998
4047
|
if (sequence !== navigationSequence) return false
|
|
3999
4048
|
if (typeof DOMParser === 'undefined') return false
|
|
4000
|
-
const paintHold = document.querySelector<HTMLElement>('[data-pnext-navigation-paint-hold]')
|
|
4001
4049
|
const doc = new DOMParser().parseFromString(shellHtml, 'text/html')
|
|
4002
4050
|
materializeClientIslandMarkers(doc)
|
|
4003
4051
|
// A STATIC STAGE owns whatever chunks streamed with it, so resolve them before picking what
|
|
@@ -4085,7 +4133,6 @@ export function showLoadingShell(
|
|
|
4085
4133
|
// useOffline(), say), and an unmounted island keeps its SSR value forever.
|
|
4086
4134
|
// mountRoute is idempotent, so mounting on every paint is safe.
|
|
4087
4135
|
mountPaintedIslands(doc)
|
|
4088
|
-
if (paintHold) document.body.append(paintHold)
|
|
4089
4136
|
return true
|
|
4090
4137
|
}
|
|
4091
4138
|
|
|
@@ -4263,7 +4310,6 @@ function unwrapSuspenseFallbacks(root: ParentNode): void {
|
|
|
4263
4310
|
*/
|
|
4264
4311
|
export function paintStaticStageSubtree(html: string): boolean {
|
|
4265
4312
|
if (typeof DOMParser === 'undefined') return false
|
|
4266
|
-
const paintHold = document.querySelector<HTMLElement>('[data-pnext-navigation-paint-hold]')
|
|
4267
4313
|
const doc = new DOMParser().parseFromString(html, 'text/html')
|
|
4268
4314
|
if (!isPNextDocument(doc)) return false
|
|
4269
4315
|
// Materialize BEFORE comparing structure: the renderer ships the page slot (and
|
|
@@ -4297,7 +4343,6 @@ export function paintStaticStageSubtree(html: string): boolean {
|
|
|
4297
4343
|
// against the tree painted here. `swapBody`'s own body-child reuse still applies.
|
|
4298
4344
|
swapBody(doc)
|
|
4299
4345
|
mountPaintedIslands(doc)
|
|
4300
|
-
if (paintHold) document.body.append(paintHold)
|
|
4301
4346
|
return true
|
|
4302
4347
|
}
|
|
4303
4348
|
|
|
@@ -4425,6 +4470,10 @@ function unsettledPrefetchDeadline(): Promise<null> {
|
|
|
4425
4470
|
return new Promise(resolve => setTimeout(() => resolve(null), UNSETTLED_PREFETCH_WAIT_MS))
|
|
4426
4471
|
}
|
|
4427
4472
|
|
|
4473
|
+
// Kept copies older than REVALIDATE_AFTER_MS that core commits at once, then refreshes in place.
|
|
4474
|
+
const keptCopies = new WeakSet<PrefetchedPage>()
|
|
4475
|
+
const REVALIDATE_AFTER_MS = 2_000
|
|
4476
|
+
|
|
4428
4477
|
async function pageForNavigation(
|
|
4429
4478
|
url: URL,
|
|
4430
4479
|
options: SoftNavigateOptions = {},
|
|
@@ -4491,7 +4540,15 @@ async function pageForNavigation(
|
|
|
4491
4540
|
cached.settled || cached.full
|
|
4492
4541
|
? await cached.page
|
|
4493
4542
|
: await Promise.race([cached.page, unsettledPrefetchDeadline()])
|
|
4494
|
-
if (page && !page.shellOnly)
|
|
4543
|
+
if (page && !page.shellOnly) {
|
|
4544
|
+
if (
|
|
4545
|
+
typeof __PNEXT_NEXT_ROUTER__ === 'undefined' &&
|
|
4546
|
+
!prefetchStaleTimePolicy &&
|
|
4547
|
+
now - cached.time > REVALIDATE_AFTER_MS
|
|
4548
|
+
)
|
|
4549
|
+
keptCopies.add(page)
|
|
4550
|
+
return page
|
|
4551
|
+
}
|
|
4495
4552
|
// Attached to an in-flight (or already settled) SHELL prefetch for this exact target:
|
|
4496
4553
|
// the navigation issued no duplicate fetch, so paint the static stage it landed and let
|
|
4497
4554
|
// the dynamic stage stream in below.
|
|
@@ -4988,6 +5045,8 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
4988
5045
|
// into the live body. Capturing later would save the target's fallback as
|
|
4989
5046
|
// the previous history entry and restore a permanently stuck "Loading...".
|
|
4990
5047
|
if (!options.pop) saveScrollPosition()
|
|
5048
|
+
if (typeof __PNEXT_NEXT_ROUTER__ === 'undefined')
|
|
5049
|
+
entryScroll.set(routerState.renderedEntryId, [window.scrollX, window.scrollY])
|
|
4991
5050
|
if (departingBfcacheId) saveFormState(departingBfcacheId)
|
|
4992
5051
|
// Snapshot the departing page's live island roots NOW, before a loading shell
|
|
4993
5052
|
// can paint over (and detach) them below — they are stashed for back/forward
|
|
@@ -4996,11 +5055,6 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
4996
5055
|
// Snapshot plain root-layout DOM before a cached/streamed shell can detach it. The final swap
|
|
4997
5056
|
// reconciles onto these nodes so server layouts keep the same identity Next's root reconciler does.
|
|
4998
5057
|
const departingReusableBody = reusableBodyChildren()
|
|
4999
|
-
// Capture the complete outgoing screen before any loading/static stage can replace it. The
|
|
5000
|
-
// resolved client-root commit uses this copy only while its first complete paint settles.
|
|
5001
|
-
const paintHoldScrollTop =
|
|
5002
|
-
document.querySelector<HTMLElement>('[data-scroll-root]')?.scrollTop ?? 0
|
|
5003
|
-
const paintHold = createNavigationPaintHold()
|
|
5004
5058
|
// Painting a prefetched loading shell IS a committed navigation, so push the requested URL
|
|
5005
5059
|
// here and let usePathname() and the address bar reflect the destination while the fetch is
|
|
5006
5060
|
// still in flight; the final commit replaces this entry with the resolved one (or, on a
|
|
@@ -5056,19 +5110,14 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
5056
5110
|
// server-side, and re-painting would replace a committed loading state with a second,
|
|
5057
5111
|
// different one. A segment's loading fallback commits ONCE per navigation.
|
|
5058
5112
|
let cachedStagePainted = false
|
|
5059
|
-
const devSoftNavigation =
|
|
5113
|
+
const devSoftNavigation = nextDevDocument()
|
|
5060
5114
|
const onShell =
|
|
5061
5115
|
restorePage || options.pop || refreshLike
|
|
5062
5116
|
? undefined
|
|
5063
5117
|
: (shellHtml: string) => {
|
|
5064
5118
|
if (cachedStagePainted) return
|
|
5065
5119
|
if (devSoftNavigation) return
|
|
5066
|
-
|
|
5067
|
-
// content is otherwise ready. Keep the departing page until that compile-only hole is
|
|
5068
|
-
// resolved; an explicit loading.js boundary remains paintable through the ordinary
|
|
5069
|
-
// loading-boundary path. Production retains streamed application-Suspense behavior.
|
|
5070
|
-
const devNavigation = shellHtml.includes('data-pnext-dev')
|
|
5071
|
-
if (!showLoadingShell(shellHtml, sequence, url, undefined, false, !devNavigation)) return
|
|
5120
|
+
if (!showLoadingShell(shellHtml, sequence, url, undefined, false, true)) return
|
|
5072
5121
|
// A loading boundary is a committed navigation state.
|
|
5073
5122
|
pushOptimisticUrl()
|
|
5074
5123
|
scheduleNavigationScroll(url, options)
|
|
@@ -5396,6 +5445,11 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
5396
5445
|
// page slot is a marker-range proxy); the entry's unmount compares roots by
|
|
5397
5446
|
// identity, so it rides the same set.
|
|
5398
5447
|
if (preservedPage) keptIslands.add(preservedPage.root as unknown as Element)
|
|
5448
|
+
// Copy the screen as it is now, a loading or static stage included, before anything unmounts.
|
|
5449
|
+
// The resolved client-root commit shows this copy only while its first complete paint settles.
|
|
5450
|
+
const paintHoldScrollTop =
|
|
5451
|
+
document.querySelector<HTMLElement>('[data-scroll-root]')?.scrollTop ?? 0
|
|
5452
|
+
const paintHold = createNavigationPaintHold()
|
|
5399
5453
|
// Wake route-keyed layout children while the departing DOM is still attached. Their layout
|
|
5400
5454
|
// cleanups can save shell state before unmount/swap; pop mounts the destination first instead.
|
|
5401
5455
|
window.__PNEXT_ACTIVE_ENTRY__?.unmount?.(keptIslands)
|
|
@@ -5465,7 +5519,8 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
5465
5519
|
// selector waiter can observe the destination immediately after this stack unwinds; pruning in
|
|
5466
5520
|
// the later post-mount phase let it catch the target node between its correct route sheet and a
|
|
5467
5521
|
// stale asynchronous prune from the preceding navigation.
|
|
5468
|
-
|
|
5522
|
+
// Core keeps the departing sheets while the paint hold still shows the departing screen.
|
|
5523
|
+
if (!url.hash && (stylesheetReconciler || !paintHold)) pruneStylesheets(doc)
|
|
5469
5524
|
// The swapped document carries the render's parallel-route state; pin it
|
|
5470
5525
|
// (plus the document itself) to this history entry so back/forward restores
|
|
5471
5526
|
// what was actually shown, without a server round trip.
|
|
@@ -5514,7 +5569,10 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
5514
5569
|
}
|
|
5515
5570
|
}
|
|
5516
5571
|
} finally {
|
|
5517
|
-
await releaseNavigationPaintHold(paintHold)
|
|
5572
|
+
await releaseNavigationPaintHold(paintHold, () => {
|
|
5573
|
+
if (!url.hash && !stylesheetReconciler && sequence === navigationSequence)
|
|
5574
|
+
pruneStylesheets(doc)
|
|
5575
|
+
})
|
|
5518
5576
|
}
|
|
5519
5577
|
if (sequence !== navigationSequence) return
|
|
5520
5578
|
// Mounting/location subscribers run after the initial pre-mount scroll action.
|
|
@@ -5547,6 +5605,23 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
5547
5605
|
if (!options.pop && departingBfcacheId && departingBfcacheId === historyBfcacheId()) {
|
|
5548
5606
|
restoreFormStateWhenMounted(departingBfcacheId, sequence, true)
|
|
5549
5607
|
}
|
|
5608
|
+
if (
|
|
5609
|
+
typeof __PNEXT_NEXT_ROUTER__ === 'undefined' &&
|
|
5610
|
+
keptCopies.has(page) &&
|
|
5611
|
+
!documentStaticHintFromHtml(page.html)?.isStatic
|
|
5612
|
+
)
|
|
5613
|
+
void revalidateCommittedPage(targetUrl, sequence, page)
|
|
5614
|
+
}
|
|
5615
|
+
|
|
5616
|
+
async function revalidateCommittedPage(url: URL, sequence: number, page: PrefetchedPage) {
|
|
5617
|
+
const fresh = await fetchPage(url.href, { fullRender: true }).catch(() => null)
|
|
5618
|
+
if (!fresh?.ok || fresh.html === page.html || sequence !== navigationSequence) return
|
|
5619
|
+
await softNavigate(url.href, {
|
|
5620
|
+
replace: true,
|
|
5621
|
+
scroll: false,
|
|
5622
|
+
refreshLike: true,
|
|
5623
|
+
cachedPage: fresh,
|
|
5624
|
+
})
|
|
5550
5625
|
}
|
|
5551
5626
|
|
|
5552
5627
|
function isBotUserAgent() {
|
|
@@ -6103,5 +6178,7 @@ export function installRouterFull() {
|
|
|
6103
6178
|
)
|
|
6104
6179
|
window.addEventListener('popstate', onPopState)
|
|
6105
6180
|
window.addEventListener('pageshow', onPageShow)
|
|
6181
|
+
if (typeof __PNEXT_NEXT_ROUTER__ === 'undefined' && !navigationScrollAction)
|
|
6182
|
+
setNavigationScrollAction(coreNavigationScroll)
|
|
6106
6183
|
watchEagerPrefetchLinks()
|
|
6107
6184
|
}
|
|
@@ -387,6 +387,8 @@ export function registerBundlerExtensions(config: ResolvedConfig): void {
|
|
|
387
387
|
// the defaults + resolver from every client bundle.
|
|
388
388
|
__PNEXT_IMAGE_CONFIG_INLINE__: JSON.stringify(getImagesConfig()),
|
|
389
389
|
__PNEXT_IMAGE_CONFIG_INLINED__: 'true',
|
|
390
|
+
// Next's router policies replace core's scroll and refresh ones, so client bundles drop those.
|
|
391
|
+
...(nextCompatEnabled(config) ? { __PNEXT_NEXT_ROUTER__: 'true' } : {}),
|
|
390
392
|
// publicRuntimeConfig inlined the same way; omitted entirely when unset so an app that
|
|
391
393
|
// never configures it adds zero bytes (unlike images, there is no default to drop).
|
|
392
394
|
...(Object.keys(publicRuntimeConfig()).length > 0
|
package/src/render/renderer.ts
CHANGED
|
@@ -7001,7 +7001,11 @@ function unwrapUnusedPageSlot(html: string, mode: PageSlotMarkerMode): string {
|
|
|
7001
7001
|
return html.replace(/<!--pnext-page:[^>]*-->/, '<!--pnext-page:-->')
|
|
7002
7002
|
}
|
|
7003
7003
|
const slot = pageSlotSpan(html)
|
|
7004
|
-
|
|
7004
|
+
if (!slot) return html
|
|
7005
|
+
// `keep` leaves the empty anchors a soft navigation grafts a kept layout's next page between.
|
|
7006
|
+
const children =
|
|
7007
|
+
mode === 'keep' ? `<!--pnext-page:-->${slot.children}<!--/pnext-page-->` : slot.children
|
|
7008
|
+
return html.slice(0, slot.start) + children + html.slice(slot.end)
|
|
7005
7009
|
}
|
|
7006
7010
|
|
|
7007
7011
|
function serializeNeutralClientIslands(html: string) {
|