@pracht/core 0.8.0 → 0.9.0
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/README.md +5 -1
- package/dist/app-BN3pjxtw.d.mts +24 -0
- package/dist/{app-BvC1uQG5.mjs → app-CGRKjZ6Z.mjs} +97 -8
- package/dist/app-graph-BlaCcGUZ.mjs +52 -0
- package/dist/app-graph-DBoDsHJ5.d.mts +38 -0
- package/dist/browser.d.mts +7 -6
- package/dist/browser.mjs +8 -7
- package/dist/client.d.mts +3 -3
- package/dist/client.mjs +3 -3
- package/dist/dev-404.d.mts +26 -0
- package/dist/dev-404.mjs +166 -0
- package/dist/devtools.d.mts +8 -0
- package/dist/devtools.mjs +156 -0
- package/dist/error-overlay.d.mts +38 -1
- package/dist/error-overlay.mjs +134 -5
- package/dist/index.d.mts +9 -7
- package/dist/index.mjs +9 -7
- package/dist/manifest.d.mts +2 -1
- package/dist/manifest.mjs +1 -1
- package/dist/{runtime-context-B5pREhcM.mjs → navigation-state-BjwXTeVz.mjs} +74 -2
- package/dist/{prefetch-B50kX9RL.mjs → prefetch-COYeuvQK.mjs} +44 -27
- package/dist/prefetch-api-Bf-P7XFo.d.mts +39 -0
- package/dist/prefetch-api-ChSCTbEB.mjs +69 -0
- package/dist/{prefetch-cache-DzP2Bj9H.mjs → prefetch-cache-BNLEp9H5.mjs} +12 -1
- package/dist/{prerender-B8_CqYwd.d.mts → prerender-BV42B9jK.d.mts} +31 -2
- package/dist/{prerender-D3E2H96p.mjs → prerender-C8BpANz3.mjs} +57 -15
- package/dist/{router-BcUmg1kB.d.mts → router-B_Aj5TtY.d.mts} +2 -2
- package/dist/router-E5Wh-dOR.mjs +694 -0
- package/dist/{runtime-context-ytVd7GmZ.d.mts → runtime-context-BYBwVA0M.d.mts} +1 -1
- package/dist/{runtime-middleware-aeBdgjYr.d.mts → runtime-middleware-pYubngnL.d.mts} +57 -4
- package/dist/server.d.mts +7 -5
- package/dist/server.mjs +6 -5
- package/dist/{app-BWriseLj.d.mts → types-B-sfVjaH.d.mts} +42 -22
- package/dist/{types-idmK5omD.mjs → types-DEPXdbyp.mjs} +42 -21
- package/package.json +9 -1
- package/dist/hydration-M1QzbQ1O.d.mts +0 -23
- package/dist/router-B7J4YYwg.mjs +0 -418
- /package/dist/{forwardRef-grZ6t4GS.mjs → forwardRef-B0cSkHwH.mjs} +0 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
import { o as matchAppRoute, t as buildHref } from "./app-CGRKjZ6Z.mjs";
|
|
2
|
+
import { c as fetchPrachtRouteState, d as routeNeedsServerFetch, i as getCachedRouteState, u as parseSafeNavigationUrl } from "./prefetch-cache-BNLEp9H5.mjs";
|
|
3
|
+
import { a as settleNavigation, p as deserializeRouteError, r as createNavigationLocation, s as PrachtRuntimeProvider, t as beginLoadingNavigation } from "./navigation-state-BjwXTeVz.mjs";
|
|
4
|
+
import { r as registerPrefetchTarget } from "./prefetch-api-ChSCTbEB.mjs";
|
|
5
|
+
import { createContext, h, hydrate, options, render } from "preact";
|
|
6
|
+
import { useContext, useEffect, useLayoutEffect, useMemo, useState } from "preact/hooks";
|
|
7
|
+
import { Suspense } from "preact-suspense";
|
|
8
|
+
//#region src/hydration.ts
|
|
9
|
+
const MODE_HYDRATE$1 = 32;
|
|
10
|
+
let _hydrating = false;
|
|
11
|
+
let _suspensionCount = 0;
|
|
12
|
+
let _hydrated = false;
|
|
13
|
+
new Suspense({});
|
|
14
|
+
const oldCatchError = options.__e;
|
|
15
|
+
options.__e = (err, newVNode, oldVNode, errorInfo) => {
|
|
16
|
+
if (_hydrating && !_hydrated && err && err.then) {
|
|
17
|
+
if (!!(newVNode && newVNode.__u && newVNode.__u & MODE_HYDRATE$1) || !!(newVNode && newVNode.__h)) {
|
|
18
|
+
_suspensionCount++;
|
|
19
|
+
let settled = false;
|
|
20
|
+
const onSettled = () => {
|
|
21
|
+
if (settled) return;
|
|
22
|
+
settled = true;
|
|
23
|
+
_suspensionCount--;
|
|
24
|
+
};
|
|
25
|
+
err.then(onSettled, onSettled);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (oldCatchError) oldCatchError(err, newVNode, oldVNode, errorInfo);
|
|
29
|
+
};
|
|
30
|
+
const oldCommit = options.__c;
|
|
31
|
+
options.__c = (vnode, commitQueue) => {
|
|
32
|
+
if (_hydrating && !_hydrated && _suspensionCount <= 0) {
|
|
33
|
+
_hydrated = true;
|
|
34
|
+
_hydrating = false;
|
|
35
|
+
}
|
|
36
|
+
if (oldCommit) oldCommit(vnode, commitQueue);
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Mark the start of a hydration pass. Call this right before `hydrate()`.
|
|
40
|
+
*/
|
|
41
|
+
function markHydrating() {
|
|
42
|
+
if (!_hydrated) _hydrating = true;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns `true` once the initial hydration (including all Suspense
|
|
46
|
+
* boundaries) has fully resolved. During SSR and hydration this returns
|
|
47
|
+
* `false`.
|
|
48
|
+
*/
|
|
49
|
+
function useIsHydrated() {
|
|
50
|
+
const [hydrated, setHydrated] = useState(_hydrated);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
setHydrated(true);
|
|
53
|
+
}, []);
|
|
54
|
+
return hydrated;
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/hydration-mismatch.ts
|
|
58
|
+
const HYDRATION_BANNER_ID = "__pracht_hydration_mismatch__";
|
|
59
|
+
const MODE_HYDRATE = 32;
|
|
60
|
+
let installed = false;
|
|
61
|
+
let prevMismatch;
|
|
62
|
+
let prevCatchError;
|
|
63
|
+
let prevCommit;
|
|
64
|
+
const pendingSuspenseChecks = /* @__PURE__ */ new Set();
|
|
65
|
+
let flushScheduled = false;
|
|
66
|
+
function installHydrationMismatchWarning() {
|
|
67
|
+
if (installed) return;
|
|
68
|
+
installed = true;
|
|
69
|
+
const opts = options;
|
|
70
|
+
prevMismatch = opts.__m;
|
|
71
|
+
prevCatchError = opts.__e;
|
|
72
|
+
prevCommit = opts.__c;
|
|
73
|
+
opts.__m = function(vnode) {
|
|
74
|
+
appendHydrationWarning(vnode);
|
|
75
|
+
if (prevMismatch) prevMismatch(vnode);
|
|
76
|
+
};
|
|
77
|
+
opts.__e = function(err, newVNode, oldVNode, errorInfo) {
|
|
78
|
+
trackSuspendingVNode(err, newVNode);
|
|
79
|
+
if (prevCatchError) prevCatchError(err, newVNode, oldVNode, errorInfo);
|
|
80
|
+
};
|
|
81
|
+
opts.__c = function(vnode, commitQueue) {
|
|
82
|
+
if (prevCommit) prevCommit(vnode, commitQueue);
|
|
83
|
+
scheduleSuspenseCheckFlush();
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function trackSuspendingVNode(err, vnode) {
|
|
87
|
+
if (!vnode) return;
|
|
88
|
+
if (!err || typeof err.then !== "function") return;
|
|
89
|
+
if (!!!(vnode.__u && vnode.__u & MODE_HYDRATE || vnode.__h)) return;
|
|
90
|
+
const promise = err;
|
|
91
|
+
const onSettle = () => {
|
|
92
|
+
pendingSuspenseChecks.add(vnode);
|
|
93
|
+
};
|
|
94
|
+
promise.then(onSettle, onSettle);
|
|
95
|
+
}
|
|
96
|
+
function scheduleSuspenseCheckFlush() {
|
|
97
|
+
if (flushScheduled) return;
|
|
98
|
+
if (pendingSuspenseChecks.size === 0) return;
|
|
99
|
+
flushScheduled = true;
|
|
100
|
+
queueMicrotask(flushSuspenseChecks);
|
|
101
|
+
}
|
|
102
|
+
function flushSuspenseChecks() {
|
|
103
|
+
flushScheduled = false;
|
|
104
|
+
if (pendingSuspenseChecks.size === 0) return;
|
|
105
|
+
const checks = Array.from(pendingSuspenseChecks);
|
|
106
|
+
pendingSuspenseChecks.clear();
|
|
107
|
+
for (const vnode of checks) {
|
|
108
|
+
const rendered = currentVNode(vnode);
|
|
109
|
+
const count = countTopLevelDomNodes(rendered);
|
|
110
|
+
if (count !== 1) appendSuspenseOffsetWarning(pickReportableVNode(rendered), count);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function currentVNode(vnode) {
|
|
114
|
+
return vnode.__c?.__v ?? vnode;
|
|
115
|
+
}
|
|
116
|
+
function pickReportableVNode(vnode) {
|
|
117
|
+
let current = vnode;
|
|
118
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
119
|
+
if (!isLazyWrapperVNode(current)) break;
|
|
120
|
+
const children = current.__k;
|
|
121
|
+
if (!Array.isArray(children) || children.length !== 1) break;
|
|
122
|
+
const child = children[0];
|
|
123
|
+
if (!child || typeof child.type !== "function") break;
|
|
124
|
+
current = currentVNode(child);
|
|
125
|
+
}
|
|
126
|
+
return current;
|
|
127
|
+
}
|
|
128
|
+
function isLazyWrapperVNode(vnode) {
|
|
129
|
+
const type = vnode.type;
|
|
130
|
+
if (typeof type !== "function") return false;
|
|
131
|
+
const fn = type;
|
|
132
|
+
return fn.displayName === "Lazy" || fn.name === "Lazy";
|
|
133
|
+
}
|
|
134
|
+
function countTopLevelDomNodes(vnode) {
|
|
135
|
+
if (!vnode || typeof vnode !== "object") return 0;
|
|
136
|
+
const type = vnode.type;
|
|
137
|
+
if (type === null) return 1;
|
|
138
|
+
if (typeof type === "string") return 1;
|
|
139
|
+
const children = vnode.__k;
|
|
140
|
+
if (!Array.isArray(children)) return 0;
|
|
141
|
+
let total = 0;
|
|
142
|
+
for (const child of children) total += countTopLevelDomNodes(child);
|
|
143
|
+
return total;
|
|
144
|
+
}
|
|
145
|
+
function appendSuspenseOffsetWarning(vnode, count) {
|
|
146
|
+
if (typeof document === "undefined") return;
|
|
147
|
+
appendBannerMessage(`Suspense boundary resolved during hydration: <${getVNodeName(vnode)}> ${count === 0 ? "rendered 0 DOM nodes" : `rendered ${count} DOM nodes`}. Components that unsuspend during hydration must render exactly one DOM node — otherwise sibling offsets can drift and later updates may bind to the wrong nodes.`);
|
|
148
|
+
}
|
|
149
|
+
function appendHydrationWarning(vnode) {
|
|
150
|
+
appendBannerMessage(`Hydration mismatch detected on <${getVNodeName(vnode)}>. The server-rendered HTML did not match the client.`);
|
|
151
|
+
}
|
|
152
|
+
function appendBannerMessage(message) {
|
|
153
|
+
if (typeof document === "undefined") return;
|
|
154
|
+
let banner = document.getElementById(HYDRATION_BANNER_ID);
|
|
155
|
+
if (banner) {
|
|
156
|
+
const list = banner.querySelector(`[data-pracht-mismatch-list]`);
|
|
157
|
+
if (list) {
|
|
158
|
+
const item = document.createElement("li");
|
|
159
|
+
item.textContent = message;
|
|
160
|
+
list.appendChild(item);
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
banner = document.createElement("div");
|
|
165
|
+
banner.id = HYDRATION_BANNER_ID;
|
|
166
|
+
banner.setAttribute("role", "alert");
|
|
167
|
+
banner.style.cssText = [
|
|
168
|
+
"position:fixed",
|
|
169
|
+
"top:0",
|
|
170
|
+
"left:0",
|
|
171
|
+
"right:0",
|
|
172
|
+
"z-index:2147483647",
|
|
173
|
+
"background:#1a1a2e",
|
|
174
|
+
"color:#ff6b6b",
|
|
175
|
+
"padding:12px 16px",
|
|
176
|
+
"font:12px/1.5 ui-monospace,Menlo,Consolas,monospace",
|
|
177
|
+
"border-bottom:2px solid #e74c3c",
|
|
178
|
+
"box-shadow:0 2px 8px rgba(0,0,0,0.3)"
|
|
179
|
+
].join(";");
|
|
180
|
+
const title = document.createElement("strong");
|
|
181
|
+
title.textContent = "pracht: hydration mismatch";
|
|
182
|
+
title.style.cssText = "display:block;margin-bottom:4px;color:#fff";
|
|
183
|
+
banner.appendChild(title);
|
|
184
|
+
const list = document.createElement("ul");
|
|
185
|
+
list.setAttribute("data-pracht-mismatch-list", "");
|
|
186
|
+
list.style.cssText = "margin:0;padding-left:18px";
|
|
187
|
+
const item = document.createElement("li");
|
|
188
|
+
item.textContent = message;
|
|
189
|
+
list.appendChild(item);
|
|
190
|
+
banner.appendChild(list);
|
|
191
|
+
document.body.appendChild(banner);
|
|
192
|
+
}
|
|
193
|
+
function getVNodeName(vnode) {
|
|
194
|
+
if (!vnode) return "Unknown";
|
|
195
|
+
const type = vnode.type;
|
|
196
|
+
if (typeof type === "string") return type;
|
|
197
|
+
if (typeof type === "function") {
|
|
198
|
+
const fn = type;
|
|
199
|
+
return fn.displayName || fn.name || "Component";
|
|
200
|
+
}
|
|
201
|
+
return "Unknown";
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/scroll-restoration.ts
|
|
205
|
+
const STORAGE_KEY = "pracht:scroll-positions";
|
|
206
|
+
const MAX_SCROLL_ENTRIES = 50;
|
|
207
|
+
const HISTORY_STATE_KEY = "__prachtScrollKey";
|
|
208
|
+
function readEntries(storage) {
|
|
209
|
+
if (!storage) return [];
|
|
210
|
+
let raw = null;
|
|
211
|
+
try {
|
|
212
|
+
raw = storage.getItem(STORAGE_KEY);
|
|
213
|
+
} catch {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
if (!raw) return [];
|
|
217
|
+
try {
|
|
218
|
+
const parsed = JSON.parse(raw);
|
|
219
|
+
if (!Array.isArray(parsed)) return [];
|
|
220
|
+
return parsed.filter((entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && typeof entry[2] === "number");
|
|
221
|
+
} catch {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Create a scroll position store backed by the given storage (normally
|
|
227
|
+
* `sessionStorage`). Storage failures (private mode, quota) degrade to an
|
|
228
|
+
* in-memory map for the current page lifetime.
|
|
229
|
+
*/
|
|
230
|
+
function createScrollPositionStore(storage, maxEntries = MAX_SCROLL_ENTRIES) {
|
|
231
|
+
const positions = /* @__PURE__ */ new Map();
|
|
232
|
+
for (const [key, x, y] of readEntries(storage)) positions.set(key, {
|
|
233
|
+
x,
|
|
234
|
+
y
|
|
235
|
+
});
|
|
236
|
+
function persist() {
|
|
237
|
+
if (!storage) return;
|
|
238
|
+
const entries = [];
|
|
239
|
+
for (const [key, position] of positions) entries.push([
|
|
240
|
+
key,
|
|
241
|
+
position.x,
|
|
242
|
+
position.y
|
|
243
|
+
]);
|
|
244
|
+
try {
|
|
245
|
+
storage.setItem(STORAGE_KEY, JSON.stringify(entries));
|
|
246
|
+
} catch {}
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
get(key) {
|
|
250
|
+
return positions.get(key) ?? null;
|
|
251
|
+
},
|
|
252
|
+
set(key, position) {
|
|
253
|
+
positions.delete(key);
|
|
254
|
+
positions.set(key, position);
|
|
255
|
+
while (positions.size > maxEntries) {
|
|
256
|
+
const oldest = positions.keys().next();
|
|
257
|
+
if (oldest.done) break;
|
|
258
|
+
positions.delete(oldest.value);
|
|
259
|
+
}
|
|
260
|
+
persist();
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function getSessionScrollStorage() {
|
|
265
|
+
if (typeof window === "undefined") return null;
|
|
266
|
+
try {
|
|
267
|
+
return window.sessionStorage ?? null;
|
|
268
|
+
} catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function generateScrollKey() {
|
|
273
|
+
return Math.random().toString(36).slice(2, 10);
|
|
274
|
+
}
|
|
275
|
+
/** Read the pracht scroll key from a `history.state` value, if present. */
|
|
276
|
+
function readScrollKeyFromHistoryState(state) {
|
|
277
|
+
if (!state || typeof state !== "object") return null;
|
|
278
|
+
const key = state[HISTORY_STATE_KEY];
|
|
279
|
+
return typeof key === "string" ? key : null;
|
|
280
|
+
}
|
|
281
|
+
/** Merge the pracht scroll key into an existing `history.state` value. */
|
|
282
|
+
function withScrollKeyInHistoryState(state, key) {
|
|
283
|
+
if (state && typeof state === "object" && !Array.isArray(state)) return {
|
|
284
|
+
...state,
|
|
285
|
+
[HISTORY_STATE_KEY]: key
|
|
286
|
+
};
|
|
287
|
+
return { [HISTORY_STATE_KEY]: key };
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/router.ts
|
|
291
|
+
const NavigateContext = createContext(async () => {});
|
|
292
|
+
function useNavigate() {
|
|
293
|
+
return useContext(NavigateContext);
|
|
294
|
+
}
|
|
295
|
+
async function initClientRouter(options) {
|
|
296
|
+
const { app, routeModules, shellModules, root, findModuleKey } = options;
|
|
297
|
+
if (import.meta.env?.DEV) installHydrationMismatchWarning();
|
|
298
|
+
const moduleCache = /* @__PURE__ */ new Map();
|
|
299
|
+
function loadModule(modules, key) {
|
|
300
|
+
let cached = moduleCache.get(key);
|
|
301
|
+
if (!cached) {
|
|
302
|
+
cached = modules[key]();
|
|
303
|
+
moduleCache.set(key, cached);
|
|
304
|
+
}
|
|
305
|
+
return cached;
|
|
306
|
+
}
|
|
307
|
+
function startRouteImport(match) {
|
|
308
|
+
const routeKey = findModuleKey(routeModules, match.route.file);
|
|
309
|
+
if (!routeKey) return null;
|
|
310
|
+
return loadModule(routeModules, routeKey);
|
|
311
|
+
}
|
|
312
|
+
function startShellImport(match) {
|
|
313
|
+
if (!match.route.shellFile) return null;
|
|
314
|
+
const shellKey = findModuleKey(shellModules, match.route.shellFile);
|
|
315
|
+
if (!shellKey) return null;
|
|
316
|
+
return loadModule(shellModules, shellKey);
|
|
317
|
+
}
|
|
318
|
+
let updateRouteState = null;
|
|
319
|
+
let routeStateVersion = 0;
|
|
320
|
+
let latestNavigationId = 0;
|
|
321
|
+
let activeNavigationAbort = null;
|
|
322
|
+
const scrollStore = createScrollPositionStore(getSessionScrollStorage());
|
|
323
|
+
if ("scrollRestoration" in history) history.scrollRestoration = "manual";
|
|
324
|
+
let currentScrollKey = readScrollKeyFromHistoryState(history.state) ?? "";
|
|
325
|
+
const hadExistingScrollKey = currentScrollKey !== "";
|
|
326
|
+
if (!hadExistingScrollKey) {
|
|
327
|
+
currentScrollKey = generateScrollKey();
|
|
328
|
+
try {
|
|
329
|
+
history.replaceState(withScrollKeyInHistoryState(history.state, currentScrollKey), "", window.location.href);
|
|
330
|
+
} catch {}
|
|
331
|
+
}
|
|
332
|
+
function saveScrollPosition() {
|
|
333
|
+
scrollStore.set(currentScrollKey, {
|
|
334
|
+
x: window.scrollX,
|
|
335
|
+
y: window.scrollY
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
window.addEventListener("pagehide", saveScrollPosition);
|
|
339
|
+
function restoreOrResetScroll(opts, browserUrl) {
|
|
340
|
+
if (opts?.preserveScroll) return;
|
|
341
|
+
if (opts?._popstate) {
|
|
342
|
+
const saved = scrollStore.get(currentScrollKey);
|
|
343
|
+
window.scrollTo(saved?.x ?? 0, saved?.y ?? 0);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const hashIndex = browserUrl.indexOf("#");
|
|
347
|
+
if (hashIndex !== -1) {
|
|
348
|
+
let id = browserUrl.slice(hashIndex + 1);
|
|
349
|
+
try {
|
|
350
|
+
id = decodeURIComponent(id);
|
|
351
|
+
} catch {}
|
|
352
|
+
const hashTarget = id ? document.getElementById(id) : null;
|
|
353
|
+
if (hashTarget && typeof hashTarget.scrollIntoView === "function") {
|
|
354
|
+
hashTarget.scrollIntoView();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
window.scrollTo(0, 0);
|
|
359
|
+
}
|
|
360
|
+
let afterCommitCallback = null;
|
|
361
|
+
function RouterRoot({ initialState }) {
|
|
362
|
+
const [routeState, setRouteState] = useState(initialState);
|
|
363
|
+
updateRouteState = setRouteState;
|
|
364
|
+
const navigateValue = useMemo(() => navigate, []);
|
|
365
|
+
const { Shell, Component, componentProps, data, params, routeId, url, version } = routeState;
|
|
366
|
+
useLayoutEffect(() => {
|
|
367
|
+
if (!afterCommitCallback) return;
|
|
368
|
+
const callback = afterCommitCallback;
|
|
369
|
+
afterCommitCallback = null;
|
|
370
|
+
callback();
|
|
371
|
+
}, [version]);
|
|
372
|
+
const componentTree = Shell ? h(Shell, null, h(Component, componentProps)) : h(Component, componentProps);
|
|
373
|
+
return h(NavigateContext.Provider, { value: navigateValue }, h(PrachtRuntimeProvider, {
|
|
374
|
+
data,
|
|
375
|
+
params,
|
|
376
|
+
routeId,
|
|
377
|
+
routes: app.routes,
|
|
378
|
+
stateVersion: version,
|
|
379
|
+
url
|
|
380
|
+
}, componentTree));
|
|
381
|
+
}
|
|
382
|
+
function applyRouteState(routeState) {
|
|
383
|
+
if (updateRouteState) {
|
|
384
|
+
updateRouteState(routeState);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
render(h(RouterRoot, { initialState: routeState }), root);
|
|
388
|
+
}
|
|
389
|
+
async function resolveRouteState(match, state, currentUrl, routeModPromise, shellModPromise) {
|
|
390
|
+
const routeMod = await (routeModPromise ?? startRouteImport(match));
|
|
391
|
+
if (!routeMod) return null;
|
|
392
|
+
let Shell = null;
|
|
393
|
+
const resolvedShell = await (shellModPromise ?? startShellImport(match));
|
|
394
|
+
if (resolvedShell) Shell = resolvedShell.Shell;
|
|
395
|
+
const DefaultComponent = typeof routeMod.default === "function" ? routeMod.default : void 0;
|
|
396
|
+
const ErrorBoundary = routeMod.ErrorBoundary ?? resolvedShell?.ErrorBoundary;
|
|
397
|
+
const Component = state.error ? ErrorBoundary : routeMod.Component ?? DefaultComponent;
|
|
398
|
+
if (!Component) return null;
|
|
399
|
+
const componentProps = state.error ? { error: deserializeRouteError(state.error) } : {
|
|
400
|
+
data: state.data,
|
|
401
|
+
params: match.params
|
|
402
|
+
};
|
|
403
|
+
return {
|
|
404
|
+
Shell,
|
|
405
|
+
Component,
|
|
406
|
+
componentProps,
|
|
407
|
+
data: state.data,
|
|
408
|
+
params: match.params,
|
|
409
|
+
routeId: match.route.id ?? "",
|
|
410
|
+
url: currentUrl,
|
|
411
|
+
version: ++routeStateVersion
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
async function resolveSpaPendingState(match, currentUrl, shellModPromise) {
|
|
415
|
+
const resolvedShell = await (shellModPromise ?? startShellImport(match));
|
|
416
|
+
if (!resolvedShell) return null;
|
|
417
|
+
const Shell = resolvedShell.Shell ?? null;
|
|
418
|
+
const Loading = resolvedShell.Loading;
|
|
419
|
+
if (!Shell && !Loading) return null;
|
|
420
|
+
return {
|
|
421
|
+
Shell,
|
|
422
|
+
Component: Loading ?? (() => null),
|
|
423
|
+
componentProps: {},
|
|
424
|
+
data: void 0,
|
|
425
|
+
params: match.params,
|
|
426
|
+
routeId: match.route.id ?? "",
|
|
427
|
+
url: currentUrl,
|
|
428
|
+
version: ++routeStateVersion
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function resolveRedirectTarget(location) {
|
|
432
|
+
const targetUrl = parseSafeNavigationUrl(location, window.location.href);
|
|
433
|
+
if (!targetUrl) return {
|
|
434
|
+
isCurrentLocation: false,
|
|
435
|
+
unsafe: true
|
|
436
|
+
};
|
|
437
|
+
const fullInternalTarget = targetUrl.pathname + targetUrl.search + targetUrl.hash;
|
|
438
|
+
const internalPath = targetUrl.pathname + targetUrl.search;
|
|
439
|
+
const currentPath = window.location.pathname + window.location.search + window.location.hash;
|
|
440
|
+
const isCurrentLocation = targetUrl.origin === window.location.origin && fullInternalTarget === currentPath;
|
|
441
|
+
if (targetUrl.origin !== window.location.origin) return {
|
|
442
|
+
externalUrl: targetUrl.toString(),
|
|
443
|
+
isCurrentLocation: false
|
|
444
|
+
};
|
|
445
|
+
if (targetUrl.hash) return {
|
|
446
|
+
documentUrl: targetUrl.toString(),
|
|
447
|
+
isCurrentLocation
|
|
448
|
+
};
|
|
449
|
+
return {
|
|
450
|
+
internalPath,
|
|
451
|
+
isCurrentLocation
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
async function navigate(to, opts) {
|
|
455
|
+
const navigationId = ++latestNavigationId;
|
|
456
|
+
activeNavigationAbort?.abort();
|
|
457
|
+
const abortController = new AbortController();
|
|
458
|
+
activeNavigationAbort = abortController;
|
|
459
|
+
const navigationTarget = typeof to === "string" ? to : buildHref(app.routes, to.route, to);
|
|
460
|
+
const target = resolveBrowserRouteTarget(navigationTarget);
|
|
461
|
+
if (!target) {
|
|
462
|
+
const safeUrl = parseSafeNavigationUrl(navigationTarget, window.location.href);
|
|
463
|
+
if (safeUrl) window.location.href = safeUrl.toString();
|
|
464
|
+
else if (navigationTarget) console.error(`[pracht] refused to navigate to unsafe URL: ${navigationTarget}`);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const match = matchAppRoute(app, target.pathname);
|
|
468
|
+
if (!match) {
|
|
469
|
+
window.location.href = target.browserUrl;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const navigationToken = beginLoadingNavigation(createNavigationLocation(target.browserUrl));
|
|
473
|
+
try {
|
|
474
|
+
let statePromise;
|
|
475
|
+
if (routeNeedsServerFetch(match.route)) statePromise = getCachedRouteState(target.requestUrl) ?? fetchPrachtRouteState(target.requestUrl, { signal: abortController.signal });
|
|
476
|
+
else statePromise = Promise.resolve({
|
|
477
|
+
type: "data",
|
|
478
|
+
data: void 0
|
|
479
|
+
});
|
|
480
|
+
const routeModPromise = startRouteImport(match);
|
|
481
|
+
const shellModPromise = startShellImport(match);
|
|
482
|
+
let state = {
|
|
483
|
+
data: void 0,
|
|
484
|
+
error: null
|
|
485
|
+
};
|
|
486
|
+
try {
|
|
487
|
+
const result = await statePromise;
|
|
488
|
+
if (navigationId !== latestNavigationId) return;
|
|
489
|
+
if (result.type === "redirect") {
|
|
490
|
+
if (result.location) {
|
|
491
|
+
const redirect = resolveRedirectTarget(result.location);
|
|
492
|
+
if (redirect.unsafe) {
|
|
493
|
+
console.error(`[pracht] refused to navigate to unsafe URL: ${result.location}`);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (redirect.externalUrl) {
|
|
497
|
+
window.location.href = redirect.externalUrl;
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (redirect.isCurrentLocation) return;
|
|
501
|
+
if (redirect.documentUrl) {
|
|
502
|
+
window.location.href = redirect.documentUrl;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (redirect.internalPath) {
|
|
506
|
+
await navigate(redirect.internalPath, opts);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
window.location.href = target.browserUrl;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
window.location.href = target.browserUrl;
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (result.type === "error") state = {
|
|
516
|
+
data: void 0,
|
|
517
|
+
error: result.error
|
|
518
|
+
};
|
|
519
|
+
else state = {
|
|
520
|
+
data: result.data,
|
|
521
|
+
error: null
|
|
522
|
+
};
|
|
523
|
+
} catch {
|
|
524
|
+
if (abortController.signal.aborted || navigationId !== latestNavigationId) return;
|
|
525
|
+
window.location.href = target.browserUrl;
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (navigationId !== latestNavigationId) return;
|
|
529
|
+
if (!opts?._popstate) {
|
|
530
|
+
saveScrollPosition();
|
|
531
|
+
if (opts?.replace) history.replaceState(withScrollKeyInHistoryState(history.state, currentScrollKey), "", target.browserUrl);
|
|
532
|
+
else {
|
|
533
|
+
const nextScrollKey = generateScrollKey();
|
|
534
|
+
history.pushState(withScrollKeyInHistoryState(null, nextScrollKey), "", target.browserUrl);
|
|
535
|
+
currentScrollKey = nextScrollKey;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const routeState = await resolveRouteState(match, state, target.requestUrl, routeModPromise, shellModPromise);
|
|
539
|
+
if (navigationId !== latestNavigationId) return;
|
|
540
|
+
if (!routeState) {
|
|
541
|
+
window.location.href = target.browserUrl;
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const commit = () => {
|
|
545
|
+
afterCommitCallback = () => restoreOrResetScroll(opts, target.browserUrl);
|
|
546
|
+
applyRouteState(routeState);
|
|
547
|
+
};
|
|
548
|
+
await commitWithOptionalViewTransition(commit, opts?.viewTransition ?? app.viewTransitions === true);
|
|
549
|
+
} finally {
|
|
550
|
+
settleNavigation(navigationToken);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const initialTarget = resolveBrowserRouteTarget(options.initialState.url);
|
|
554
|
+
const initialRequestUrl = initialTarget?.requestUrl ?? options.initialState.url;
|
|
555
|
+
const initialBrowserUrl = initialTarget?.browserUrl ?? options.initialState.url;
|
|
556
|
+
const initialMatch = matchAppRoute(app, initialTarget?.pathname ?? options.initialState.url);
|
|
557
|
+
if (initialMatch) {
|
|
558
|
+
const initialShellPromise = initialMatch.route.render === "spa" && options.initialState.pending ? startShellImport(initialMatch) : null;
|
|
559
|
+
let state = {
|
|
560
|
+
data: options.initialState.data,
|
|
561
|
+
error: options.initialState.error ?? null
|
|
562
|
+
};
|
|
563
|
+
if (initialMatch.route.render === "spa" && options.initialState.pending) {
|
|
564
|
+
const dataPromise = fetchPrachtRouteState(initialRequestUrl, { useDataParam: true });
|
|
565
|
+
const pendingState = await resolveSpaPendingState(initialMatch, initialRequestUrl, initialShellPromise);
|
|
566
|
+
if (pendingState) hydrate(h(RouterRoot, { initialState: pendingState }), root);
|
|
567
|
+
try {
|
|
568
|
+
const result = await dataPromise;
|
|
569
|
+
if (result.type === "redirect") {
|
|
570
|
+
const safeRedirect = parseSafeNavigationUrl(result.location, window.location.href);
|
|
571
|
+
if (!safeRedirect) {
|
|
572
|
+
console.error(`[pracht] refused to navigate to unsafe URL: ${result.location}`);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
window.location.href = safeRedirect.toString();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (result.type === "error") state = {
|
|
579
|
+
data: void 0,
|
|
580
|
+
error: result.error
|
|
581
|
+
};
|
|
582
|
+
else state = {
|
|
583
|
+
data: result.data,
|
|
584
|
+
error: null
|
|
585
|
+
};
|
|
586
|
+
} catch {
|
|
587
|
+
window.location.href = initialBrowserUrl;
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const resolvedState = await resolveRouteState(initialMatch, state, initialRequestUrl, void 0, initialShellPromise);
|
|
591
|
+
if (resolvedState) applyRouteState(resolvedState);
|
|
592
|
+
} else {
|
|
593
|
+
const initialRouteState = await resolveRouteState(initialMatch, state, initialRequestUrl, void 0, initialShellPromise);
|
|
594
|
+
if (initialRouteState) if (initialMatch.route.render === "spa") render(h(RouterRoot, { initialState: initialRouteState }), root);
|
|
595
|
+
else {
|
|
596
|
+
markHydrating();
|
|
597
|
+
hydrate(h(RouterRoot, { initialState: initialRouteState }), root);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
document.addEventListener("click", (e) => {
|
|
602
|
+
const anchor = e.target.closest?.("a");
|
|
603
|
+
if (!anchor) return;
|
|
604
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
605
|
+
if (e.defaultPrevented) return;
|
|
606
|
+
if (e.button !== 0) return;
|
|
607
|
+
const target = anchor.getAttribute("target");
|
|
608
|
+
if (target && target !== "_self") return;
|
|
609
|
+
if (anchor.hasAttribute("download")) return;
|
|
610
|
+
const href = anchor.getAttribute("href");
|
|
611
|
+
if (!href || href.startsWith("#")) return;
|
|
612
|
+
let url;
|
|
613
|
+
try {
|
|
614
|
+
url = new URL(href, window.location.origin);
|
|
615
|
+
} catch {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (url.origin !== window.location.origin) return;
|
|
619
|
+
e.preventDefault();
|
|
620
|
+
const navOptions = {};
|
|
621
|
+
if (anchor.hasAttribute("data-pracht-preserve-scroll")) navOptions.preserveScroll = true;
|
|
622
|
+
if (anchor.hasAttribute("data-pracht-view-transition")) navOptions.viewTransition = true;
|
|
623
|
+
navigate(url.pathname + url.search + url.hash, navOptions);
|
|
624
|
+
});
|
|
625
|
+
window.addEventListener("popstate", () => {
|
|
626
|
+
saveScrollPosition();
|
|
627
|
+
let nextScrollKey = readScrollKeyFromHistoryState(history.state);
|
|
628
|
+
if (!nextScrollKey) {
|
|
629
|
+
nextScrollKey = generateScrollKey();
|
|
630
|
+
try {
|
|
631
|
+
history.replaceState(withScrollKeyInHistoryState(history.state, nextScrollKey), "", window.location.href);
|
|
632
|
+
} catch {}
|
|
633
|
+
}
|
|
634
|
+
currentScrollKey = nextScrollKey;
|
|
635
|
+
navigate(window.location.pathname + window.location.search + window.location.hash, { _popstate: true });
|
|
636
|
+
});
|
|
637
|
+
window.__PRACHT_NAVIGATE__ = navigate;
|
|
638
|
+
window.__PRACHT_ROUTER_READY__ = true;
|
|
639
|
+
document.documentElement.setAttribute("data-pracht-hydrated", "true");
|
|
640
|
+
if (hadExistingScrollKey) {
|
|
641
|
+
const savedPosition = scrollStore.get(currentScrollKey);
|
|
642
|
+
if (savedPosition) window.scrollTo(savedPosition.x, savedPosition.y);
|
|
643
|
+
}
|
|
644
|
+
const warmModules = (match) => {
|
|
645
|
+
startRouteImport(match);
|
|
646
|
+
startShellImport(match);
|
|
647
|
+
};
|
|
648
|
+
registerPrefetchTarget(app, warmModules);
|
|
649
|
+
import("./prefetch-COYeuvQK.mjs").then(({ setupPrefetching }) => {
|
|
650
|
+
setupPrefetching(app, warmModules);
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Commit a navigation's DOM update, optionally wrapped in
|
|
655
|
+
* `document.startViewTransition()`. Falls back to a plain commit when view
|
|
656
|
+
* transitions are disabled or unsupported. Resolves once the DOM update has
|
|
657
|
+
* been applied (not when the transition animation finishes).
|
|
658
|
+
*/
|
|
659
|
+
async function commitWithOptionalViewTransition(commit, useViewTransition) {
|
|
660
|
+
const doc = document;
|
|
661
|
+
if (!useViewTransition || typeof doc.startViewTransition !== "function") {
|
|
662
|
+
commit();
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
let committed = false;
|
|
666
|
+
let transition;
|
|
667
|
+
try {
|
|
668
|
+
transition = doc.startViewTransition(async () => {
|
|
669
|
+
committed = true;
|
|
670
|
+
commit();
|
|
671
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
672
|
+
});
|
|
673
|
+
} catch {}
|
|
674
|
+
try {
|
|
675
|
+
await transition?.updateCallbackDone;
|
|
676
|
+
} catch {}
|
|
677
|
+
if (!committed) commit();
|
|
678
|
+
}
|
|
679
|
+
function resolveBrowserRouteTarget(to) {
|
|
680
|
+
if (typeof window === "undefined") return null;
|
|
681
|
+
try {
|
|
682
|
+
const url = new URL(to, window.location.href);
|
|
683
|
+
if (url.origin !== window.location.origin) return null;
|
|
684
|
+
return {
|
|
685
|
+
browserUrl: url.pathname + url.search + url.hash,
|
|
686
|
+
pathname: url.pathname,
|
|
687
|
+
requestUrl: url.pathname + url.search
|
|
688
|
+
};
|
|
689
|
+
} catch {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
//#endregion
|
|
694
|
+
export { useNavigate as n, useIsHydrated as r, initClientRouter as t };
|