@wular/pnext 0.1.1 → 0.1.2
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
|
@@ -14,6 +14,7 @@ import { resolveImport, workspacePackageRoots } from '../../resolve/imports'
|
|
|
14
14
|
import { findProxyFile, proxyRoutePatterns, type ProxyModule } from '../../routing/proxy'
|
|
15
15
|
import { cacheRoot } from '../boot/named-bin'
|
|
16
16
|
import { startWarmChild, type WarmChild } from './vercel-warm'
|
|
17
|
+
import { escapeRegex } from '../../utils/code'
|
|
17
18
|
import { listFiles, toPosixPath, writeText } from '../../utils/fs'
|
|
18
19
|
import { createVerboseLogger, type VerboseLogger } from '../../utils/verbose'
|
|
19
20
|
import type { BuildManifest, StaticFileMetadata } from '../../types'
|
|
@@ -149,7 +150,13 @@ function shipsFile(pack: PackRules, name: string) {
|
|
|
149
150
|
interface VercelConfig {
|
|
150
151
|
version: 3
|
|
151
152
|
routes?: (
|
|
152
|
-
| {
|
|
153
|
+
| {
|
|
154
|
+
src: string
|
|
155
|
+
dest?: string
|
|
156
|
+
methods?: string[]
|
|
157
|
+
headers?: Record<string, string>
|
|
158
|
+
continue?: boolean
|
|
159
|
+
}
|
|
153
160
|
| { handle: 'filesystem' }
|
|
154
161
|
)[]
|
|
155
162
|
overrides?: Record<string, { path?: string; contentType?: string }>
|
|
@@ -205,15 +212,12 @@ export async function writeVercelOutput(
|
|
|
205
212
|
// CDN serves those bytes itself - without it every stylesheet and chunk fell through to the
|
|
206
213
|
// server function: served, but at function cost with no edge cache.
|
|
207
214
|
...compatStaticRewrite(config, outputPath),
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
src: '^/assets/(chunks|fonts)/.*',
|
|
211
|
-
headers: { 'cache-control': 'public, max-age=31536000, immutable' },
|
|
212
|
-
continue: true,
|
|
213
|
-
},
|
|
215
|
+
// Every content-hashed build asset, exactly the set `pnext start` serves immutable.
|
|
216
|
+
await immutableAssetRoute(),
|
|
214
217
|
// Proxy-matched paths go to the server function before the CDN filesystem
|
|
215
218
|
// check — `pnext start` runs the proxy ahead of static files too.
|
|
216
219
|
...(await proxyRoutes(config)),
|
|
220
|
+
...staticHeaderRoutes(staticFiles),
|
|
217
221
|
{ handle: 'filesystem' },
|
|
218
222
|
{ src: '^/.*$', dest: `/${SERVER_FUNCTION}` },
|
|
219
223
|
]
|
|
@@ -1375,9 +1379,83 @@ async function staticOverrides(
|
|
|
1375
1379
|
return overrides
|
|
1376
1380
|
}
|
|
1377
1381
|
|
|
1382
|
+
async function immutableAssetRoute() {
|
|
1383
|
+
const { immutableAssetPrefixes, immutableCacheControl } = await import('../serve/pipeline')
|
|
1384
|
+
const prefixes = [...new Set(immutableAssetPrefixes())].map(escapeRegex)
|
|
1385
|
+
return {
|
|
1386
|
+
src: `^/(?:${prefixes.join('|')})`,
|
|
1387
|
+
headers: { 'cache-control': immutableCacheControl },
|
|
1388
|
+
continue: true,
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Headers a CDN route cannot replay; a file that sets one stays on the server function.
|
|
1393
|
+
const functionOnlyHeaders = new Set([
|
|
1394
|
+
'content-length',
|
|
1395
|
+
'content-encoding',
|
|
1396
|
+
'transfer-encoding',
|
|
1397
|
+
'connection',
|
|
1398
|
+
'set-cookie',
|
|
1399
|
+
])
|
|
1400
|
+
|
|
1378
1401
|
function canServeStaticOnVercel(metadata: StaticFileMetadata) {
|
|
1402
|
+
if (metadata.status !== 200) return false
|
|
1403
|
+
const headers = routeHeaders(metadata)
|
|
1404
|
+
if (headers.length === 0) return true
|
|
1405
|
+
// ISR state needs the function to revalidate.
|
|
1379
1406
|
return (
|
|
1380
|
-
metadata.
|
|
1381
|
-
metadata.
|
|
1407
|
+
metadata.revalidateSeconds === undefined &&
|
|
1408
|
+
metadata.expireSeconds === undefined &&
|
|
1409
|
+
!metadata.tags?.length &&
|
|
1410
|
+
headers.every(([name]) => !functionOnlyHeaders.has(name.toLowerCase()))
|
|
1382
1411
|
)
|
|
1383
1412
|
}
|
|
1413
|
+
|
|
1414
|
+
/** Headers beyond content-type, which an override carries instead. */
|
|
1415
|
+
function routeHeaders(metadata: StaticFileMetadata) {
|
|
1416
|
+
return metadata.headers.filter(([name]) => name.toLowerCase() !== 'content-type')
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// Keeps each route pattern well inside Vercel's route size limits.
|
|
1420
|
+
const maxRouteSourceLength = 2000
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* Route headers for the CDN-served static files whose headers an override cannot carry, so the CDN
|
|
1424
|
+
* answers with what `pnext start` sends. Files sharing a header set share a route.
|
|
1425
|
+
*/
|
|
1426
|
+
function staticHeaderRoutes(staticFiles: Record<string, StaticFileMetadata>) {
|
|
1427
|
+
const groups = new Map<string, { headers: Record<string, string>; patterns: string[] }>()
|
|
1428
|
+
for (const [relative, metadata] of Object.entries(staticFiles)) {
|
|
1429
|
+
const extra = routeHeaders(metadata)
|
|
1430
|
+
if (extra.length === 0 || !canServeStaticOnVercel(metadata)) continue
|
|
1431
|
+
const headers = Object.fromEntries(new Headers(extra))
|
|
1432
|
+
const key = JSON.stringify(headers)
|
|
1433
|
+
const group = groups.get(key) ?? { headers, patterns: [] }
|
|
1434
|
+
groups.set(key, group)
|
|
1435
|
+
group.patterns.push(servedPathPattern(relative))
|
|
1436
|
+
}
|
|
1437
|
+
return [...groups.values()].flatMap(({ headers, patterns }) => {
|
|
1438
|
+
const sources: string[] = []
|
|
1439
|
+
for (const pattern of patterns) {
|
|
1440
|
+
const last = sources.length - 1
|
|
1441
|
+
if (last >= 0 && sources[last]!.length + pattern.length < maxRouteSourceLength) {
|
|
1442
|
+
sources[last] += `|${pattern}`
|
|
1443
|
+
} else {
|
|
1444
|
+
sources.push(pattern)
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return sources.map(source => ({
|
|
1448
|
+
src: `^/(?:${source})$`,
|
|
1449
|
+
methods: ['GET', 'HEAD'],
|
|
1450
|
+
headers,
|
|
1451
|
+
continue: true,
|
|
1452
|
+
}))
|
|
1453
|
+
})
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/** A static file's request paths: `a/index.html` also answers `/a` and `/a/`. */
|
|
1457
|
+
function servedPathPattern(relative: string) {
|
|
1458
|
+
if (relative === 'index.html') return '(?:index\\.html)?'
|
|
1459
|
+
if (!relative.endsWith('/index.html')) return escapeRegex(relative)
|
|
1460
|
+
return `${escapeRegex(relative.slice(0, -'/index.html'.length))}(?:/|/index\\.html)?`
|
|
1461
|
+
}
|
|
@@ -1352,10 +1352,18 @@ export const immutableCacheControl = 'public, max-age=31536000, immutable'
|
|
|
1352
1352
|
* and stay revalidating; the public/ tree the app ships is likewise untouched.
|
|
1353
1353
|
*/
|
|
1354
1354
|
export function immutableAssetPath(relativePath: string) {
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1355
|
+
return immutableAssetPrefixes().some(prefix => relativePath.startsWith(prefix))
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/** The public-relative prefixes `immutableAssetPath` covers. */
|
|
1359
|
+
export function immutableAssetPrefixes() {
|
|
1360
|
+
return [
|
|
1361
|
+
'assets/',
|
|
1362
|
+
'_next/static/',
|
|
1363
|
+
...getAssetExtensions()
|
|
1364
|
+
.staticAssetPublicPrefixes()
|
|
1365
|
+
.map(prefix => prefix.replace(/^\/+/, '')),
|
|
1366
|
+
]
|
|
1359
1367
|
}
|
|
1360
1368
|
|
|
1361
1369
|
async function firstFile(root: string, files: string[]) {
|
|
@@ -1118,6 +1118,10 @@ type NavigationPaintHold = HTMLElement & {
|
|
|
1118
1118
|
__pnextObserver?: MutationObserver
|
|
1119
1119
|
/** The window scroll when the copy was taken. */
|
|
1120
1120
|
__pnextScroll?: [number, number]
|
|
1121
|
+
/** The `[data-scroll-root]` scroll the copy shows. */
|
|
1122
|
+
__pnextScrollTop?: number
|
|
1123
|
+
/** The navigation that removes the hold. */
|
|
1124
|
+
__pnextSequence?: number
|
|
1121
1125
|
}
|
|
1122
1126
|
|
|
1123
1127
|
function removeNavigationPaintHold(hold: NavigationPaintHold) {
|
|
@@ -1125,14 +1129,19 @@ function removeNavigationPaintHold(hold: NavigationPaintHold) {
|
|
|
1125
1129
|
hold.remove()
|
|
1126
1130
|
}
|
|
1127
1131
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1132
|
+
// A released hold until a frame renders without it: until then it is still what the screen shows.
|
|
1133
|
+
let releasedHold: NavigationPaintHold | null = null
|
|
1134
|
+
|
|
1135
|
+
function createNavigationPaintHold(sequence: number): NavigationPaintHold | null {
|
|
1136
|
+
// A prior hold on screen is taken over, so the older release is a no-op.
|
|
1137
|
+
const priorHold =
|
|
1138
|
+
document.querySelector<NavigationPaintHold>('[data-pnext-navigation-paint-hold]') ??
|
|
1139
|
+
releasedHold
|
|
1140
|
+
releasedHold = null
|
|
1141
|
+
if (priorHold) {
|
|
1142
|
+
priorHold.__pnextSequence = sequence
|
|
1143
|
+
return priorHold
|
|
1144
|
+
}
|
|
1136
1145
|
const roots = [...document.body.children].filter(
|
|
1137
1146
|
element =>
|
|
1138
1147
|
!element.hasAttribute('data-pnext-navigation-paint-hold') &&
|
|
@@ -1145,7 +1154,10 @@ function createNavigationPaintHold(): NavigationPaintHold | null {
|
|
|
1145
1154
|
hold.setAttribute('aria-hidden', 'true')
|
|
1146
1155
|
hold.style.cssText =
|
|
1147
1156
|
'position:fixed;inset:0;z-index:2147483646;overflow:hidden;pointer-events:none;background:Canvas;visibility:visible!important'
|
|
1148
|
-
|
|
1157
|
+
// compat.next keeps 0.1.0's unscrolled copy, which its server action refresh scroll relies on.
|
|
1158
|
+
if (typeof __PNEXT_NEXT_ROUTER__ === 'undefined')
|
|
1159
|
+
hold.__pnextScroll = [window.scrollX, window.scrollY]
|
|
1160
|
+
hold.__pnextSequence = sequence
|
|
1149
1161
|
// A <body> shell over the whole hold: body styles lay out and paint the copy as the page was.
|
|
1150
1162
|
const body = document.body.cloneNode() as HTMLElement
|
|
1151
1163
|
body.style.minHeight = '100%'
|
|
@@ -1181,15 +1193,13 @@ function attachNavigationPaintHold(
|
|
|
1181
1193
|
// Keep that live tree visible to focus management and selector observers;
|
|
1182
1194
|
// hiding <body> also hides every real destination element from browser gates.
|
|
1183
1195
|
placeNavigationPaintHold(hold)
|
|
1196
|
+
// A newer navigation leaves the hold up: its commit takes it over, or this release removes it.
|
|
1184
1197
|
hold.__pnextObserver ??= new MutationObserver(() => {
|
|
1185
|
-
if (sequence !== navigationSequence) {
|
|
1186
|
-
return removeNavigationPaintHold(hold)
|
|
1187
|
-
}
|
|
1188
1198
|
if (!hold.isConnected) placeNavigationPaintHold(hold)
|
|
1189
1199
|
})
|
|
1190
1200
|
hold.__pnextObserver.observe(document.documentElement, { childList: true, subtree: true })
|
|
1191
1201
|
const scrollRoot = hold.querySelector<HTMLElement>('[data-scroll-root]')
|
|
1192
|
-
if (scrollRoot) scrollRoot.scrollTop = scrollTop
|
|
1202
|
+
if (scrollRoot) scrollRoot.scrollTop = hold.__pnextScrollTop ??= scrollTop
|
|
1193
1203
|
}
|
|
1194
1204
|
|
|
1195
1205
|
/** Append the hold scrolled as the departing page was; appending resets an element's scroll. */
|
|
@@ -1198,7 +1208,11 @@ function placeNavigationPaintHold(hold: NavigationPaintHold) {
|
|
|
1198
1208
|
if (hold.__pnextScroll) hold.scrollTo?.(...hold.__pnextScroll)
|
|
1199
1209
|
}
|
|
1200
1210
|
|
|
1201
|
-
async function releaseNavigationPaintHold(
|
|
1211
|
+
async function releaseNavigationPaintHold(
|
|
1212
|
+
hold: NavigationPaintHold | null,
|
|
1213
|
+
sequence: number,
|
|
1214
|
+
reveal?: () => void,
|
|
1215
|
+
) {
|
|
1202
1216
|
if (!hold) return
|
|
1203
1217
|
// mountRoute, client effects and the navigation commit have completed before
|
|
1204
1218
|
// this runs. Keep the departing frame through the next rendering opportunity,
|
|
@@ -1206,8 +1220,17 @@ async function releaseNavigationPaintHold(hold: NavigationPaintHold | null, reve
|
|
|
1206
1220
|
if (window.requestAnimationFrame) {
|
|
1207
1221
|
await new Promise<void>(resolve => window.requestAnimationFrame(() => resolve()))
|
|
1208
1222
|
}
|
|
1223
|
+
if (hold.__pnextSequence !== sequence) return
|
|
1209
1224
|
reveal?.()
|
|
1210
1225
|
removeNavigationPaintHold(hold)
|
|
1226
|
+
if (!window.ResizeObserver) return
|
|
1227
|
+
releasedHold = hold
|
|
1228
|
+
// Observing fires at the next rendering update, the first frame that paints without the hold.
|
|
1229
|
+
const rendered = new ResizeObserver(() => {
|
|
1230
|
+
rendered.disconnect()
|
|
1231
|
+
if (releasedHold === hold) releasedHold = null
|
|
1232
|
+
})
|
|
1233
|
+
rendered.observe(document.documentElement)
|
|
1211
1234
|
}
|
|
1212
1235
|
|
|
1213
1236
|
/**
|
|
@@ -1581,10 +1604,13 @@ declare const __PNEXT_NEXT_ROUTER__: boolean | undefined
|
|
|
1581
1604
|
// returns to where its entry was left.
|
|
1582
1605
|
const coreNavigationScroll: NavigationScrollAction = (url, { pop, scroll }) => {
|
|
1583
1606
|
const state = historyState()
|
|
1584
|
-
const
|
|
1585
|
-
? (entryScroll.get(state.__pnextEntry) ?? (state.__pnextScroll as number[] | undefined)
|
|
1586
|
-
:
|
|
1587
|
-
const
|
|
1607
|
+
const saved = pop
|
|
1608
|
+
? (entryScroll.get(state.__pnextEntry) ?? (state.__pnextScroll as number[] | undefined))
|
|
1609
|
+
: undefined
|
|
1610
|
+
const [x = 0, y = 0] = saved ?? []
|
|
1611
|
+
// A traversal to an entry with no saved position lands on its hash, as a new visit does.
|
|
1612
|
+
const target =
|
|
1613
|
+
!saved && url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1)))
|
|
1588
1614
|
if (target) target.scrollIntoView()
|
|
1589
1615
|
else if (pop || scroll !== false) window.scrollTo(x, y)
|
|
1590
1616
|
}
|
|
@@ -2317,6 +2343,8 @@ export function rearmVisiblePrefetches(): void {
|
|
|
2317
2343
|
const retry = () => {
|
|
2318
2344
|
if (generation !== revalidationPrefetchGeneration) return
|
|
2319
2345
|
revalidationPrefetchBlocked = false
|
|
2346
|
+
// A test host may tear the window down before the timer fires.
|
|
2347
|
+
if (typeof window === 'undefined') return
|
|
2320
2348
|
for (const element of prefetchedElements) {
|
|
2321
2349
|
if (!element.isConnected) {
|
|
2322
2350
|
prefetchedElements.delete(element)
|
|
@@ -2567,6 +2595,8 @@ export interface PrefetchEntry {
|
|
|
2567
2595
|
stateKey: string
|
|
2568
2596
|
/** A pending prefetch always dedupes; settled stale data must refetch. */
|
|
2569
2597
|
settled: boolean
|
|
2598
|
+
/** When the response landed, for entries that started as a pending prefetch. */
|
|
2599
|
+
settledTime?: number
|
|
2570
2600
|
/** Byte size of the settled document (LRU size accounting). */
|
|
2571
2601
|
bytes?: number
|
|
2572
2602
|
/** Settled as a shell-only (partial prefetch) response — see PrefetchedPage. */
|
|
@@ -3114,8 +3144,10 @@ export function prefetchRoute(
|
|
|
3114
3144
|
// Null = task stopped short with no data; a settled null would dedupe
|
|
3115
3145
|
// every later reveal into null, so drop the entry and let a viewport
|
|
3116
3146
|
// re-entry reschedule.
|
|
3117
|
-
if (fetched)
|
|
3118
|
-
|
|
3147
|
+
if (fetched) {
|
|
3148
|
+
entry.settled = true
|
|
3149
|
+
entry.settledTime = clientClockNow()
|
|
3150
|
+
} else prefetchCache.delete(cacheKey)
|
|
3119
3151
|
}
|
|
3120
3152
|
// Size accounting only applies to settled documents — re-trim now that
|
|
3121
3153
|
// this entry's bytes count against the budget.
|
|
@@ -4083,6 +4115,10 @@ export function showLoadingShell(
|
|
|
4083
4115
|
if (!markerRange && slotContainer === document.body && !scopedOwner) return false
|
|
4084
4116
|
const container = scopedOwner ?? slotContainer
|
|
4085
4117
|
const fragment = document.createDocumentFragment()
|
|
4118
|
+
// A live page slot is filled from the incoming page slot, never from a container inside it.
|
|
4119
|
+
// compat.next keeps its own fill, which its nested parallel-slot layouts rely on.
|
|
4120
|
+
const incomingSlot =
|
|
4121
|
+
typeof __PNEXT_NEXT_ROUTER__ === 'undefined' && markerRange ? pageSlotRange(doc.body) : null
|
|
4086
4122
|
// The streamed document can already contain an ancestor layout that resolved before the
|
|
4087
4123
|
// loading boundary. Keep that prefix when painting the fallback: replacing the target
|
|
4088
4124
|
// with only the suspense children loses the eagerly prefetched layout.
|
|
@@ -4091,17 +4127,19 @@ export function showLoadingShell(
|
|
|
4091
4127
|
(container.id ? doc.getElementById(container.id) : null) ??
|
|
4092
4128
|
doc.querySelector('[data-pnext-root]') ??
|
|
4093
4129
|
(container === document.body ? doc.body : null)
|
|
4094
|
-
const sourceNodes =
|
|
4095
|
-
? [
|
|
4096
|
-
:
|
|
4097
|
-
?
|
|
4098
|
-
|
|
4099
|
-
inline
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
:
|
|
4103
|
-
|
|
4104
|
-
|
|
4130
|
+
const sourceNodes = incomingSlot
|
|
4131
|
+
? incomingSlot[2]
|
|
4132
|
+
: incomingTarget
|
|
4133
|
+
? [...incomingTarget.childNodes]
|
|
4134
|
+
: inline
|
|
4135
|
+
? // The inline anchor is whatever element holds the markers (often <body>);
|
|
4136
|
+
// only the fallback BETWEEN them is this boundary's content.
|
|
4137
|
+
inline.nodes
|
|
4138
|
+
: suspense
|
|
4139
|
+
? [...(suspense.closest('pnext-layout[data-pnext-segment]') ?? suspense).childNodes]
|
|
4140
|
+
: // Boundary-free markup with no page container to copy from: there is
|
|
4141
|
+
// nothing this paint could put on screen.
|
|
4142
|
+
null
|
|
4105
4143
|
if (!sourceNodes) return false
|
|
4106
4144
|
// A painted loading shell IS a committed navigation (pushOptimisticUrl moves the address
|
|
4107
4145
|
// bar the instant this returns true), so the window route state must reflect the
|
|
@@ -4470,8 +4508,8 @@ function unsettledPrefetchDeadline(): Promise<null> {
|
|
|
4470
4508
|
return new Promise(resolve => setTimeout(() => resolve(null), UNSETTLED_PREFETCH_WAIT_MS))
|
|
4471
4509
|
}
|
|
4472
4510
|
|
|
4473
|
-
// Kept copies older than REVALIDATE_AFTER_MS that core commits
|
|
4474
|
-
const keptCopies = new
|
|
4511
|
+
// Kept copies older than REVALIDATE_AFTER_MS that core commits, then swaps for their fresh render.
|
|
4512
|
+
const keptCopies = new WeakMap<PrefetchedPage, Promise<PrefetchedPage | null>>()
|
|
4475
4513
|
const REVALIDATE_AFTER_MS = 2_000
|
|
4476
4514
|
|
|
4477
4515
|
async function pageForNavigation(
|
|
@@ -4544,9 +4582,12 @@ async function pageForNavigation(
|
|
|
4544
4582
|
if (
|
|
4545
4583
|
typeof __PNEXT_NEXT_ROUTER__ === 'undefined' &&
|
|
4546
4584
|
!prefetchStaleTimePolicy &&
|
|
4547
|
-
now - cached.time > REVALIDATE_AFTER_MS
|
|
4548
|
-
|
|
4549
|
-
|
|
4585
|
+
now - (cached.settledTime ?? cached.time) > REVALIDATE_AFTER_MS &&
|
|
4586
|
+
!documentStaticHintFromHtml(page.html)?.isStatic
|
|
4587
|
+
) {
|
|
4588
|
+
const fresh = fetchPage(url.href, { fullRender: true }).catch(() => null)
|
|
4589
|
+
keptCopies.set(page, fresh)
|
|
4590
|
+
}
|
|
4550
4591
|
return page
|
|
4551
4592
|
}
|
|
4552
4593
|
// Attached to an in-flight (or already settled) SHELL prefetch for this exact target:
|
|
@@ -5449,13 +5490,13 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
5449
5490
|
// The resolved client-root commit shows this copy only while its first complete paint settles.
|
|
5450
5491
|
const paintHoldScrollTop =
|
|
5451
5492
|
document.querySelector<HTMLElement>('[data-scroll-root]')?.scrollTop ?? 0
|
|
5452
|
-
const paintHold = createNavigationPaintHold()
|
|
5453
|
-
// Wake route-keyed layout children while the departing DOM is still attached. Their layout
|
|
5454
|
-
// cleanups can save shell state before unmount/swap; pop mounts the destination first instead.
|
|
5455
|
-
window.__PNEXT_ACTIVE_ENTRY__?.unmount?.(keptIslands)
|
|
5493
|
+
const paintHold = createNavigationPaintHold(sequence)
|
|
5456
5494
|
let focusedBeforeSwap: Element | null = null
|
|
5457
5495
|
let navigationFocusTarget: HTMLElement | null = null
|
|
5458
5496
|
try {
|
|
5497
|
+
// Wake route-keyed layout children while the departing DOM is still attached. Their layout
|
|
5498
|
+
// cleanups can save shell state before unmount/swap; pop mounts the destination first instead.
|
|
5499
|
+
window.__PNEXT_ACTIVE_ENTRY__?.unmount?.(keptIslands)
|
|
5459
5500
|
if (unmountSlotlessPage && departingRouteKey && departingRouteKey !== targetRouteKey) {
|
|
5460
5501
|
// Cover the lifecycle-only empty-page pass. This is the same outgoing screen clone used for
|
|
5461
5502
|
// the final atomic commit, attached early enough that no empty intermediate frame can paint.
|
|
@@ -5569,7 +5610,7 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
5569
5610
|
}
|
|
5570
5611
|
}
|
|
5571
5612
|
} finally {
|
|
5572
|
-
await releaseNavigationPaintHold(paintHold, () => {
|
|
5613
|
+
await releaseNavigationPaintHold(paintHold, sequence, () => {
|
|
5573
5614
|
if (!url.hash && !stylesheetReconciler && sequence === navigationSequence)
|
|
5574
5615
|
pruneStylesheets(doc)
|
|
5575
5616
|
})
|
|
@@ -5605,16 +5646,12 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
5605
5646
|
if (!options.pop && departingBfcacheId && departingBfcacheId === historyBfcacheId()) {
|
|
5606
5647
|
restoreFormStateWhenMounted(departingBfcacheId, sequence, true)
|
|
5607
5648
|
}
|
|
5608
|
-
if (
|
|
5609
|
-
typeof __PNEXT_NEXT_ROUTER__ === 'undefined' &&
|
|
5610
|
-
keptCopies.has(page) &&
|
|
5611
|
-
!documentStaticHintFromHtml(page.html)?.isStatic
|
|
5612
|
-
)
|
|
5649
|
+
if (typeof __PNEXT_NEXT_ROUTER__ === 'undefined' && keptCopies.has(page))
|
|
5613
5650
|
void revalidateCommittedPage(targetUrl, sequence, page)
|
|
5614
5651
|
}
|
|
5615
5652
|
|
|
5616
5653
|
async function revalidateCommittedPage(url: URL, sequence: number, page: PrefetchedPage) {
|
|
5617
|
-
const fresh = await
|
|
5654
|
+
const fresh = await keptCopies.get(page)
|
|
5618
5655
|
if (!fresh?.ok || fresh.html === page.html || sequence !== navigationSequence) return
|
|
5619
5656
|
await softNavigate(url.href, {
|
|
5620
5657
|
replace: true,
|
|
@@ -222,6 +222,8 @@ function installStreamErrorBoundaries(
|
|
|
222
222
|
const observe = () => {
|
|
223
223
|
scan(document)
|
|
224
224
|
new MutationObserver(mutations => {
|
|
225
|
+
// A test host may tear the DOM globals down before a queued record is delivered.
|
|
226
|
+
if (typeof Element === 'undefined') return
|
|
225
227
|
for (const mutation of mutations) {
|
|
226
228
|
for (const node of Array.from(mutation.addedNodes)) {
|
|
227
229
|
if (!(node instanceof Element)) continue
|
package/src/runtime/modules.ts
CHANGED
|
@@ -1440,6 +1440,7 @@ async function writeCompiledFile(file: string, contents: string) {
|
|
|
1440
1440
|
function missingCompiledArtifact(entry: string): string | undefined {
|
|
1441
1441
|
const root = compiledArtifactProfileRoot(entry)
|
|
1442
1442
|
if (!root) return existsSync(entry) ? undefined : entry
|
|
1443
|
+
const serverRoot = path.dirname(root)
|
|
1443
1444
|
|
|
1444
1445
|
const visiting = new Set<string>()
|
|
1445
1446
|
const visit = (file: string): string | undefined => {
|
|
@@ -1447,7 +1448,7 @@ function missingCompiledArtifact(entry: string): string | undefined {
|
|
|
1447
1448
|
if (!existsSync(file)) return file
|
|
1448
1449
|
// Raw framework/package file URLs are ordinary immutable dependencies, not
|
|
1449
1450
|
// members of the materialized graph whose publication order we own.
|
|
1450
|
-
if (!isInside(
|
|
1451
|
+
if (!isInside(serverRoot, file) || !compiledScriptFilePattern.test(file)) return undefined
|
|
1451
1452
|
|
|
1452
1453
|
visiting.add(file)
|
|
1453
1454
|
const cachedTargets = completeArtifactClosures.get(file)
|
|
@@ -1480,13 +1481,13 @@ function missingCompiledArtifact(entry: string): string | undefined {
|
|
|
1480
1481
|
} catch {
|
|
1481
1482
|
return sourcePath
|
|
1482
1483
|
}
|
|
1483
|
-
|
|
1484
|
+
const emittedRoot = compiledArtifactProfileRoot(target)
|
|
1485
|
+
if (!emittedRoot) continue
|
|
1486
|
+
// Profiles share one closure: a 'use client' module compiles its imports into the client profile.
|
|
1487
|
+
if (!isInside(serverRoot, target)) {
|
|
1484
1488
|
if (existsSync(target)) continue
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
// dead reference to this same profile can be a relocated cache member.
|
|
1488
|
-
if (!emittedRoot || path.basename(emittedRoot) !== path.basename(root)) continue
|
|
1489
|
-
target = path.resolve(root, path.relative(emittedRoot, target))
|
|
1489
|
+
// A dead reference into another cache is a relocated member of this one.
|
|
1490
|
+
target = path.resolve(serverRoot, path.relative(path.dirname(emittedRoot), target))
|
|
1490
1491
|
}
|
|
1491
1492
|
const missing = visit(target)
|
|
1492
1493
|
if (missing) return missing
|