@wular/pnext 0.1.4 → 0.1.6
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/config/ts/react.json +2 -1
- package/package.json +1 -1
- package/reference/navigation.md +3 -1
- package/src/api/client-navigation.ts +53 -4
- package/src/api/link.tsx +6 -7
- package/src/cli/adapters/vercel-warm.ts +6 -1
- package/src/cli/adapters/vercel.ts +1 -1
- package/src/cli/build/cache.ts +8 -3
- package/src/cli/build.ts +5 -12
- package/src/cli/index.ts +1 -8
- package/src/cli/serve/entry.ts +16 -161
- package/src/cli/serve/pipeline.ts +11 -3
- package/src/cli/start.ts +1 -1
- package/src/cli/typegen.ts +65 -3
- package/src/client/build.ts +10 -7
- package/src/client/entry.ts +50 -29
- package/src/client/reference-stub.ts +1 -1
- package/src/client/router/hub.ts +13 -11
- package/src/client/router/page-slot.ts +58 -0
- package/src/client/router/runtime.ts +377 -388
- package/src/compat/bundler/symlink-imports.ts +1 -1
- package/src/compat/client/errors/bare-boundary.ts +1 -1
- package/src/compat/client/errors/error-boundary.ts +1 -1
- package/src/compat/client/errors/global-error.ts +40 -79
- package/src/compat/client/errors/install.ts +1 -1
- package/src/compat/client/errors/lazy.ts +7 -0
- package/src/compat/client/errors/primitive-throw.ts +36 -53
- package/src/compat/client/segment-cache-policy.ts +5 -22
- package/src/compat/client/segment-cache.ts +7 -26
- package/src/compat/css/modules.ts +1 -7
- package/src/compat/css/sass.ts +1 -7
- package/src/compat/react/parity.ts +1 -1
- package/src/compat/register/bundler.ts +9 -1
- package/src/compat/tsconfig-defaults.ts +4 -7
- package/src/compat-bootstrap.ts +15 -0
- package/src/css/build.ts +18 -15
- package/src/dev/server.ts +2 -2
- package/src/index.ts +2 -0
- package/src/render/renderer.ts +209 -24
- package/src/render/resource-hints.ts +4 -1
- package/src/resolve/imports.ts +13 -8
- package/src/routing/routes.ts +32 -18
- package/src/runtime/loader.ts +29 -10
- package/src/runtime/modules.ts +28 -7
- package/src/runtime/vendor-build.ts +2 -2
- package/src/types.ts +2 -0
- package/src/utils/fs.ts +122 -0
package/src/client/build.ts
CHANGED
|
@@ -1532,7 +1532,8 @@ function createClientSourcePipeline(config: ResolvedConfig, dev = false, routeId
|
|
|
1532
1532
|
contents = rewriteDeferredDynamicImports(
|
|
1533
1533
|
contents,
|
|
1534
1534
|
resolved,
|
|
1535
|
-
specifier =>
|
|
1535
|
+
specifier =>
|
|
1536
|
+
resolveImport(rootFromFile(resolved), resolved, specifier, config.workspaceRoot),
|
|
1536
1537
|
target => {
|
|
1537
1538
|
const id = clientReferenceId(target.file, target.exportName)
|
|
1538
1539
|
const ref = { ...target, routeId }
|
|
@@ -2089,7 +2090,9 @@ export async function clientCacheKeyParts(
|
|
|
2089
2090
|
): Promise<ClientCacheKeyParts> {
|
|
2090
2091
|
const staticDigest = staticHash ?? clientCacheStaticHash(route, nextCompat, config)
|
|
2091
2092
|
const sources = await Promise.all(
|
|
2092
|
-
(await clientSourceFiles(route)).map(file =>
|
|
2093
|
+
(await clientSourceFiles(route, config?.workspaceRoot)).map(file =>
|
|
2094
|
+
readClientCacheSource(file),
|
|
2095
|
+
),
|
|
2093
2096
|
)
|
|
2094
2097
|
return { key: clientCacheKeyFrom(staticDigest, sources), staticHash: staticDigest, sources }
|
|
2095
2098
|
}
|
|
@@ -2102,14 +2105,14 @@ export async function clientCacheKey(
|
|
|
2102
2105
|
return (await clientCacheKeyParts(route, nextCompat, config)).key
|
|
2103
2106
|
}
|
|
2104
2107
|
|
|
2105
|
-
async function clientSourceFiles(route: RouteManifestEntry) {
|
|
2108
|
+
async function clientSourceFiles(route: RouteManifestEntry, workspaceRoot?: string) {
|
|
2106
2109
|
const entryFiles = [
|
|
2107
2110
|
...(route.client ? [route.file] : []),
|
|
2108
2111
|
...route.clientReferences.map(reference => reference.file),
|
|
2109
2112
|
]
|
|
2110
2113
|
const files = new Set<string>()
|
|
2111
2114
|
|
|
2112
|
-
await Promise.all(entryFiles.map(file => collectClientSourceFile(file, files)))
|
|
2115
|
+
await Promise.all(entryFiles.map(file => collectClientSourceFile(file, files, workspaceRoot)))
|
|
2113
2116
|
|
|
2114
2117
|
return [...files].sort()
|
|
2115
2118
|
}
|
|
@@ -2117,7 +2120,7 @@ async function clientSourceFiles(route: RouteManifestEntry) {
|
|
|
2117
2120
|
// The walk fans out: every file joins `files` before its own read starts, so
|
|
2118
2121
|
// concurrent branches still visit each file once. Serially this was the whole
|
|
2119
2122
|
// cost of a route's client cache key (~1.8 s on a 300-module route).
|
|
2120
|
-
async function collectClientSourceFile(file: string, files: Set<string
|
|
2123
|
+
async function collectClientSourceFile(file: string, files: Set<string>, workspaceRoot?: string) {
|
|
2121
2124
|
if (files.has(file) || !existsSync(file)) return
|
|
2122
2125
|
files.add(file)
|
|
2123
2126
|
if (isAssetLike(file)) return
|
|
@@ -2125,8 +2128,8 @@ async function collectClientSourceFile(file: string, files: Set<string>) {
|
|
|
2125
2128
|
const source = rewriteLiteralDynamicCalls(await readText(file), file)
|
|
2126
2129
|
await Promise.all(
|
|
2127
2130
|
[...localImports(file, source)].map(specifier => {
|
|
2128
|
-
const resolved = resolveImport(rootFromFile(file), file, specifier)
|
|
2129
|
-
return resolved ? collectClientSourceFile(resolved, files) : Promise.resolve()
|
|
2131
|
+
const resolved = resolveImport(rootFromFile(file), file, specifier, workspaceRoot)
|
|
2132
|
+
return resolved ? collectClientSourceFile(resolved, files, workspaceRoot) : Promise.resolve()
|
|
2130
2133
|
}),
|
|
2131
2134
|
)
|
|
2132
2135
|
}
|
package/src/client/entry.ts
CHANGED
|
@@ -104,10 +104,23 @@ function serializeModulePath() {
|
|
|
104
104
|
// Re-provide the per-island layout-segment snapshot (stamped by the renderer as
|
|
105
105
|
// `data-pnext-layout-segments`) around a hydrated island so compat's
|
|
106
106
|
// useSelectedLayoutSegment(s) recomputes live from the layout depth. Emitted
|
|
107
|
-
// only under next compat (the hooks are compat-only);
|
|
107
|
+
// only under next compat (the hooks are compat-only); core keeps the children guard.
|
|
108
108
|
function layoutSegmentHelperSource(nextCompat?: boolean) {
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
// useId's mask (preact's _mask), seeded by data-pnext-id and kept on the instance (__c) across re-adoption.
|
|
110
|
+
const seed = `
|
|
111
|
+
function pnextIslandMask(vnode, root) {
|
|
112
|
+
const mask = root.__pnextMask ??= [root.getAttribute('data-pnext-id'), 0];
|
|
113
|
+
Object.defineProperty(vnode, '__m', { enumerable: true, get() { return (this.__c || this).__pnextMask ??= mask; } });
|
|
114
|
+
}`
|
|
115
|
+
if (!nextCompat)
|
|
116
|
+
return `${seed}
|
|
117
|
+
function pnextIslandVNode(h, Component, props, children, root) {
|
|
118
|
+
const vnode = children !== undefined ? h(Component, props, children) : h(Component, props);
|
|
119
|
+
pnextIslandMask(vnode, root);
|
|
120
|
+
return vnode;
|
|
121
|
+
}
|
|
122
|
+
`
|
|
123
|
+
return `${seed}
|
|
111
124
|
import { LayoutSegmentContext as __PNextLayoutSegmentContext, RouteParamsContext as __PNextRouteParamsContext } from ${JSON.stringify(islandContextModulePath())};
|
|
112
125
|
function pnextIslandReadJson(root, attr) {
|
|
113
126
|
const raw = root && root.getAttribute ? root.getAttribute(attr) : null;
|
|
@@ -118,6 +131,7 @@ function pnextIslandVNode(h, Component, props, children, root) {
|
|
|
118
131
|
// An explicit undefined children argument clobbers props.children in
|
|
119
132
|
// preact's createElement; plain children travel inside the parsed props.
|
|
120
133
|
let vnode = children !== undefined ? h(Component, props, children) : h(Component, props);
|
|
134
|
+
pnextIslandMask(vnode, root);
|
|
121
135
|
const segments = pnextIslandReadJson(root, 'data-pnext-layout-segments');
|
|
122
136
|
if (segments) vnode = h(__PNextLayoutSegmentContext.Provider, { value: segments }, vnode);
|
|
123
137
|
const params = pnextIslandReadJson(root, 'data-pnext-params');
|
|
@@ -132,11 +146,8 @@ function islandVNodeExpr(
|
|
|
132
146
|
propsExpr: string,
|
|
133
147
|
childrenExpr: string,
|
|
134
148
|
rootExpr: string,
|
|
135
|
-
nextCompat?: boolean,
|
|
136
149
|
) {
|
|
137
|
-
return
|
|
138
|
-
? `pnextIslandVNode(h, ${componentExpr}, ${propsExpr}, ${childrenExpr}, ${rootExpr})`
|
|
139
|
-
: `h(${componentExpr}, ${propsExpr}, ${childrenExpr})`
|
|
150
|
+
return `pnextIslandVNode(h, ${componentExpr}, ${propsExpr}, ${childrenExpr}, ${rootExpr})`
|
|
140
151
|
}
|
|
141
152
|
|
|
142
153
|
/**
|
|
@@ -1136,11 +1147,10 @@ function observeVisibleIsland(root, island) {
|
|
|
1136
1147
|
}
|
|
1137
1148
|
|
|
1138
1149
|
function visibleTarget(root) {
|
|
1139
|
-
//
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
return root.parentElement ?? root;
|
|
1150
|
+
// IntersectionObserver never fires for a display:contents target: observe the nearest box.
|
|
1151
|
+
let target = root;
|
|
1152
|
+
while (target.parentElement && getComputedStyle(target).display === 'contents') target = target.parentElement;
|
|
1153
|
+
return target;
|
|
1144
1154
|
}
|
|
1145
1155
|
|
|
1146
1156
|
${mountOnceSource()}
|
|
@@ -1159,7 +1169,8 @@ async function mountIslandTree(root, island) {
|
|
|
1159
1169
|
]);
|
|
1160
1170
|
${facts.suspense !== false ? ' islandBoundary = Suspense;\n' : ''} const rawProps = root.getAttribute('data-pnext-props') ?? '{}';
|
|
1161
1171
|
const source = preservedSource(root, render);
|
|
1162
|
-
|
|
1172
|
+
if (!source) return adoptPreserved(render, root);
|
|
1173
|
+
const vnode = islandVNode(h, Component, await islandProps(rawProps, source, node => domChildren(h, node), h), await staticChildren(h, source, island.id), root);
|
|
1163
1174
|
const wrapped = ${facts.suspense !== false ? 'h(Suspense, { fallback: null }, pnextClientBoundary(h, vnode))' : 'pnextClientBoundary(h, vnode)'};
|
|
1164
1175
|
if (source !== root) adoptPreserved(render, root, wrapped);
|
|
1165
1176
|
else mount(hydrate, render, root, wrapped);
|
|
@@ -1172,7 +1183,8 @@ async function mountIslandTree(root, island) {
|
|
|
1172
1183
|
const [{ h, hydrate, render }, Component] = await Promise.all([import('preact'), islandComponent(island)]);
|
|
1173
1184
|
const rawProps = root.getAttribute('data-pnext-props') ?? '{}';
|
|
1174
1185
|
const source = preservedSource(root, render);
|
|
1175
|
-
|
|
1186
|
+
if (!source) return adoptPreserved(render, root);
|
|
1187
|
+
const vnode = islandVNode(h, Component, await islandProps(rawProps, source, node => domChildren(h, node), h), await staticChildren(h, source, island.id), root);
|
|
1176
1188
|
if (source !== root) adoptPreserved(render, root, vnode);
|
|
1177
1189
|
else mount(hydrate, render, root, vnode);
|
|
1178
1190
|
}`
|
|
@@ -1200,16 +1212,18 @@ function preservedSource(root, render) {
|
|
|
1200
1212
|
const incoming = root.__pnextIncoming;
|
|
1201
1213
|
root.__pnextIncoming = undefined;
|
|
1202
1214
|
if (!incoming) return root;
|
|
1203
|
-
|
|
1215
|
+
// A kept layout's island stashed as its own incoming has no new render: keep its tree.
|
|
1216
|
+
if (root.__pnextLive === render) return incoming === root ? null : incoming;
|
|
1204
1217
|
root.replaceChildren(...incoming.childNodes);
|
|
1205
1218
|
root.__pnextLive = undefined;
|
|
1219
|
+
root.__pnextMask = [incoming.getAttribute('data-pnext-id'), 0];
|
|
1206
1220
|
return root;
|
|
1207
1221
|
}
|
|
1208
1222
|
|
|
1209
1223
|
// Re-render the preserved root in place so component state survives while the
|
|
1210
1224
|
// routed content under the island updates.
|
|
1211
1225
|
function adoptPreserved(render, root, vnode) {
|
|
1212
|
-
render(vnode, root);
|
|
1226
|
+
if (vnode) render(vnode, root);
|
|
1213
1227
|
pnextRender = render;
|
|
1214
1228
|
pnextMountedRoots.add(root);
|
|
1215
1229
|
}
|
|
@@ -1230,8 +1244,10 @@ function mount(hydrate, render, root, vnode) {
|
|
|
1230
1244
|
// Adopted DOM children win (element children); plain children travel inside
|
|
1231
1245
|
// the serialized props instead, and an explicit undefined third argument would
|
|
1232
1246
|
// clobber props.children in preact's createElement — so only pass it when set.
|
|
1233
|
-
function islandVNode(h, Component, props, adopted) {
|
|
1234
|
-
|
|
1247
|
+
function islandVNode(h, Component, props, adopted, root) {
|
|
1248
|
+
const vnode = adopted !== undefined ? h(Component, props, adopted) : h(Component, props);
|
|
1249
|
+
pnextIslandMask(vnode, root);
|
|
1250
|
+
return vnode;
|
|
1235
1251
|
}
|
|
1236
1252
|
|
|
1237
1253
|
async function staticChildren(h, root, id) {
|
|
@@ -1278,7 +1294,7 @@ async function domNode(h, node) {
|
|
|
1278
1294
|
// A nested island must match its previous incarnation by IDENTITY, or the shift remounts
|
|
1279
1295
|
// it and resets its state - Next never remounts layout components on navigation.
|
|
1280
1296
|
if (props.key === undefined) props.key = 'pnext-island:' + id;
|
|
1281
|
-
const vnode = islandVNode(h, Component, props, await staticChildren(h, element, island.id));
|
|
1297
|
+
const vnode = islandVNode(h, Component, props, await staticChildren(h, element, island.id), element);
|
|
1282
1298
|
return ${nextCompat ? 'islandBoundary ? h(islandBoundary, { fallback: null }, pnextClientBoundary(h, vnode)) : pnextClientBoundary(h, vnode)' : 'vnode'};
|
|
1283
1299
|
}
|
|
1284
1300
|
|
|
@@ -1531,7 +1547,7 @@ function diagnoseSkippedIslands() {
|
|
|
1531
1547
|
// claim pass adopted is no longer in the document. Everything else is a host
|
|
1532
1548
|
// this route can never hydrate.
|
|
1533
1549
|
if (island?.options?.load === 'visible') continue;
|
|
1534
|
-
if (root.closest('pnext-hole-capture')) continue;
|
|
1550
|
+
if (root.closest('pnext-hole-capture, [data-pnext-stream]')) continue;
|
|
1535
1551
|
if (parent && !parent.__pnextLive) continue;
|
|
1536
1552
|
const reason = island
|
|
1537
1553
|
? 'was materialized after its parent island mounted, outside any streamed hole this route can re-adopt'
|
|
@@ -1562,6 +1578,8 @@ for (const island of islands) {
|
|
|
1562
1578
|
}
|
|
1563
1579
|
for (const island of islands) {
|
|
1564
1580
|
for (const root of document.querySelectorAll(\`[data-pnext-client="\${island.id}"]\`)) {
|
|
1581
|
+
// React hydrates a streamed boundary only once revealed; its promotion re-runs this scan.
|
|
1582
|
+
if (root.closest('[data-pnext-stream]')) continue;
|
|
1565
1583
|
const parent = nestedIslandParent(root);
|
|
1566
1584
|
if (parent) {
|
|
1567
1585
|
pending.push(claimNestedIslandRoot(root, parent));
|
|
@@ -1625,8 +1643,11 @@ async function mountIslandTree(root, island) {
|
|
|
1625
1643
|
// Preserved across a soft navigation: re-render in place with the incoming
|
|
1626
1644
|
// document's props/children so component state survives while the routed
|
|
1627
1645
|
// content under the island updates.
|
|
1628
|
-
|
|
1629
|
-
|
|
1646
|
+
// A kept layout's island (its own incoming) keeps its tree.
|
|
1647
|
+
if (incoming !== root) {
|
|
1648
|
+
const vnode = ${islandVNodeExpr('Component', 'await islandProps(rawProps, incoming, domChildren, h)', 'await staticChildren(incoming, island.id)', 'root')};
|
|
1649
|
+
render(${wrapInBoundary('vnode', nextCompat)}, root);
|
|
1650
|
+
}
|
|
1630
1651
|
pnextMountedRoots.add(root);
|
|
1631
1652
|
return;
|
|
1632
1653
|
}
|
|
@@ -1635,8 +1656,9 @@ async function mountIslandTree(root, island) {
|
|
|
1635
1656
|
// instance can't diff its tree — restore the SSR children and mount fresh.
|
|
1636
1657
|
root.replaceChildren(...incoming.childNodes);
|
|
1637
1658
|
root.__pnextLive = undefined;
|
|
1659
|
+
root.__pnextMask = [incoming.getAttribute('data-pnext-id'), 0];
|
|
1638
1660
|
}
|
|
1639
|
-
const vnode = ${islandVNodeExpr('Component', 'await islandProps(rawProps, root, domChildren, h)', 'await staticChildren(root, island.id)', 'root'
|
|
1661
|
+
const vnode = ${islandVNodeExpr('Component', 'await islandProps(rawProps, root, domChildren, h)', 'await staticChildren(root, island.id)', 'root')};
|
|
1640
1662
|
mount(root, ${wrapInBoundary('vnode', nextCompat)});
|
|
1641
1663
|
}
|
|
1642
1664
|
|
|
@@ -1713,7 +1735,7 @@ async function domNode(node) {
|
|
|
1713
1735
|
: ''
|
|
1714
1736
|
}
|
|
1715
1737
|
const Component = await islandComponent(island);
|
|
1716
|
-
return ${wrapInBoundary(islandVNodeExpr('Component', 'await islandProps(rawProps, element, domChildren, h)', 'children', 'element'
|
|
1738
|
+
return ${wrapInBoundary(islandVNodeExpr('Component', 'await islandProps(rawProps, element, domChildren, h)', 'children', 'element'), nextCompat)};
|
|
1717
1739
|
}
|
|
1718
1740
|
|
|
1719
1741
|
if (Page && element.id === 'pnext-page') {
|
|
@@ -1755,11 +1777,10 @@ ${holeSeamSource()}${
|
|
|
1755
1777
|
visibleIslands
|
|
1756
1778
|
? `
|
|
1757
1779
|
function visibleTarget(root) {
|
|
1758
|
-
//
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
return root.parentElement ?? root;
|
|
1780
|
+
// IntersectionObserver never fires for a display:contents target: observe the nearest box.
|
|
1781
|
+
let target = root;
|
|
1782
|
+
while (target.parentElement && getComputedStyle(target).display === 'contents') target = target.parentElement;
|
|
1783
|
+
return target;
|
|
1763
1784
|
}
|
|
1764
1785
|
`
|
|
1765
1786
|
: ''
|
|
@@ -86,7 +86,7 @@ export async function clientReferenceExportNames(
|
|
|
86
86
|
// excluded, per ES module star semantics). `export * as ns from` exports
|
|
87
87
|
// only `ns`, which the own-module scan already lists.
|
|
88
88
|
for (const specifier of moduleExportStars(text, file)) {
|
|
89
|
-
const resolved = resolveImport(config.root, file, specifier)
|
|
89
|
+
const resolved = resolveImport(config.root, file, specifier, config.workspaceRoot)
|
|
90
90
|
if (!resolved || !isInside(config.workspaceRoot, resolved)) continue
|
|
91
91
|
for (const name of await clientReferenceExportNames(config, resolved, undefined, visited)) {
|
|
92
92
|
if (name !== 'default') names.add(name)
|
package/src/client/router/hub.ts
CHANGED
|
@@ -16,28 +16,30 @@ import type { EntryModule } from './types'
|
|
|
16
16
|
|
|
17
17
|
export const routerState: {
|
|
18
18
|
observedLocationKey: string
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// The route key (pathname+search) of the currently committed navigation, used
|
|
20
|
+
// as the departing key on popstate (where `location` already points at the
|
|
21
|
+
// target). Seeded on router init and updated at each committed navigation.
|
|
22
|
+
activeRouteKey?: string
|
|
23
|
+
// The entry id whose document is currently on screen. A traversal to that
|
|
24
|
+
// same id — an app-level shallow pushState now carries the id forward — needs
|
|
25
|
+
// no fetch and no swap: the DOM already is this entry's.
|
|
26
|
+
renderedEntryId?: string
|
|
21
27
|
/** The navigation that last painted a fallback, and when. */
|
|
22
28
|
fallbackPaint?: [sequence: number, at: number]
|
|
29
|
+
/** Ends the clicked link's pending state; set once the runtime installs. */
|
|
30
|
+
settleLink?: () => void
|
|
23
31
|
} = {
|
|
24
32
|
// pathname+search of the URL the router last observed. A popstate that leaves both unchanged is a
|
|
25
33
|
// same-document FRAGMENT traversal - Chrome fires popstate, not just hashchange, for those - and
|
|
26
34
|
// treating one as a history traversal would refetch and swap the body, remounting the live tree
|
|
27
35
|
// under the user's feet.
|
|
28
36
|
observedLocationKey: locationKey(),
|
|
29
|
-
// The route key (pathname+search) of the currently committed navigation, used
|
|
30
|
-
// as the departing key on popstate (where `location` already points at the
|
|
31
|
-
// target). Seeded on router init and updated at each committed navigation.
|
|
32
|
-
activeRouteKey: undefined as string | undefined,
|
|
33
|
-
// The entry id whose document is currently on screen. A traversal to that
|
|
34
|
-
// same id — an app-level shallow pushState now carries the id forward — needs
|
|
35
|
-
// no fetch and no swap: the DOM already is this entry's.
|
|
36
|
-
renderedEntryId: undefined as string | undefined,
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export function locationKey() {
|
|
40
|
-
return typeof location
|
|
40
|
+
return process.browser || typeof location !== 'undefined'
|
|
41
|
+
? location.pathname + location.search
|
|
42
|
+
: ''
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
// Installers that only the navigation runtime ever observes — compat's policy
|
|
@@ -27,6 +27,64 @@ export function elementInPageSlot(element: Element): boolean {
|
|
|
27
27
|
)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
const SEGMENT = 'pnext-layout[data-pnext-segment]'
|
|
31
|
+
|
|
32
|
+
/** A server layout's identity and the URL prefix it rendered for. */
|
|
33
|
+
export function segmentKey(segment: Element): string {
|
|
34
|
+
return `${segment.getAttribute('data-pnext-segment')}\u0001${segment.getAttribute('data-pnext-scope')}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The server layout directly below `segment`, outside parallel-route slots. */
|
|
38
|
+
export function childSegment(segment: Element): Element | null {
|
|
39
|
+
for (const child of segment.querySelectorAll(SEGMENT))
|
|
40
|
+
if (!segment.contains(child.closest('[data-pnext-slot]'))) return child
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The live layout below the deepest layout `boundary` shares with `slot` (null when that layout
|
|
46
|
+
* holds `slot`), and the incoming node that replaces it: the boundary, or the new layout holding
|
|
47
|
+
* it. Null when either sits in an island.
|
|
48
|
+
*/
|
|
49
|
+
export function divergingLayout(slot: Node, boundary: Element): [Element | null, Element] | null {
|
|
50
|
+
let paint = boundary
|
|
51
|
+
let owner = boundary.closest(SEGMENT)
|
|
52
|
+
let below: Element | null
|
|
53
|
+
for (;;) {
|
|
54
|
+
const key = owner && segmentKey(owner)
|
|
55
|
+
below = null
|
|
56
|
+
let layout = slot.parentElement?.closest(SEGMENT)
|
|
57
|
+
for (; layout && segmentKey(layout) !== key; layout = layout.parentElement?.closest(SEGMENT))
|
|
58
|
+
below = layout
|
|
59
|
+
if (layout || !owner) break
|
|
60
|
+
paint = owner
|
|
61
|
+
owner = owner.parentElement!.closest(SEGMENT)
|
|
62
|
+
}
|
|
63
|
+
return !boundary.closest('pnext-client') &&
|
|
64
|
+
!(below ?? slot).parentElement?.closest('pnext-client')
|
|
65
|
+
? [below, paint]
|
|
66
|
+
: null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function childNodes(segment: Element): ChildNode[] | null {
|
|
70
|
+
const child = childSegment(segment)
|
|
71
|
+
if (child) return [child]
|
|
72
|
+
const range = pageSlotRange(segment)
|
|
73
|
+
return range && (range[1] ? [range[0], ...range[2], range[1]] : [range[0]])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Replace a kept layout's children (its child layout or page slot) with the incoming ones. */
|
|
77
|
+
export function graftSegmentChildren(liveSegment: Element, incomingSegment: Element): boolean {
|
|
78
|
+
if (!childSegment(liveSegment) && !childSegment(incomingSegment))
|
|
79
|
+
return graftPageSlot(liveSegment, incomingSegment)
|
|
80
|
+
const live = childNodes(liveSegment)
|
|
81
|
+
const incoming = childNodes(incomingSegment)
|
|
82
|
+
if (!live || !incoming) return false
|
|
83
|
+
live[0]!.before(...incoming)
|
|
84
|
+
for (const stale of live) stale.remove()
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
30
88
|
/**
|
|
31
89
|
* Graft a destination page into a preserved layout without changing the destination's mount shape.
|
|
32
90
|
* Client pages need the real `#pnext-page` element; server/island-only pages need the marker range.
|