@wular/pnext 0.1.0 → 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 +1 -1
- package/reference/navigation.md +2 -2
- package/src/cli/adapters/vercel.ts +87 -9
- package/src/cli/serve/pipeline.ts +12 -4
- package/src/client/router/runtime.ts +173 -59
- package/src/compat/client/errors/install.ts +2 -0
- package/src/compat/register/bundler.ts +2 -0
- package/src/render/renderer.ts +5 -1
- package/src/runtime/modules.ts +8 -7
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
|
|
|
@@ -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[]) {
|
|
@@ -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,12 @@ 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]
|
|
1121
|
+
/** The `[data-scroll-root]` scroll the copy shows. */
|
|
1122
|
+
__pnextScrollTop?: number
|
|
1123
|
+
/** The navigation that removes the hold. */
|
|
1124
|
+
__pnextSequence?: number
|
|
1105
1125
|
}
|
|
1106
1126
|
|
|
1107
1127
|
function removeNavigationPaintHold(hold: NavigationPaintHold) {
|
|
@@ -1109,14 +1129,19 @@ function removeNavigationPaintHold(hold: NavigationPaintHold) {
|
|
|
1109
1129
|
hold.remove()
|
|
1110
1130
|
}
|
|
1111
1131
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|
+
}
|
|
1120
1145
|
const roots = [...document.body.children].filter(
|
|
1121
1146
|
element =>
|
|
1122
1147
|
!element.hasAttribute('data-pnext-navigation-paint-hold') &&
|
|
@@ -1124,12 +1149,20 @@ function createNavigationPaintHold(): NavigationPaintHold | null {
|
|
|
1124
1149
|
element.textContent?.trim(),
|
|
1125
1150
|
)
|
|
1126
1151
|
if (roots.length === 0) return null
|
|
1127
|
-
const hold = document.createElement('div')
|
|
1152
|
+
const hold: NavigationPaintHold = document.createElement('div')
|
|
1128
1153
|
hold.setAttribute('data-pnext-navigation-paint-hold', '')
|
|
1129
1154
|
hold.setAttribute('aria-hidden', 'true')
|
|
1130
1155
|
hold.style.cssText =
|
|
1131
1156
|
'position:fixed;inset:0;z-index:2147483646;overflow:hidden;pointer-events:none;background:Canvas;visibility:visible!important'
|
|
1132
|
-
|
|
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
|
|
1161
|
+
// A <body> shell over the whole hold: body styles lay out and paint the copy as the page was.
|
|
1162
|
+
const body = document.body.cloneNode() as HTMLElement
|
|
1163
|
+
body.style.minHeight = '100%'
|
|
1164
|
+
body.append(...roots.map(root => root.cloneNode(true)))
|
|
1165
|
+
hold.append(body)
|
|
1133
1166
|
// The copy is paint-only: route mount scans and id lookups must never treat it as live UI.
|
|
1134
1167
|
for (const node of hold.querySelectorAll('[data-pnext-client]'))
|
|
1135
1168
|
node.removeAttribute('data-pnext-client')
|
|
@@ -1159,19 +1192,27 @@ function attachNavigationPaintHold(
|
|
|
1159
1192
|
// The opaque fixed clone covers the viewport while the committed tree mounts.
|
|
1160
1193
|
// Keep that live tree visible to focus management and selector observers;
|
|
1161
1194
|
// hiding <body> also hides every real destination element from browser gates.
|
|
1162
|
-
|
|
1195
|
+
placeNavigationPaintHold(hold)
|
|
1196
|
+
// A newer navigation leaves the hold up: its commit takes it over, or this release removes it.
|
|
1163
1197
|
hold.__pnextObserver ??= new MutationObserver(() => {
|
|
1164
|
-
if (
|
|
1165
|
-
return removeNavigationPaintHold(hold)
|
|
1166
|
-
}
|
|
1167
|
-
if (!hold.isConnected) document.body.append(hold)
|
|
1198
|
+
if (!hold.isConnected) placeNavigationPaintHold(hold)
|
|
1168
1199
|
})
|
|
1169
1200
|
hold.__pnextObserver.observe(document.documentElement, { childList: true, subtree: true })
|
|
1170
1201
|
const scrollRoot = hold.querySelector<HTMLElement>('[data-scroll-root]')
|
|
1171
|
-
if (scrollRoot) scrollRoot.scrollTop = scrollTop
|
|
1202
|
+
if (scrollRoot) scrollRoot.scrollTop = hold.__pnextScrollTop ??= scrollTop
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/** Append the hold scrolled as the departing page was; appending resets an element's scroll. */
|
|
1206
|
+
function placeNavigationPaintHold(hold: NavigationPaintHold) {
|
|
1207
|
+
document.body.append(hold)
|
|
1208
|
+
if (hold.__pnextScroll) hold.scrollTo?.(...hold.__pnextScroll)
|
|
1172
1209
|
}
|
|
1173
1210
|
|
|
1174
|
-
async function releaseNavigationPaintHold(
|
|
1211
|
+
async function releaseNavigationPaintHold(
|
|
1212
|
+
hold: NavigationPaintHold | null,
|
|
1213
|
+
sequence: number,
|
|
1214
|
+
reveal?: () => void,
|
|
1215
|
+
) {
|
|
1175
1216
|
if (!hold) return
|
|
1176
1217
|
// mountRoute, client effects and the navigation commit have completed before
|
|
1177
1218
|
// this runs. Keep the departing frame through the next rendering opportunity,
|
|
@@ -1179,7 +1220,17 @@ async function releaseNavigationPaintHold(hold: NavigationPaintHold | null) {
|
|
|
1179
1220
|
if (window.requestAnimationFrame) {
|
|
1180
1221
|
await new Promise<void>(resolve => window.requestAnimationFrame(() => resolve()))
|
|
1181
1222
|
}
|
|
1223
|
+
if (hold.__pnextSequence !== sequence) return
|
|
1224
|
+
reveal?.()
|
|
1182
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)
|
|
1183
1234
|
}
|
|
1184
1235
|
|
|
1185
1236
|
/**
|
|
@@ -1510,6 +1561,11 @@ function isDevDocument() {
|
|
|
1510
1561
|
return Boolean(document.querySelector('script[data-pnext-dev]'))
|
|
1511
1562
|
}
|
|
1512
1563
|
|
|
1564
|
+
// Next's dev router neither prefetches nor paints loading states; core dev navigates like production.
|
|
1565
|
+
function nextDevDocument() {
|
|
1566
|
+
return prefetchStaleTimePolicy !== undefined && isDevDocument()
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1513
1569
|
// `replace` is also set for history TRAVERSALS (options.pop): assign() would
|
|
1514
1570
|
// push a new entry over the popped one and truncate the forward stack, so a
|
|
1515
1571
|
// bailout during back/forward must replace the current entry instead.
|
|
@@ -1541,6 +1597,27 @@ function saveScrollPosition() {
|
|
|
1541
1597
|
}
|
|
1542
1598
|
}
|
|
1543
1599
|
|
|
1600
|
+
// Defined by compat.next builds, whose router policies replace core's scroll and refresh ones.
|
|
1601
|
+
declare const __PNEXT_NEXT_ROUTER__: boolean | undefined
|
|
1602
|
+
|
|
1603
|
+
// Core's scroll policy when compat installs none: a new page starts at the top, a traversal
|
|
1604
|
+
// returns to where its entry was left.
|
|
1605
|
+
const coreNavigationScroll: NavigationScrollAction = (url, { pop, scroll }) => {
|
|
1606
|
+
const state = historyState()
|
|
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)))
|
|
1614
|
+
if (target) target.scrollIntoView()
|
|
1615
|
+
else if (pop || scroll !== false) window.scrollTo(x, y)
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// Where each entry was left, pops included, which cannot write the state of the entry they leave.
|
|
1619
|
+
const entryScroll = new Map<unknown, number[]>()
|
|
1620
|
+
|
|
1544
1621
|
function storeNavState() {
|
|
1545
1622
|
try {
|
|
1546
1623
|
history.replaceState(
|
|
@@ -2266,6 +2343,8 @@ export function rearmVisiblePrefetches(): void {
|
|
|
2266
2343
|
const retry = () => {
|
|
2267
2344
|
if (generation !== revalidationPrefetchGeneration) return
|
|
2268
2345
|
revalidationPrefetchBlocked = false
|
|
2346
|
+
// A test host may tear the window down before the timer fires.
|
|
2347
|
+
if (typeof window === 'undefined') return
|
|
2269
2348
|
for (const element of prefetchedElements) {
|
|
2270
2349
|
if (!element.isConnected) {
|
|
2271
2350
|
prefetchedElements.delete(element)
|
|
@@ -2516,6 +2595,8 @@ export interface PrefetchEntry {
|
|
|
2516
2595
|
stateKey: string
|
|
2517
2596
|
/** A pending prefetch always dedupes; settled stale data must refetch. */
|
|
2518
2597
|
settled: boolean
|
|
2598
|
+
/** When the response landed, for entries that started as a pending prefetch. */
|
|
2599
|
+
settledTime?: number
|
|
2519
2600
|
/** Byte size of the settled document (LRU size accounting). */
|
|
2520
2601
|
bytes?: number
|
|
2521
2602
|
/** Settled as a shell-only (partial prefetch) response — see PrefetchedPage. */
|
|
@@ -2855,9 +2936,7 @@ export function prefetchRoute(
|
|
|
2855
2936
|
href: string,
|
|
2856
2937
|
options: PrefetchOptions = {},
|
|
2857
2938
|
): Promise<PrefetchedPage | null> {
|
|
2858
|
-
|
|
2859
|
-
// would hammer the dev server for little gain. Navigation still fetches.
|
|
2860
|
-
if (isDevDocument()) return Promise.resolve(null)
|
|
2939
|
+
if (nextDevDocument()) return Promise.resolve(null)
|
|
2861
2940
|
if (isBotUserAgent()) return Promise.resolve(null)
|
|
2862
2941
|
// `strict` is handled at the facade, the only entry point that can be handed
|
|
2863
2942
|
// an unparseable href; everything reaching here already resolved once.
|
|
@@ -3065,8 +3144,10 @@ export function prefetchRoute(
|
|
|
3065
3144
|
// Null = task stopped short with no data; a settled null would dedupe
|
|
3066
3145
|
// every later reveal into null, so drop the entry and let a viewport
|
|
3067
3146
|
// re-entry reschedule.
|
|
3068
|
-
if (fetched)
|
|
3069
|
-
|
|
3147
|
+
if (fetched) {
|
|
3148
|
+
entry.settled = true
|
|
3149
|
+
entry.settledTime = clientClockNow()
|
|
3150
|
+
} else prefetchCache.delete(cacheKey)
|
|
3070
3151
|
}
|
|
3071
3152
|
// Size accounting only applies to settled documents — re-trim now that
|
|
3072
3153
|
// this entry's bytes count against the budget.
|
|
@@ -3997,7 +4078,6 @@ export function showLoadingShell(
|
|
|
3997
4078
|
) {
|
|
3998
4079
|
if (sequence !== navigationSequence) return false
|
|
3999
4080
|
if (typeof DOMParser === 'undefined') return false
|
|
4000
|
-
const paintHold = document.querySelector<HTMLElement>('[data-pnext-navigation-paint-hold]')
|
|
4001
4081
|
const doc = new DOMParser().parseFromString(shellHtml, 'text/html')
|
|
4002
4082
|
materializeClientIslandMarkers(doc)
|
|
4003
4083
|
// A STATIC STAGE owns whatever chunks streamed with it, so resolve them before picking what
|
|
@@ -4035,6 +4115,10 @@ export function showLoadingShell(
|
|
|
4035
4115
|
if (!markerRange && slotContainer === document.body && !scopedOwner) return false
|
|
4036
4116
|
const container = scopedOwner ?? slotContainer
|
|
4037
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
|
|
4038
4122
|
// The streamed document can already contain an ancestor layout that resolved before the
|
|
4039
4123
|
// loading boundary. Keep that prefix when painting the fallback: replacing the target
|
|
4040
4124
|
// with only the suspense children loses the eagerly prefetched layout.
|
|
@@ -4043,17 +4127,19 @@ export function showLoadingShell(
|
|
|
4043
4127
|
(container.id ? doc.getElementById(container.id) : null) ??
|
|
4044
4128
|
doc.querySelector('[data-pnext-root]') ??
|
|
4045
4129
|
(container === document.body ? doc.body : null)
|
|
4046
|
-
const sourceNodes =
|
|
4047
|
-
? [
|
|
4048
|
-
:
|
|
4049
|
-
?
|
|
4050
|
-
|
|
4051
|
-
inline
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
:
|
|
4055
|
-
|
|
4056
|
-
|
|
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
|
|
4057
4143
|
if (!sourceNodes) return false
|
|
4058
4144
|
// A painted loading shell IS a committed navigation (pushOptimisticUrl moves the address
|
|
4059
4145
|
// bar the instant this returns true), so the window route state must reflect the
|
|
@@ -4085,7 +4171,6 @@ export function showLoadingShell(
|
|
|
4085
4171
|
// useOffline(), say), and an unmounted island keeps its SSR value forever.
|
|
4086
4172
|
// mountRoute is idempotent, so mounting on every paint is safe.
|
|
4087
4173
|
mountPaintedIslands(doc)
|
|
4088
|
-
if (paintHold) document.body.append(paintHold)
|
|
4089
4174
|
return true
|
|
4090
4175
|
}
|
|
4091
4176
|
|
|
@@ -4263,7 +4348,6 @@ function unwrapSuspenseFallbacks(root: ParentNode): void {
|
|
|
4263
4348
|
*/
|
|
4264
4349
|
export function paintStaticStageSubtree(html: string): boolean {
|
|
4265
4350
|
if (typeof DOMParser === 'undefined') return false
|
|
4266
|
-
const paintHold = document.querySelector<HTMLElement>('[data-pnext-navigation-paint-hold]')
|
|
4267
4351
|
const doc = new DOMParser().parseFromString(html, 'text/html')
|
|
4268
4352
|
if (!isPNextDocument(doc)) return false
|
|
4269
4353
|
// Materialize BEFORE comparing structure: the renderer ships the page slot (and
|
|
@@ -4297,7 +4381,6 @@ export function paintStaticStageSubtree(html: string): boolean {
|
|
|
4297
4381
|
// against the tree painted here. `swapBody`'s own body-child reuse still applies.
|
|
4298
4382
|
swapBody(doc)
|
|
4299
4383
|
mountPaintedIslands(doc)
|
|
4300
|
-
if (paintHold) document.body.append(paintHold)
|
|
4301
4384
|
return true
|
|
4302
4385
|
}
|
|
4303
4386
|
|
|
@@ -4425,6 +4508,10 @@ function unsettledPrefetchDeadline(): Promise<null> {
|
|
|
4425
4508
|
return new Promise(resolve => setTimeout(() => resolve(null), UNSETTLED_PREFETCH_WAIT_MS))
|
|
4426
4509
|
}
|
|
4427
4510
|
|
|
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>>()
|
|
4513
|
+
const REVALIDATE_AFTER_MS = 2_000
|
|
4514
|
+
|
|
4428
4515
|
async function pageForNavigation(
|
|
4429
4516
|
url: URL,
|
|
4430
4517
|
options: SoftNavigateOptions = {},
|
|
@@ -4491,7 +4578,18 @@ async function pageForNavigation(
|
|
|
4491
4578
|
cached.settled || cached.full
|
|
4492
4579
|
? await cached.page
|
|
4493
4580
|
: await Promise.race([cached.page, unsettledPrefetchDeadline()])
|
|
4494
|
-
if (page && !page.shellOnly)
|
|
4581
|
+
if (page && !page.shellOnly) {
|
|
4582
|
+
if (
|
|
4583
|
+
typeof __PNEXT_NEXT_ROUTER__ === 'undefined' &&
|
|
4584
|
+
!prefetchStaleTimePolicy &&
|
|
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
|
+
}
|
|
4591
|
+
return page
|
|
4592
|
+
}
|
|
4495
4593
|
// Attached to an in-flight (or already settled) SHELL prefetch for this exact target:
|
|
4496
4594
|
// the navigation issued no duplicate fetch, so paint the static stage it landed and let
|
|
4497
4595
|
// the dynamic stage stream in below.
|
|
@@ -4988,6 +5086,8 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
4988
5086
|
// into the live body. Capturing later would save the target's fallback as
|
|
4989
5087
|
// the previous history entry and restore a permanently stuck "Loading...".
|
|
4990
5088
|
if (!options.pop) saveScrollPosition()
|
|
5089
|
+
if (typeof __PNEXT_NEXT_ROUTER__ === 'undefined')
|
|
5090
|
+
entryScroll.set(routerState.renderedEntryId, [window.scrollX, window.scrollY])
|
|
4991
5091
|
if (departingBfcacheId) saveFormState(departingBfcacheId)
|
|
4992
5092
|
// Snapshot the departing page's live island roots NOW, before a loading shell
|
|
4993
5093
|
// can paint over (and detach) them below — they are stashed for back/forward
|
|
@@ -4996,11 +5096,6 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
4996
5096
|
// Snapshot plain root-layout DOM before a cached/streamed shell can detach it. The final swap
|
|
4997
5097
|
// reconciles onto these nodes so server layouts keep the same identity Next's root reconciler does.
|
|
4998
5098
|
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
5099
|
// Painting a prefetched loading shell IS a committed navigation, so push the requested URL
|
|
5005
5100
|
// here and let usePathname() and the address bar reflect the destination while the fetch is
|
|
5006
5101
|
// still in flight; the final commit replaces this entry with the resolved one (or, on a
|
|
@@ -5056,19 +5151,14 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
5056
5151
|
// server-side, and re-painting would replace a committed loading state with a second,
|
|
5057
5152
|
// different one. A segment's loading fallback commits ONCE per navigation.
|
|
5058
5153
|
let cachedStagePainted = false
|
|
5059
|
-
const devSoftNavigation =
|
|
5154
|
+
const devSoftNavigation = nextDevDocument()
|
|
5060
5155
|
const onShell =
|
|
5061
5156
|
restorePage || options.pop || refreshLike
|
|
5062
5157
|
? undefined
|
|
5063
5158
|
: (shellHtml: string) => {
|
|
5064
5159
|
if (cachedStagePainted) return
|
|
5065
5160
|
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
|
|
5161
|
+
if (!showLoadingShell(shellHtml, sequence, url, undefined, false, true)) return
|
|
5072
5162
|
// A loading boundary is a committed navigation state.
|
|
5073
5163
|
pushOptimisticUrl()
|
|
5074
5164
|
scheduleNavigationScroll(url, options)
|
|
@@ -5396,12 +5486,17 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
5396
5486
|
// page slot is a marker-range proxy); the entry's unmount compares roots by
|
|
5397
5487
|
// identity, so it rides the same set.
|
|
5398
5488
|
if (preservedPage) keptIslands.add(preservedPage.root as unknown as Element)
|
|
5399
|
-
//
|
|
5400
|
-
//
|
|
5401
|
-
|
|
5489
|
+
// Copy the screen as it is now, a loading or static stage included, before anything unmounts.
|
|
5490
|
+
// The resolved client-root commit shows this copy only while its first complete paint settles.
|
|
5491
|
+
const paintHoldScrollTop =
|
|
5492
|
+
document.querySelector<HTMLElement>('[data-scroll-root]')?.scrollTop ?? 0
|
|
5493
|
+
const paintHold = createNavigationPaintHold(sequence)
|
|
5402
5494
|
let focusedBeforeSwap: Element | null = null
|
|
5403
5495
|
let navigationFocusTarget: HTMLElement | null = null
|
|
5404
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)
|
|
5405
5500
|
if (unmountSlotlessPage && departingRouteKey && departingRouteKey !== targetRouteKey) {
|
|
5406
5501
|
// Cover the lifecycle-only empty-page pass. This is the same outgoing screen clone used for
|
|
5407
5502
|
// the final atomic commit, attached early enough that no empty intermediate frame can paint.
|
|
@@ -5465,7 +5560,8 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
5465
5560
|
// selector waiter can observe the destination immediately after this stack unwinds; pruning in
|
|
5466
5561
|
// the later post-mount phase let it catch the target node between its correct route sheet and a
|
|
5467
5562
|
// stale asynchronous prune from the preceding navigation.
|
|
5468
|
-
|
|
5563
|
+
// Core keeps the departing sheets while the paint hold still shows the departing screen.
|
|
5564
|
+
if (!url.hash && (stylesheetReconciler || !paintHold)) pruneStylesheets(doc)
|
|
5469
5565
|
// The swapped document carries the render's parallel-route state; pin it
|
|
5470
5566
|
// (plus the document itself) to this history entry so back/forward restores
|
|
5471
5567
|
// what was actually shown, without a server round trip.
|
|
@@ -5514,7 +5610,10 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
5514
5610
|
}
|
|
5515
5611
|
}
|
|
5516
5612
|
} finally {
|
|
5517
|
-
await releaseNavigationPaintHold(paintHold)
|
|
5613
|
+
await releaseNavigationPaintHold(paintHold, sequence, () => {
|
|
5614
|
+
if (!url.hash && !stylesheetReconciler && sequence === navigationSequence)
|
|
5615
|
+
pruneStylesheets(doc)
|
|
5616
|
+
})
|
|
5518
5617
|
}
|
|
5519
5618
|
if (sequence !== navigationSequence) return
|
|
5520
5619
|
// Mounting/location subscribers run after the initial pre-mount scroll action.
|
|
@@ -5547,6 +5646,19 @@ export async function softNavigate(href: string, options: SoftNavigateOptions =
|
|
|
5547
5646
|
if (!options.pop && departingBfcacheId && departingBfcacheId === historyBfcacheId()) {
|
|
5548
5647
|
restoreFormStateWhenMounted(departingBfcacheId, sequence, true)
|
|
5549
5648
|
}
|
|
5649
|
+
if (typeof __PNEXT_NEXT_ROUTER__ === 'undefined' && keptCopies.has(page))
|
|
5650
|
+
void revalidateCommittedPage(targetUrl, sequence, page)
|
|
5651
|
+
}
|
|
5652
|
+
|
|
5653
|
+
async function revalidateCommittedPage(url: URL, sequence: number, page: PrefetchedPage) {
|
|
5654
|
+
const fresh = await keptCopies.get(page)
|
|
5655
|
+
if (!fresh?.ok || fresh.html === page.html || sequence !== navigationSequence) return
|
|
5656
|
+
await softNavigate(url.href, {
|
|
5657
|
+
replace: true,
|
|
5658
|
+
scroll: false,
|
|
5659
|
+
refreshLike: true,
|
|
5660
|
+
cachedPage: fresh,
|
|
5661
|
+
})
|
|
5550
5662
|
}
|
|
5551
5663
|
|
|
5552
5664
|
function isBotUserAgent() {
|
|
@@ -6103,5 +6215,7 @@ export function installRouterFull() {
|
|
|
6103
6215
|
)
|
|
6104
6216
|
window.addEventListener('popstate', onPopState)
|
|
6105
6217
|
window.addEventListener('pageshow', onPageShow)
|
|
6218
|
+
if (typeof __PNEXT_NEXT_ROUTER__ === 'undefined' && !navigationScrollAction)
|
|
6219
|
+
setNavigationScrollAction(coreNavigationScroll)
|
|
6106
6220
|
watchEagerPrefetchLinks()
|
|
6107
6221
|
}
|
|
@@ -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
|
|
@@ -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) {
|
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
|