@vesk/runtime 0.1.14 → 0.2.9
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/dist/form.js +1 -1
- package/dist/headless.d.ts +30 -0
- package/dist/headless.d.ts.map +1 -0
- package/dist/headless.js +39 -0
- package/dist/hmr-client.js +5 -2
- package/dist/index-client.d.ts +5 -0
- package/dist/index-client.d.ts.map +1 -1
- package/dist/index-client.js +3 -0
- package/dist/index-router.d.ts +16 -0
- package/dist/index-router.d.ts.map +1 -0
- package/dist/index-router.js +15 -0
- package/dist/index-server.d.ts +3 -0
- package/dist/index-server.d.ts.map +1 -1
- package/dist/index-server.js +2 -0
- package/dist/loading-indicator.d.ts +127 -0
- package/dist/loading-indicator.d.ts.map +1 -0
- package/dist/loading-indicator.js +342 -0
- package/dist/md.d.ts +112 -3
- package/dist/md.d.ts.map +1 -1
- package/dist/md.js +1977 -198
- package/dist/network.d.ts +28 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +62 -0
- package/dist/request.d.ts.map +1 -1
- package/dist/request.js +18 -6
- package/dist/router-components.d.ts +26 -0
- package/dist/router-components.d.ts.map +1 -1
- package/dist/router-components.js +73 -0
- package/dist/router-match.d.ts +2 -0
- package/dist/router-match.d.ts.map +1 -1
- package/dist/router-match.js +6 -3
- package/dist/router.d.ts +27 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +749 -270
- package/package.json +6 -2
package/dist/router.js
CHANGED
|
@@ -2,7 +2,9 @@ import { get, set, scope, set_active_block } from '@vesk/runtime/src/ripple-runt
|
|
|
2
2
|
import { root } from '@vesk/runtime/src/ripple-blocks';
|
|
3
3
|
import { createHydrateWalker, hydrateViewport, hydrateIdle, hydrateOnInteraction } from '@vesk/runtime/src/hydrate';
|
|
4
4
|
import { matchRoute, flattenLayoutChain, buildTreeFromMap } from '@vesk/runtime/src/router-match';
|
|
5
|
-
import { setIsHydrating, _state, _scrollPositions, _isPopStateNavigation, setIsPopStateNavigation, setCurrentRouter, showLoadingInContainer, handleScroll, applyHead, findLoadingComponent, findErrorComponent, findNotFoundComponent, Outlet, Link, NavLink, useNavigate, useParams, usePathname, useSearchParams, useRouter, Redirect, redirect, permanentRedirect, NotFoundError, notFound, } from '@vesk/runtime/src/router-components';
|
|
5
|
+
import { setIsHydrating, _state, _scrollPositions, _isPopStateNavigation, setIsPopStateNavigation, setCurrentRouter, showLoadingInContainer, handleScroll, applyHead, findLoadingComponent, findErrorComponent, findNotFoundComponent, findOfflineComponent, findNetworkComponent, Outlet, Link, NavLink, useNavigate, useParams, usePathname, useSearchParams, useRouter, Redirect, redirect, permanentRedirect, NotFoundError, notFound, } from '@vesk/runtime/src/router-components';
|
|
6
|
+
import { getNetworkState, watchNetwork } from '@vesk/runtime/src/network';
|
|
7
|
+
import { loadingStart, loadingFinish, getLoadingState } from '@vesk/runtime/src/loading-indicator';
|
|
6
8
|
export { Outlet, Link, NavLink, useNavigate, useParams, usePathname, useSearchParams, useRouter, Redirect, redirect, permanentRedirect, NotFoundError, notFound, ensureChunk, };
|
|
7
9
|
export { matchRoute } from '@vesk/runtime/src/router-match';
|
|
8
10
|
const loadedChunks = new Set();
|
|
@@ -59,6 +61,17 @@ function hasPendingChunks(nodes) {
|
|
|
59
61
|
nodes.forEach(walk);
|
|
60
62
|
return urls;
|
|
61
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Classifies a data-fetch failure as a connectivity problem rather than a
|
|
66
|
+
* server problem: fetch rejects with TypeError on network failure, and
|
|
67
|
+
* `navigator.onLine === false` confirms it even where the error shape is
|
|
68
|
+
* environment-specific.
|
|
69
|
+
*/
|
|
70
|
+
function isConnectivityFailure(err) {
|
|
71
|
+
if (typeof navigator !== 'undefined' && navigator && navigator.onLine === false)
|
|
72
|
+
return true;
|
|
73
|
+
return err instanceof TypeError;
|
|
74
|
+
}
|
|
62
75
|
const _dataPromises = new Map();
|
|
63
76
|
async function fetchRouteData(path) {
|
|
64
77
|
try {
|
|
@@ -94,7 +107,13 @@ async function fetchRouteData(path) {
|
|
|
94
107
|
return null;
|
|
95
108
|
return (await res.json());
|
|
96
109
|
}
|
|
97
|
-
catch {
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (isConnectivityFailure(err))
|
|
112
|
+
return { offline: true };
|
|
113
|
+
// Passive signals can miss (proxies, emulation) — actively probe
|
|
114
|
+
// before classifying as a plain failure.
|
|
115
|
+
if (await looksOffline())
|
|
116
|
+
return { offline: true };
|
|
98
117
|
return null;
|
|
99
118
|
}
|
|
100
119
|
}
|
|
@@ -140,7 +159,27 @@ function chunkErrorForNode(node) {
|
|
|
140
159
|
return new Error(String(node._chunkError));
|
|
141
160
|
return undefined;
|
|
142
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Active connectivity check for failure paths where passive signals
|
|
164
|
+
* (`navigator.onLine`) are unreliable (proxies, CDP emulation, some
|
|
165
|
+
* browsers lag the flag). Any HTTP response — even 404/500 — proves the
|
|
166
|
+
* origin is reachable; only a fetch-level rejection counts as offline.
|
|
167
|
+
*/
|
|
168
|
+
async function looksOffline() {
|
|
169
|
+
if (typeof navigator !== 'undefined' && navigator && navigator.onLine === false)
|
|
170
|
+
return true;
|
|
171
|
+
if (typeof fetch !== 'function')
|
|
172
|
+
return false;
|
|
173
|
+
try {
|
|
174
|
+
await fetch('/__vesk_connectivity_' + Date.now(), { method: 'HEAD', cache: 'no-store' });
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
return err instanceof TypeError;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
143
181
|
async function renderNotFound(router, match, container) {
|
|
182
|
+
clearOfflineFlag(router, container);
|
|
144
183
|
const chain = match.matchChain;
|
|
145
184
|
const paramValues = match.params;
|
|
146
185
|
const notFoundFn = findNotFoundComponent(chain);
|
|
@@ -169,7 +208,142 @@ async function renderNotFound(router, match, container) {
|
|
|
169
208
|
container.innerHTML = '<h1>404 — Not Found</h1>';
|
|
170
209
|
router._currentMatch = match;
|
|
171
210
|
}
|
|
211
|
+
function defaultOfflineHtml(url) {
|
|
212
|
+
return '<div data-vesk-offline style="all:initial;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:60vh;font-family:system-ui,-apple-system,sans-serif;color:#1f2937;text-align:center;padding:24px">' +
|
|
213
|
+
'<div style="font-size:40px;line-height:1;margin-bottom:16px" aria-hidden="true">📡</div>' +
|
|
214
|
+
'<h1 style="font-size:22px;font-weight:700;margin:0 0 8px">You\u2019re offline</h1>' +
|
|
215
|
+
'<p style="margin:0 0 20px;color:#6b7280;max-width:34em">Check your connection. <span style="color:#9ca3af">' + escapeOfflineUrl(url) + '</span> will load automatically once you\u2019re back online.</p>' +
|
|
216
|
+
'<button type="button" data-vesk-offline-retry style="all:unset;cursor:pointer;background:#2563eb;color:#fff;padding:10px 22px;border-radius:8px;font-size:14px;font-weight:600">Retry now</button>' +
|
|
217
|
+
'</div>';
|
|
218
|
+
}
|
|
219
|
+
function escapeOfflineUrl(url) {
|
|
220
|
+
return String(url).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Renders the connectivity experience for a navigation that failed due to
|
|
224
|
+
* loss of (or degraded) network. Precedence:
|
|
225
|
+
* 1. nearest route `offline.vsk` — dedicated offline UI (when offline)
|
|
226
|
+
* 2. nearest route `network.vsk` — state-aware UI receiving
|
|
227
|
+
* `{ url, params, retry, online, effectiveType, downlink, rtt, saveData }`
|
|
228
|
+
* 3. router `offline` option (component or HTML string)
|
|
229
|
+
* 4. nearest route `error.vsk`, invoked with an `offline: true` error so
|
|
230
|
+
* existing error boundaries can style the offline case themselves
|
|
231
|
+
* 5. built-in default panel
|
|
232
|
+
*
|
|
233
|
+
* A network failure never renders the not-found page. While a boundary from
|
|
234
|
+
* steps 1–3 or 5 is displayed it re-renders on connectivity changes
|
|
235
|
+
* (3g→4g→offline…), and the router re-navigates automatically when the
|
|
236
|
+
* browser comes back online.
|
|
237
|
+
*/
|
|
238
|
+
async function renderOfflinePage(router, match, container) {
|
|
239
|
+
const url = match.pathname || (typeof window !== 'undefined' ? window.location.pathname : '');
|
|
240
|
+
const chain = match.matchChain;
|
|
241
|
+
const navTokenAtRender = router._navToken;
|
|
242
|
+
// Debounced retry: a component that calls retry() while rendering must
|
|
243
|
+
// not trigger a navigate→fail→render→retry microtask loop.
|
|
244
|
+
const safeRetry = () => {
|
|
245
|
+
const r = router;
|
|
246
|
+
const now = Date.now();
|
|
247
|
+
if (now - (r._lastOfflineRetry || 0) < 300)
|
|
248
|
+
return;
|
|
249
|
+
r._lastOfflineRetry = now;
|
|
250
|
+
router.navigate(url, { replace: true });
|
|
251
|
+
};
|
|
252
|
+
const mountComponent = async (entry) => {
|
|
253
|
+
if (typeof entry === 'string') {
|
|
254
|
+
container.innerHTML = entry;
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
const tempRoot = document.createDocumentFragment();
|
|
258
|
+
const walker = createHydrateWalker(tempRoot, []);
|
|
259
|
+
const props = { url, params: match.params, retry: safeRetry, ...getNetworkState() };
|
|
260
|
+
const dom = await runInBlockWindow(() => entry(props, new Map(), walker));
|
|
261
|
+
if (dom && typeof dom === 'object' && dom.nodeType) {
|
|
262
|
+
if (container.replaceChildren)
|
|
263
|
+
container.replaceChildren(dom);
|
|
264
|
+
else {
|
|
265
|
+
container.innerHTML = '';
|
|
266
|
+
container.appendChild(dom);
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
if (typeof dom === 'string') {
|
|
271
|
+
container.innerHTML = dom;
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
};
|
|
276
|
+
let mounted = false;
|
|
277
|
+
let reactive = false; // re-render on connectivity change
|
|
278
|
+
const offlineEntry = findOfflineComponent(chain);
|
|
279
|
+
if (!mounted && offlineEntry && !(getNetworkState().online)) {
|
|
280
|
+
mounted = await mountComponent(offlineEntry);
|
|
281
|
+
reactive = mounted;
|
|
282
|
+
}
|
|
283
|
+
const networkEntry = findNetworkComponent(chain);
|
|
284
|
+
if (!mounted && networkEntry) {
|
|
285
|
+
mounted = await mountComponent(networkEntry);
|
|
286
|
+
reactive = mounted;
|
|
287
|
+
}
|
|
288
|
+
const optionUI = router._offlineUI;
|
|
289
|
+
if (!mounted && optionUI !== undefined && optionUI !== null) {
|
|
290
|
+
mounted = typeof optionUI === 'function'
|
|
291
|
+
? await mountComponent(optionUI)
|
|
292
|
+
: ((container.innerHTML = String(optionUI)), true);
|
|
293
|
+
reactive = typeof optionUI === 'function';
|
|
294
|
+
}
|
|
295
|
+
const errorFn = findErrorComponent(chain);
|
|
296
|
+
if (!mounted && errorFn) {
|
|
297
|
+
// Existing error boundary opts in via `props.offline` /
|
|
298
|
+
// `props.networkState`.
|
|
299
|
+
const err = new Error('You are offline');
|
|
300
|
+
err.statusCode = 0;
|
|
301
|
+
err.offline = true;
|
|
302
|
+
await renderErrorPage(router, match, container, err);
|
|
303
|
+
mounted = true;
|
|
304
|
+
}
|
|
305
|
+
if (!mounted) {
|
|
306
|
+
container.innerHTML = defaultOfflineHtml(url);
|
|
307
|
+
const retryBtn = (container.querySelector ? container.querySelector('[data-vesk-offline-retry]') : null);
|
|
308
|
+
if (retryBtn)
|
|
309
|
+
retryBtn.addEventListener('click', safeRetry);
|
|
310
|
+
reactive = true;
|
|
311
|
+
}
|
|
312
|
+
router._showingOffline = true;
|
|
313
|
+
registerOnlineRecovery(router);
|
|
314
|
+
// Live-swap the displayed boundary when connectivity changes (e.g.
|
|
315
|
+
// offline → 3g → 4g), but stop as soon as a newer navigation takes over.
|
|
316
|
+
if (reactive && typeof window !== 'undefined') {
|
|
317
|
+
container.__veskNetworkUI = true;
|
|
318
|
+
const unsubscribe = watchNetwork(() => {
|
|
319
|
+
const stillCurrent = router._navToken === navTokenAtRender &&
|
|
320
|
+
container.__veskNetworkUI === true &&
|
|
321
|
+
router._showingOffline === true;
|
|
322
|
+
if (!stillCurrent) {
|
|
323
|
+
unsubscribe();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
void renderOfflinePage(router, match, container);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function registerOnlineRecovery(router) {
|
|
331
|
+
const r = router;
|
|
332
|
+
if (r._onlineHandler || typeof window === 'undefined' || typeof window.addEventListener !== 'function')
|
|
333
|
+
return;
|
|
334
|
+
r._onlineHandler = () => {
|
|
335
|
+
if (!r._showingOffline)
|
|
336
|
+
return;
|
|
337
|
+
r._showingOffline = false;
|
|
338
|
+
router.navigate(window.location.pathname + window.location.search, { replace: true });
|
|
339
|
+
};
|
|
340
|
+
window.addEventListener('online', r._onlineHandler);
|
|
341
|
+
}
|
|
172
342
|
async function applyRouteData(router, match, data, container, render = renderMatch) {
|
|
343
|
+
if (data.offline) {
|
|
344
|
+
await renderOfflinePage(router, match, container);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
173
347
|
if (data.notFound) {
|
|
174
348
|
await renderNotFound(router, match, container);
|
|
175
349
|
return;
|
|
@@ -240,7 +414,7 @@ function shouldFetchData(router, match) {
|
|
|
240
414
|
return Date.now() - fetchedAt >= ttl;
|
|
241
415
|
}
|
|
242
416
|
function hasRealPageData(data) {
|
|
243
|
-
if (data.notFound || data.redirect || data.error)
|
|
417
|
+
if (data.offline || data.notFound || data.redirect || data.error)
|
|
244
418
|
return true;
|
|
245
419
|
if (data.head && data.head.trim().length > 0)
|
|
246
420
|
return true;
|
|
@@ -350,6 +524,26 @@ function replacePageContent(container, newContent) {
|
|
|
350
524
|
}
|
|
351
525
|
return true;
|
|
352
526
|
}
|
|
527
|
+
function storePageInstance(router, pageNode, pageProps, result) {
|
|
528
|
+
if (!router || !pageNode._pageName || !result || result.nodeType !== 1)
|
|
529
|
+
return;
|
|
530
|
+
if (!router.__componentInstances)
|
|
531
|
+
router.__componentInstances = new Map();
|
|
532
|
+
const name = pageNode._pageName;
|
|
533
|
+
if (!router.__componentInstances.has(name))
|
|
534
|
+
router.__componentInstances.set(name, []);
|
|
535
|
+
router.__componentInstances.get(name).push({ root: result, props: pageProps, node: pageNode, type: 'page' });
|
|
536
|
+
}
|
|
537
|
+
function storeLayoutInstance(router, node, layoutProps, result) {
|
|
538
|
+
if (!router || !node._layoutName || !result || result.nodeType !== 1)
|
|
539
|
+
return;
|
|
540
|
+
if (!router.__componentInstances)
|
|
541
|
+
router.__componentInstances = new Map();
|
|
542
|
+
const name = node._layoutName;
|
|
543
|
+
if (!router.__componentInstances.has(name))
|
|
544
|
+
router.__componentInstances.set(name, []);
|
|
545
|
+
router.__componentInstances.get(name).push({ root: result, props: layoutProps, node, type: 'layout' });
|
|
546
|
+
}
|
|
353
547
|
function renderMatch(router, match, container, navToken) {
|
|
354
548
|
const chain = match.matchChain;
|
|
355
549
|
const paramValues = match.params;
|
|
@@ -390,11 +584,25 @@ function renderMatch(router, match, container, navToken) {
|
|
|
390
584
|
return;
|
|
391
585
|
}
|
|
392
586
|
// The route matched and has a page name, but its component chunk failed
|
|
393
|
-
// to load or compile
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
587
|
+
// to load or compile (or its component ref is still an unwired name).
|
|
588
|
+
// While offline that is a connectivity problem, not a broken route —
|
|
589
|
+
// show the offline experience instead of an error page. The connectivity
|
|
590
|
+
// probe only runs when a chunk actually failed or the browser reports
|
|
591
|
+
// offline, so ordinary lazy-loading never pays for it.
|
|
592
|
+
if (pageNode._pageName && (!pageNode.page || typeof pageNode.page === 'string')) {
|
|
593
|
+
const suspectOffline = failedChunks.has(pageNode._chunk)
|
|
594
|
+
|| (typeof navigator !== 'undefined' && !!navigator && navigator.onLine === false);
|
|
595
|
+
navDebug('unresolved page guard', String(pageNode._pageName), 'suspect=', String(suspectOffline));
|
|
596
|
+
if (!suspectOffline) {
|
|
597
|
+
const chunkErr0 = chunkErrorForNode(pageNode);
|
|
598
|
+
return renderErrorPage(router, match, container, chunkErr0 || new Error(`Component "${pageNode._pageName}" is unavailable`));
|
|
599
|
+
}
|
|
600
|
+
return looksOffline().then((offline) => {
|
|
601
|
+
if (offline)
|
|
602
|
+
return renderOfflinePage(router, match, container);
|
|
603
|
+
const chunkErr = chunkErrorForNode(pageNode);
|
|
604
|
+
return renderErrorPage(router, match, container, chunkErr || new Error(`Component "${pageNode._pageName}" is unavailable`));
|
|
605
|
+
});
|
|
398
606
|
}
|
|
399
607
|
const layoutNodes = chain.filter(n => n.layout);
|
|
400
608
|
const tempRoot = document.createDocumentFragment();
|
|
@@ -445,7 +653,10 @@ function renderMatch(router, match, container, navToken) {
|
|
|
445
653
|
const pageStep = () => {
|
|
446
654
|
set(_state.params, paramValues);
|
|
447
655
|
set(_state.path, match.pathname || window.location.pathname);
|
|
448
|
-
|
|
656
|
+
{
|
|
657
|
+
const s = window.location.search || '';
|
|
658
|
+
set(_state.search, s.startsWith('?') ? s.slice(1) : s);
|
|
659
|
+
}
|
|
449
660
|
const pageProps = { params: paramValues, ...pageNode.props };
|
|
450
661
|
const result = pageNode.page(pageProps, new Map(), clientWalker);
|
|
451
662
|
if (result && typeof result.then === 'function') {
|
|
@@ -468,7 +679,7 @@ function renderMatch(router, match, container, navToken) {
|
|
|
468
679
|
router.navigate(window.location.pathname, { replace: true });
|
|
469
680
|
}
|
|
470
681
|
};
|
|
471
|
-
const errorProps = { error, retry, params: paramValues, statusCode: error?.statusCode ?? 500, stack: error instanceof Error ? error.stack : String(error), url: typeof window !== 'undefined' ? window.location.pathname : '' };
|
|
682
|
+
const errorProps = { error, retry, params: paramValues, statusCode: error?.statusCode ?? 500, stack: error instanceof Error ? error.stack : String(error), url: typeof window !== 'undefined' ? window.location.pathname : '', offline: error?.offline === true, networkState: getNetworkState() };
|
|
472
683
|
return runInBlockWindow(() => renderLayoutWith(0, () => {
|
|
473
684
|
const result = errorFn(errorProps, new Map(), clientWalker);
|
|
474
685
|
if (result && typeof result.then === 'function') {
|
|
@@ -489,7 +700,7 @@ function renderMatch(router, match, container, navToken) {
|
|
|
489
700
|
router.navigate(window.location.pathname, { replace: true });
|
|
490
701
|
}
|
|
491
702
|
};
|
|
492
|
-
const errorProps = { error, retry, params: paramValues, statusCode: error?.statusCode ?? 500, stack: error instanceof Error ? error.stack : String(error), url: typeof window !== 'undefined' ? window.location.pathname : '' };
|
|
703
|
+
const errorProps = { error, retry, params: paramValues, statusCode: error?.statusCode ?? 500, stack: error instanceof Error ? error.stack : String(error), url: typeof window !== 'undefined' ? window.location.pathname : '', offline: error?.offline === true, networkState: getNetworkState() };
|
|
493
704
|
const errDom = runInBlockWindow(() => errorFn(errorProps, new Map(), clientWalker));
|
|
494
705
|
if (errDom && typeof errDom.then === 'function') {
|
|
495
706
|
errDom.then(mountDom);
|
|
@@ -498,26 +709,6 @@ function renderMatch(router, match, container, navToken) {
|
|
|
498
709
|
mountDom(errDom);
|
|
499
710
|
}
|
|
500
711
|
}
|
|
501
|
-
function storePageInstance(router, pageNode, pageProps, result) {
|
|
502
|
-
if (!router || !pageNode._pageName || !result || result.nodeType !== 1)
|
|
503
|
-
return;
|
|
504
|
-
if (!router.__componentInstances)
|
|
505
|
-
router.__componentInstances = new Map();
|
|
506
|
-
const name = pageNode._pageName;
|
|
507
|
-
if (!router.__componentInstances.has(name))
|
|
508
|
-
router.__componentInstances.set(name, []);
|
|
509
|
-
router.__componentInstances.get(name).push({ root: result, props: pageProps, node: pageNode, type: 'page' });
|
|
510
|
-
}
|
|
511
|
-
function storeLayoutInstance(router, node, layoutProps, result) {
|
|
512
|
-
if (!router || !node._layoutName || !result || result.nodeType !== 1)
|
|
513
|
-
return;
|
|
514
|
-
if (!router.__componentInstances)
|
|
515
|
-
router.__componentInstances = new Map();
|
|
516
|
-
const name = node._layoutName;
|
|
517
|
-
if (!router.__componentInstances.has(name))
|
|
518
|
-
router.__componentInstances.set(name, []);
|
|
519
|
-
router.__componentInstances.get(name).push({ root: result, props: layoutProps, node, type: 'layout' });
|
|
520
|
-
}
|
|
521
712
|
const mountDom = (dom) => {
|
|
522
713
|
// Staleness guard: a suspended (async) page whose render resolves after
|
|
523
714
|
// a NEWER navigation must not clobber the newer page's DOM. The file
|
|
@@ -584,7 +775,13 @@ function makeSsrError(message) {
|
|
|
584
775
|
// failure or when the SSR output is already an error page). The error
|
|
585
776
|
// component renders in place of the page inside the layout chain so the error
|
|
586
777
|
// page keeps the site nav.
|
|
778
|
+
function clearOfflineFlag(router, container) {
|
|
779
|
+
router._showingOffline = false;
|
|
780
|
+
if (container)
|
|
781
|
+
container.__veskNetworkUI = false;
|
|
782
|
+
}
|
|
587
783
|
async function renderErrorPage(router, match, container, error) {
|
|
784
|
+
clearOfflineFlag(router, container);
|
|
588
785
|
const chain = match.matchChain;
|
|
589
786
|
const paramValues = match.params;
|
|
590
787
|
const errorFn = findErrorComponent(chain);
|
|
@@ -604,7 +801,7 @@ async function renderErrorPage(router, match, container, error) {
|
|
|
604
801
|
router.navigate(window.location.pathname, { replace: true });
|
|
605
802
|
}
|
|
606
803
|
};
|
|
607
|
-
const errorProps = { error, retry, params: paramValues, statusCode: error?.statusCode ?? 500, stack: error instanceof Error ? error.stack : String(error), url: typeof window !== 'undefined' ? window.location.pathname : '' };
|
|
804
|
+
const errorProps = { error, retry, params: paramValues, statusCode: error?.statusCode ?? 500, stack: error instanceof Error ? error.stack : String(error), url: typeof window !== 'undefined' ? window.location.pathname : '', offline: error?.offline === true, networkState: getNetworkState() };
|
|
608
805
|
const renderErrorChain = (index) => {
|
|
609
806
|
if (index >= layoutNodes.length) {
|
|
610
807
|
const result = errorFn(errorProps, new Map(), walker);
|
|
@@ -678,7 +875,10 @@ async function hydrateInitial(router, match, container, strategy) {
|
|
|
678
875
|
})();
|
|
679
876
|
set(_state.params, paramValues);
|
|
680
877
|
set(_state.path, match.pathname || window.location.pathname);
|
|
681
|
-
|
|
878
|
+
{
|
|
879
|
+
const s = window.location.search || '';
|
|
880
|
+
set(_state.search, s.startsWith('?') ? s.slice(1) : s);
|
|
881
|
+
}
|
|
682
882
|
if (ssrErrorMarker !== null) {
|
|
683
883
|
let message = 'Internal Server Error';
|
|
684
884
|
if (ssrErrorMarker.startsWith(':')) {
|
|
@@ -695,8 +895,13 @@ async function hydrateInitial(router, match, container, strategy) {
|
|
|
695
895
|
const layoutNodes = chain.filter(n => n.layout);
|
|
696
896
|
const hydrators = router.__hydrators;
|
|
697
897
|
if (!pageNode.page && !(hydrators && pageNode._pageName && hydrators[pageNode._pageName])) {
|
|
698
|
-
// The route's component chunk failed to load or compile.
|
|
699
|
-
//
|
|
898
|
+
// The route's component chunk failed to load or compile. While
|
|
899
|
+
// offline that is a connectivity problem — show the offline
|
|
900
|
+
// experience; otherwise render the error boundary for this route.
|
|
901
|
+
if (await looksOffline()) {
|
|
902
|
+
await renderOfflinePage(router, match, container);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
700
905
|
const chunkErr = chunkErrorForNode(pageNode);
|
|
701
906
|
await renderErrorPage(router, match, container, chunkErr || new Error(`Component "${pageNode._pageName}" is unavailable`));
|
|
702
907
|
return;
|
|
@@ -716,7 +921,10 @@ async function hydrateInitial(router, match, container, strategy) {
|
|
|
716
921
|
if (!strategy || strategy === 'full') {
|
|
717
922
|
const walker = createHydrateWalker(container);
|
|
718
923
|
setIsHydrating(true);
|
|
719
|
-
await runInBlockWindow(() =>
|
|
924
|
+
await runInBlockWindow(() => {
|
|
925
|
+
const result = hydPage(pageProps, new Map(), walker);
|
|
926
|
+
storePageInstance(router, pageNode, pageProps, result);
|
|
927
|
+
});
|
|
720
928
|
setIsHydrating(false);
|
|
721
929
|
}
|
|
722
930
|
else if (strategy === 'viewport') {
|
|
@@ -741,7 +949,10 @@ async function hydrateInitial(router, match, container, strategy) {
|
|
|
741
949
|
if (index >= layoutNodes.length) {
|
|
742
950
|
return (subWalker) => {
|
|
743
951
|
if (!strategy || strategy === 'full') {
|
|
744
|
-
|
|
952
|
+
const pageProps = { params: paramValues, ...pageNode.props };
|
|
953
|
+
const result = hydPage(pageProps, new Map(), subWalker);
|
|
954
|
+
storePageInstance(router, pageNode, pageProps, result);
|
|
955
|
+
return result;
|
|
745
956
|
}
|
|
746
957
|
else if (strategy === 'viewport') {
|
|
747
958
|
hydrateViewport(subWalker.root, hydPage, { params: paramValues, ...pageNode.props });
|
|
@@ -759,7 +970,9 @@ async function hydrateInitial(router, match, container, strategy) {
|
|
|
759
970
|
const hydLayout = hydLayouts[index];
|
|
760
971
|
const childHydrator = renderLayoutChain(index + 1);
|
|
761
972
|
const layoutProps = { children: childHydrator, params: paramValues };
|
|
762
|
-
|
|
973
|
+
const result = hydLayout(layoutProps, new Map(), walker);
|
|
974
|
+
storeLayoutInstance(router, node, layoutProps, result);
|
|
975
|
+
return result;
|
|
763
976
|
}
|
|
764
977
|
const walker = createHydrateWalker(container);
|
|
765
978
|
await runInBlockWindow(() => renderLayoutChain(0));
|
|
@@ -782,6 +995,20 @@ async function hydrateInitial(router, match, container, strategy) {
|
|
|
782
995
|
}
|
|
783
996
|
}
|
|
784
997
|
}
|
|
998
|
+
function scrollToHash() {
|
|
999
|
+
const h = typeof window !== 'undefined' ? window.location.hash : '';
|
|
1000
|
+
if (!h || h === '#')
|
|
1001
|
+
return;
|
|
1002
|
+
const id = h.slice(1);
|
|
1003
|
+
setTimeout(() => {
|
|
1004
|
+
try {
|
|
1005
|
+
const el = document.getElementById(id);
|
|
1006
|
+
if (el && typeof el.scrollIntoView === 'function')
|
|
1007
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1008
|
+
}
|
|
1009
|
+
catch { /* element may not exist */ }
|
|
1010
|
+
}, 80);
|
|
1011
|
+
}
|
|
785
1012
|
export function createRouter(routes, options = {}) {
|
|
786
1013
|
const container = options.container || document.getElementById('root');
|
|
787
1014
|
const prefetch = options.prefetch !== false;
|
|
@@ -795,6 +1022,51 @@ export function createRouter(routes, options = {}) {
|
|
|
795
1022
|
_currentSegments: null,
|
|
796
1023
|
_depth: 0,
|
|
797
1024
|
_routeDataCache: options.routeDataCache ?? 0,
|
|
1025
|
+
_offlineUI: (options.offline ?? null),
|
|
1026
|
+
_beforeGuards: [],
|
|
1027
|
+
_viewTransitions: options.viewTransitions === true,
|
|
1028
|
+
beforeEach(fn) {
|
|
1029
|
+
this._beforeGuards.push(fn);
|
|
1030
|
+
let active = true;
|
|
1031
|
+
return () => {
|
|
1032
|
+
if (!active)
|
|
1033
|
+
return;
|
|
1034
|
+
active = false;
|
|
1035
|
+
const guards = this._beforeGuards;
|
|
1036
|
+
const i = guards.indexOf(fn);
|
|
1037
|
+
if (i > -1)
|
|
1038
|
+
guards.splice(i, 1);
|
|
1039
|
+
};
|
|
1040
|
+
},
|
|
1041
|
+
_runGuards(to) {
|
|
1042
|
+
const from = window.location.pathname;
|
|
1043
|
+
for (const g of this._beforeGuards ?? []) {
|
|
1044
|
+
const d = g(to, from);
|
|
1045
|
+
if (d instanceof Promise) {
|
|
1046
|
+
return d.then((r) => {
|
|
1047
|
+
if (r === false || typeof r === 'string')
|
|
1048
|
+
return r;
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
if (d === false || typeof d === 'string')
|
|
1052
|
+
return d;
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
go(n) {
|
|
1056
|
+
window.history.go(n);
|
|
1057
|
+
},
|
|
1058
|
+
get route() {
|
|
1059
|
+
const m = this._currentMatch;
|
|
1060
|
+
if (!m)
|
|
1061
|
+
return null;
|
|
1062
|
+
const chain = m.matchChain ?? [];
|
|
1063
|
+
const deepest = chain[chain.length - 1];
|
|
1064
|
+
return {
|
|
1065
|
+
pathname: m.pathname || '',
|
|
1066
|
+
params: m.params,
|
|
1067
|
+
pattern: deepest?.fullPath || '/',
|
|
1068
|
+
};
|
|
1069
|
+
},
|
|
798
1070
|
start() {
|
|
799
1071
|
setCurrentRouter(this);
|
|
800
1072
|
if (typeof window !== 'undefined' && 'scrollRestoration' in window.history) {
|
|
@@ -813,24 +1085,52 @@ export function createRouter(routes, options = {}) {
|
|
|
813
1085
|
}, 100);
|
|
814
1086
|
}, { passive: true });
|
|
815
1087
|
}
|
|
1088
|
+
// Navigation policy: SPA navigation happens through <Link>/
|
|
1089
|
+
// <NavLink>/navigate() and opt-in plain anchors
|
|
1090
|
+
// `<a href="..." no-reload>`. Plain <a href> without `no-reload`
|
|
1091
|
+
// (or `data-no-reload`) always does native browser navigation.
|
|
1092
|
+
// `no-reload` anchors are intercepted via a delegated document
|
|
1093
|
+
// click listener that respects modifier keys, target="_blank",
|
|
1094
|
+
// download, external origins, hash/mailto/tel schemes.
|
|
1095
|
+
window.addEventListener('popstate', () => {
|
|
1096
|
+
setIsPopStateNavigation(true);
|
|
1097
|
+
this.navigate(window.location.href, { replace: true });
|
|
1098
|
+
});
|
|
816
1099
|
document.addEventListener('click', (e) => {
|
|
817
|
-
const
|
|
818
|
-
if (!
|
|
1100
|
+
const target = e.target;
|
|
1101
|
+
if (!target || target.nodeType !== 1)
|
|
1102
|
+
return;
|
|
1103
|
+
const anchor = target.closest('a[href]');
|
|
1104
|
+
if (!anchor)
|
|
1105
|
+
return;
|
|
1106
|
+
if (!anchor.hasAttribute('no-reload') && !anchor.hasAttribute('data-no-reload'))
|
|
1107
|
+
return;
|
|
1108
|
+
if (e.defaultPrevented)
|
|
819
1109
|
return;
|
|
820
|
-
|
|
1110
|
+
const me = e;
|
|
1111
|
+
if (me.metaKey || me.ctrlKey || me.shiftKey || me.altKey || me.button !== 0)
|
|
821
1112
|
return;
|
|
822
|
-
|
|
823
|
-
if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:'))
|
|
1113
|
+
if (anchor.target === '_blank')
|
|
824
1114
|
return;
|
|
825
|
-
if (
|
|
1115
|
+
if (anchor.hasAttribute('download'))
|
|
826
1116
|
return;
|
|
1117
|
+
if (anchor.getAttribute('rel') === 'external')
|
|
1118
|
+
return;
|
|
1119
|
+
const href = anchor.getAttribute('href');
|
|
1120
|
+
if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('javascript:'))
|
|
1121
|
+
return;
|
|
1122
|
+
try {
|
|
1123
|
+
const url = new URL(href, window.location.origin);
|
|
1124
|
+
if (url.origin !== window.location.origin)
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
catch {
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
827
1130
|
e.preventDefault();
|
|
1131
|
+
e.stopPropagation();
|
|
828
1132
|
this.navigate(href);
|
|
829
1133
|
});
|
|
830
|
-
window.addEventListener('popstate', () => {
|
|
831
|
-
setIsPopStateNavigation(true);
|
|
832
|
-
this.navigate(window.location.href, { replace: true });
|
|
833
|
-
});
|
|
834
1134
|
if (prefetch) {
|
|
835
1135
|
document.addEventListener('mouseenter', (e) => {
|
|
836
1136
|
const link = e.target?.nodeType === 1 ? e.target.closest('a[href]') : null;
|
|
@@ -864,74 +1164,133 @@ export function createRouter(routes, options = {}) {
|
|
|
864
1164
|
return;
|
|
865
1165
|
}
|
|
866
1166
|
match.pathname = url.pathname;
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
let firstRenderFailed = false;
|
|
874
|
-
const updateUrl = () => {
|
|
875
|
-
if (!opts.replace) {
|
|
876
|
-
window.history.pushState({ path: url.pathname }, '', url.pathname);
|
|
877
|
-
}
|
|
878
|
-
else {
|
|
879
|
-
window.history.replaceState({ path: url.pathname }, '', url.pathname);
|
|
1167
|
+
// Navigation guards (programmatic/Link navigations only).
|
|
1168
|
+
const self = this;
|
|
1169
|
+
const guardResult = self._runGuards(url.pathname);
|
|
1170
|
+
const proceedNav = () => {
|
|
1171
|
+
if (!_isPopStateNavigation) {
|
|
1172
|
+
_scrollPositions.set(window.location.pathname, window.scrollY);
|
|
880
1173
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1174
|
+
const loadingFn = findLoadingComponent(match.matchChain);
|
|
1175
|
+
this._navToken = (this._navToken || 0) + 1;
|
|
1176
|
+
const navToken = this._navToken;
|
|
1177
|
+
loadingStart();
|
|
1178
|
+
let firstRenderFailed = false;
|
|
1179
|
+
const updateUrl = () => {
|
|
1180
|
+
if (!opts.replace) {
|
|
1181
|
+
window.history.pushState({ path: url.pathname }, '', url.pathname + url.search);
|
|
1182
|
+
}
|
|
1183
|
+
else {
|
|
1184
|
+
window.history.replaceState({ path: url.pathname }, '', url.pathname + url.search);
|
|
1185
|
+
}
|
|
1186
|
+
set(_state.path, url.pathname);
|
|
1187
|
+
set(_state.search, url.search.startsWith('?') ? url.search.slice(1) : url.search);
|
|
1188
|
+
};
|
|
1189
|
+
const renderContent = () => {
|
|
1190
|
+
try {
|
|
1191
|
+
renderMatch(this, match, this.container);
|
|
1192
|
+
}
|
|
1193
|
+
catch (e) {
|
|
1194
|
+
firstRenderFailed = true;
|
|
1195
|
+
throw e;
|
|
1196
|
+
}
|
|
1197
|
+
this._currentMatch = match;
|
|
1198
|
+
clearOfflineFlag(this, container);
|
|
1199
|
+
handleScroll(url.pathname, opts.replace);
|
|
1200
|
+
};
|
|
1201
|
+
const swapView = (fn) => {
|
|
1202
|
+
const d = document;
|
|
1203
|
+
if (this._viewTransitions && typeof d.startViewTransition === 'function')
|
|
1204
|
+
d.startViewTransition(fn);
|
|
1205
|
+
else
|
|
1206
|
+
fn();
|
|
1207
|
+
};
|
|
1208
|
+
const doRender = () => {
|
|
1209
|
+
updateUrl();
|
|
1210
|
+
swapView(() => renderContent());
|
|
1211
|
+
scrollToHash();
|
|
1212
|
+
};
|
|
1213
|
+
const fetchData = () => {
|
|
1214
|
+
if (firstRenderFailed)
|
|
1215
|
+
return;
|
|
1216
|
+
if (!shouldFetchData(this, match)) {
|
|
1217
|
+
loadingFinish();
|
|
1218
|
+
scrollToHash();
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
getRouteData(url.pathname + url.search, 'nav' + navToken).then(async (data) => {
|
|
1222
|
+
if (navToken !== this._navToken)
|
|
1223
|
+
return;
|
|
1224
|
+
if (!data) {
|
|
1225
|
+
loadingFinish();
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
if (data.redirect) {
|
|
1229
|
+
loadingFinish();
|
|
1230
|
+
this.navigate(data.redirect, { replace: true });
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
if (!hasRealPageData(data)) {
|
|
1234
|
+
loadingFinish();
|
|
1235
|
+
scrollToHash();
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
await swapView(() => applyRouteData(this, match, data, this.container));
|
|
1239
|
+
scrollToHash();
|
|
1240
|
+
loadingFinish({ error: !!data.error });
|
|
1241
|
+
}).catch(() => loadingFinish({ error: true }));
|
|
1242
|
+
};
|
|
1243
|
+
if (loadingFn) {
|
|
1244
|
+
showLoadingInContainer(this.container, loadingFn, match.params);
|
|
1245
|
+
Promise.resolve().then(() => { try {
|
|
1246
|
+
doRender();
|
|
1247
|
+
}
|
|
1248
|
+
finally {
|
|
1249
|
+
fetchData();
|
|
1250
|
+
} });
|
|
887
1251
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1252
|
+
else {
|
|
1253
|
+
try {
|
|
1254
|
+
doRender();
|
|
1255
|
+
}
|
|
1256
|
+
finally {
|
|
1257
|
+
fetchData();
|
|
1258
|
+
}
|
|
891
1259
|
}
|
|
892
|
-
this._currentMatch = match;
|
|
893
|
-
handleScroll(url.pathname, opts.replace);
|
|
894
|
-
};
|
|
895
|
-
const doRender = () => {
|
|
896
|
-
updateUrl();
|
|
897
|
-
renderContent();
|
|
898
1260
|
};
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
if (navToken !== this._navToken)
|
|
906
|
-
return;
|
|
907
|
-
if (!data)
|
|
1261
|
+
// If guards return a Promise, chain the rest asynchronously.
|
|
1262
|
+
// If sync (the common case), the navigation completes synchronously,
|
|
1263
|
+
// preserving backward compatibility with callers that don't await.
|
|
1264
|
+
if (guardResult instanceof Promise) {
|
|
1265
|
+
return guardResult.then((decision) => {
|
|
1266
|
+
if (decision === false)
|
|
908
1267
|
return;
|
|
909
|
-
if (
|
|
910
|
-
|
|
1268
|
+
if (typeof decision === 'string' && decision !== url.pathname && (self._guardDepth ?? 0) < 5) {
|
|
1269
|
+
self._guardDepth = (self._guardDepth ?? 0) + 1;
|
|
1270
|
+
try {
|
|
1271
|
+
self.navigate(decision, { replace: true });
|
|
1272
|
+
}
|
|
1273
|
+
finally {
|
|
1274
|
+
self._guardDepth = 0;
|
|
1275
|
+
}
|
|
911
1276
|
return;
|
|
912
1277
|
}
|
|
913
|
-
|
|
914
|
-
return;
|
|
915
|
-
await applyRouteData(this, match, data, this.container);
|
|
1278
|
+
proceedNav();
|
|
916
1279
|
});
|
|
917
|
-
};
|
|
918
|
-
if (loadingFn) {
|
|
919
|
-
showLoadingInContainer(this.container, loadingFn, match.params);
|
|
920
|
-
Promise.resolve().then(() => { try {
|
|
921
|
-
doRender();
|
|
922
|
-
}
|
|
923
|
-
finally {
|
|
924
|
-
fetchData();
|
|
925
|
-
} });
|
|
926
1280
|
}
|
|
927
|
-
|
|
1281
|
+
if (guardResult === false)
|
|
1282
|
+
return;
|
|
1283
|
+
if (typeof guardResult === 'string' && guardResult !== url.pathname && (self._guardDepth ?? 0) < 5) {
|
|
1284
|
+
self._guardDepth = (self._guardDepth ?? 0) + 1;
|
|
928
1285
|
try {
|
|
929
|
-
|
|
1286
|
+
self.navigate(guardResult, { replace: true });
|
|
930
1287
|
}
|
|
931
1288
|
finally {
|
|
932
|
-
|
|
1289
|
+
self._guardDepth = 0;
|
|
933
1290
|
}
|
|
1291
|
+
return;
|
|
934
1292
|
}
|
|
1293
|
+
proceedNav();
|
|
935
1294
|
},
|
|
936
1295
|
prefetch(path) {
|
|
937
1296
|
const url = new URL(path, window.location.origin);
|
|
@@ -951,6 +1310,10 @@ export function createRouter(routes, options = {}) {
|
|
|
951
1310
|
get currentPath() {
|
|
952
1311
|
return get(_state.path);
|
|
953
1312
|
},
|
|
1313
|
+
get isLoading() {
|
|
1314
|
+
const cells = getLoadingState();
|
|
1315
|
+
return Boolean(get(cells.isLoading));
|
|
1316
|
+
},
|
|
954
1317
|
hmrUpdate() {
|
|
955
1318
|
const updated = globalThis.__updatedComponents;
|
|
956
1319
|
if (!updated || updated.size === 0)
|
|
@@ -1014,6 +1377,51 @@ export function createFileRouter(routeTree, options = {}) {
|
|
|
1014
1377
|
_currentSegments: null,
|
|
1015
1378
|
_depth: 0,
|
|
1016
1379
|
_routeDataCache: options.routeDataCache ?? 0,
|
|
1380
|
+
_offlineUI: (options.offline ?? null),
|
|
1381
|
+
_beforeGuards: [],
|
|
1382
|
+
_viewTransitions: options.viewTransitions === true,
|
|
1383
|
+
beforeEach(fn) {
|
|
1384
|
+
this._beforeGuards.push(fn);
|
|
1385
|
+
let active = true;
|
|
1386
|
+
return () => {
|
|
1387
|
+
if (!active)
|
|
1388
|
+
return;
|
|
1389
|
+
active = false;
|
|
1390
|
+
const guards = this._beforeGuards;
|
|
1391
|
+
const i = guards.indexOf(fn);
|
|
1392
|
+
if (i > -1)
|
|
1393
|
+
guards.splice(i, 1);
|
|
1394
|
+
};
|
|
1395
|
+
},
|
|
1396
|
+
_runGuards(to) {
|
|
1397
|
+
const from = window.location.pathname;
|
|
1398
|
+
for (const g of this._beforeGuards ?? []) {
|
|
1399
|
+
const d = g(to, from);
|
|
1400
|
+
if (d instanceof Promise) {
|
|
1401
|
+
return d.then((r) => {
|
|
1402
|
+
if (r === false || typeof r === 'string')
|
|
1403
|
+
return r;
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
if (d === false || typeof d === 'string')
|
|
1407
|
+
return d;
|
|
1408
|
+
}
|
|
1409
|
+
},
|
|
1410
|
+
go(n) {
|
|
1411
|
+
window.history.go(n);
|
|
1412
|
+
},
|
|
1413
|
+
get route() {
|
|
1414
|
+
const m = router._currentMatch;
|
|
1415
|
+
if (!m)
|
|
1416
|
+
return null;
|
|
1417
|
+
const chain = m.matchChain ?? [];
|
|
1418
|
+
const deepest = chain[chain.length - 1];
|
|
1419
|
+
return {
|
|
1420
|
+
pathname: m.pathname || '',
|
|
1421
|
+
params: m.params,
|
|
1422
|
+
pattern: deepest?.fullPath || '/',
|
|
1423
|
+
};
|
|
1424
|
+
},
|
|
1017
1425
|
start() {
|
|
1018
1426
|
setCurrentRouter(this);
|
|
1019
1427
|
if (typeof window !== 'undefined' && 'scrollRestoration' in window.history) {
|
|
@@ -1037,6 +1445,41 @@ export function createFileRouter(routeTree, options = {}) {
|
|
|
1037
1445
|
setIsPopStateNavigation(true);
|
|
1038
1446
|
router.navigate(window.location.pathname + window.location.search, { replace: true });
|
|
1039
1447
|
});
|
|
1448
|
+
document.addEventListener('click', (e) => {
|
|
1449
|
+
const target = e.target;
|
|
1450
|
+
if (!target || target.nodeType !== 1)
|
|
1451
|
+
return;
|
|
1452
|
+
const anchor = target.closest('a[href]');
|
|
1453
|
+
if (!anchor)
|
|
1454
|
+
return;
|
|
1455
|
+
if (!anchor.hasAttribute('no-reload') && !anchor.hasAttribute('data-no-reload'))
|
|
1456
|
+
return;
|
|
1457
|
+
if (e.defaultPrevented)
|
|
1458
|
+
return;
|
|
1459
|
+
const me = e;
|
|
1460
|
+
if (me.metaKey || me.ctrlKey || me.shiftKey || me.altKey || me.button !== 0)
|
|
1461
|
+
return;
|
|
1462
|
+
if (anchor.target === '_blank')
|
|
1463
|
+
return;
|
|
1464
|
+
if (anchor.hasAttribute('download'))
|
|
1465
|
+
return;
|
|
1466
|
+
if (anchor.getAttribute('rel') === 'external')
|
|
1467
|
+
return;
|
|
1468
|
+
const href = anchor.getAttribute('href');
|
|
1469
|
+
if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('javascript:'))
|
|
1470
|
+
return;
|
|
1471
|
+
try {
|
|
1472
|
+
const url = new URL(href, window.location.origin);
|
|
1473
|
+
if (url.origin !== window.location.origin)
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
catch {
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
e.preventDefault();
|
|
1480
|
+
e.stopPropagation();
|
|
1481
|
+
router.navigate(href);
|
|
1482
|
+
});
|
|
1040
1483
|
if (options.prefetch !== false) {
|
|
1041
1484
|
document.addEventListener('mouseenter', (e) => {
|
|
1042
1485
|
const link = e.target?.nodeType === 1 ? e.target.closest('a[href]') : null;
|
|
@@ -1092,167 +1535,225 @@ export function createFileRouter(routeTree, options = {}) {
|
|
|
1092
1535
|
return;
|
|
1093
1536
|
}
|
|
1094
1537
|
match.pathname = url.pathname;
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const loadingFn = findLoadingComponent(match.matchChain);
|
|
1099
|
-
router._navToken = (router._navToken || 0) + 1;
|
|
1100
|
-
navDebug('navigate', url.pathname, 'token=' + router._navToken, 'pendingChunks', hasPendingChunks(match.matchChain));
|
|
1101
|
-
const navToken = router._navToken;
|
|
1102
|
-
const middlewareFns = Array.isArray(middleware) ? middleware : (middleware ? [middleware] : []);
|
|
1103
|
-
let firstRenderFailed = false;
|
|
1104
|
-
const updateUrl = () => {
|
|
1105
|
-
const fullUrl = url.pathname + url.search;
|
|
1106
|
-
if (!opts.replace) {
|
|
1107
|
-
window.history.pushState({ path: fullUrl }, '', fullUrl);
|
|
1538
|
+
const afterGuards = () => {
|
|
1539
|
+
if (!_isPopStateNavigation) {
|
|
1540
|
+
_scrollPositions.set(window.location.pathname, window.scrollY);
|
|
1108
1541
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1542
|
+
const swapView = (fn) => {
|
|
1543
|
+
const d = document;
|
|
1544
|
+
if (router._viewTransitions && typeof d.startViewTransition === 'function')
|
|
1545
|
+
d.startViewTransition(fn);
|
|
1546
|
+
else
|
|
1547
|
+
fn();
|
|
1548
|
+
};
|
|
1549
|
+
const loadingFn = findLoadingComponent(match.matchChain);
|
|
1550
|
+
router._navToken = (router._navToken || 0) + 1;
|
|
1551
|
+
navDebug('navigate', url.pathname, 'token=' + router._navToken, 'pendingChunks', hasPendingChunks(match.matchChain));
|
|
1552
|
+
const navToken = router._navToken;
|
|
1553
|
+
loadingStart();
|
|
1554
|
+
const middlewareFns = Array.isArray(middleware) ? middleware : (middleware ? [middleware] : []);
|
|
1555
|
+
let firstRenderFailed = false;
|
|
1556
|
+
const updateUrl = () => {
|
|
1557
|
+
const fullUrl = url.pathname + url.search;
|
|
1558
|
+
if (!opts.replace) {
|
|
1559
|
+
window.history.pushState({ path: fullUrl }, '', fullUrl);
|
|
1560
|
+
}
|
|
1561
|
+
else {
|
|
1562
|
+
window.history.replaceState({ path: fullUrl }, '', fullUrl);
|
|
1563
|
+
}
|
|
1564
|
+
set(_state.path, url.pathname);
|
|
1565
|
+
set(_state.search, url.search.startsWith('?') ? url.search.slice(1) : url.search);
|
|
1566
|
+
};
|
|
1567
|
+
const renderContent = () => {
|
|
1568
|
+
const propsNode = findPageNode(match);
|
|
1569
|
+
if (propsNode && propsNode._dataPath !== url.pathname) {
|
|
1570
|
+
propsNode.props = undefined;
|
|
1571
|
+
}
|
|
1572
|
+
try {
|
|
1573
|
+
container.__veskMountToken = navToken;
|
|
1574
|
+
renderFn(router, match, container, navToken);
|
|
1575
|
+
}
|
|
1576
|
+
catch (e) {
|
|
1577
|
+
firstRenderFailed = true;
|
|
1578
|
+
throw e;
|
|
1579
|
+
}
|
|
1580
|
+
router._currentMatch = match;
|
|
1581
|
+
clearOfflineFlag(router, container);
|
|
1582
|
+
handleScroll(url.pathname, opts.replace);
|
|
1583
|
+
};
|
|
1584
|
+
const doRender = () => {
|
|
1585
|
+
navDebug('doRender', url.pathname, 'token=' + navToken);
|
|
1586
|
+
updateUrl();
|
|
1587
|
+
swapView(() => renderContent());
|
|
1588
|
+
scrollToHash();
|
|
1589
|
+
navDebug('painted', url.pathname);
|
|
1590
|
+
};
|
|
1591
|
+
const fetchData = () => {
|
|
1592
|
+
if (firstRenderFailed)
|
|
1157
1593
|
return;
|
|
1158
|
-
|
|
1159
|
-
|
|
1594
|
+
navDebug('fetchData?', url.pathname, String(shouldFetchData(router, match)));
|
|
1595
|
+
if (!shouldFetchData(router, match)) {
|
|
1596
|
+
loadingFinish();
|
|
1597
|
+
scrollToHash();
|
|
1160
1598
|
return;
|
|
1161
1599
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
const pending = hasPendingChunks(match.matchChain);
|
|
1165
|
-
if (pending.length > 0) {
|
|
1166
|
-
await loadChunksQuietly(pending);
|
|
1600
|
+
getRouteData(url.pathname + url.search, 'nav' + navToken).then(async (data) => {
|
|
1601
|
+
navDebug('data arrived', url.pathname, 'token=' + navToken, 'current=' + router._navToken, 'realProps', JSON.stringify(data && hasRealPageData(data)));
|
|
1167
1602
|
if (navToken !== router._navToken)
|
|
1168
1603
|
return;
|
|
1169
|
-
if (
|
|
1170
|
-
|
|
1604
|
+
if (!data) {
|
|
1605
|
+
loadingFinish();
|
|
1606
|
+
return;
|
|
1171
1607
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
updateUrl();
|
|
1180
|
-
return loadChunksQuietly(pendingChunks).then(() => {
|
|
1181
|
-
// A newer navigation (e.g. popstate back while the chunk
|
|
1182
|
-
// was still loading) may have superseded this one; don't
|
|
1183
|
-
// paint the stale route over the current URL.
|
|
1184
|
-
if (navToken !== router._navToken)
|
|
1608
|
+
if (data.redirect) {
|
|
1609
|
+
loadingFinish();
|
|
1610
|
+
router.navigate(data.redirect, { replace: true });
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (!hasRealPageData(data)) {
|
|
1614
|
+
loadingFinish();
|
|
1185
1615
|
return;
|
|
1616
|
+
}
|
|
1617
|
+
const pending = hasPendingChunks(match.matchChain);
|
|
1618
|
+
if (pending.length > 0) {
|
|
1619
|
+
await loadChunksQuietly(pending);
|
|
1620
|
+
if (navToken !== router._navToken)
|
|
1621
|
+
return;
|
|
1622
|
+
if (typeof router.__updateComponents === 'function') {
|
|
1623
|
+
router.__updateComponents(match.matchChain);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
await swapView(() => applyRouteData(router, match, data, container, renderFn));
|
|
1627
|
+
scrollToHash();
|
|
1628
|
+
loadingFinish({ error: !!data.error });
|
|
1629
|
+
}).catch(() => loadingFinish({ error: true }));
|
|
1630
|
+
};
|
|
1631
|
+
const pendingChunks = hasPendingChunks(match.matchChain);
|
|
1632
|
+
const handleNavFailure = (err) => {
|
|
1633
|
+
navDebug('handleNavFailure', String(err?.message || err).slice(0, 80));
|
|
1634
|
+
if (navToken !== router._navToken)
|
|
1635
|
+
return;
|
|
1636
|
+
loadingFinish({ error: true });
|
|
1637
|
+
looksOffline().then((offline) => {
|
|
1638
|
+
navDebug('handleNavFailure verdict', offline ? 'offline' : 'online');
|
|
1639
|
+
if (offline)
|
|
1640
|
+
return void renderOfflinePage(router, match, container);
|
|
1641
|
+
return void renderErrorPage(router, match, container, err);
|
|
1642
|
+
}).catch(() => { });
|
|
1643
|
+
};
|
|
1644
|
+
const doRenderWithChunks = pendingChunks.length > 0
|
|
1645
|
+
? () => {
|
|
1646
|
+
updateUrl();
|
|
1647
|
+
return loadChunksQuietly(pendingChunks).then(() => {
|
|
1648
|
+
if (navToken !== router._navToken)
|
|
1649
|
+
return;
|
|
1650
|
+
if (typeof router.__updateComponents === 'function') {
|
|
1651
|
+
router.__updateComponents(match.matchChain);
|
|
1652
|
+
}
|
|
1653
|
+
try {
|
|
1654
|
+
renderContent();
|
|
1655
|
+
loadingFinish();
|
|
1656
|
+
}
|
|
1657
|
+
catch (e) {
|
|
1658
|
+
handleNavFailure(e);
|
|
1659
|
+
}
|
|
1660
|
+
}, (e) => {
|
|
1661
|
+
handleNavFailure(e);
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
: (() => {
|
|
1186
1665
|
if (typeof router.__updateComponents === 'function') {
|
|
1187
1666
|
router.__updateComponents(match.matchChain);
|
|
1188
1667
|
}
|
|
1189
|
-
|
|
1668
|
+
doRender();
|
|
1190
1669
|
});
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
// rather than string names (code-split __resolveNames only
|
|
1196
|
-
// stores _pageName; __updateComponents turns it into a ref).
|
|
1197
|
-
if (typeof router.__updateComponents === 'function') {
|
|
1198
|
-
router.__updateComponents(match.matchChain);
|
|
1670
|
+
async function runMwChain(index) {
|
|
1671
|
+
if (index >= middlewareFns.length) {
|
|
1672
|
+
await doRenderWithChunks();
|
|
1673
|
+
return;
|
|
1199
1674
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
const ctx = { url: url.pathname, params: match.params, router, locals: {} };
|
|
1209
|
-
async function next(rewrite) {
|
|
1210
|
-
if (rewrite) {
|
|
1211
|
-
match.pathname = rewrite;
|
|
1212
|
-
url.pathname = rewrite;
|
|
1675
|
+
const fn = middlewareFns[index];
|
|
1676
|
+
const ctx = { url: url.pathname, params: match.params, router, locals: {} };
|
|
1677
|
+
async function next(rewrite) {
|
|
1678
|
+
if (rewrite) {
|
|
1679
|
+
match.pathname = rewrite;
|
|
1680
|
+
url.pathname = rewrite;
|
|
1681
|
+
}
|
|
1682
|
+
return runMwChain(index + 1);
|
|
1213
1683
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
return;
|
|
1684
|
+
try {
|
|
1685
|
+
const result = await fn(ctx, next);
|
|
1686
|
+
if (result && result.redirect) {
|
|
1687
|
+
router.navigate(result.redirect, { replace: true });
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1221
1690
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1691
|
+
catch (e) {
|
|
1692
|
+
if (e && e.name === 'Redirect') {
|
|
1693
|
+
router.navigate(e.url, { replace: true });
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1227
1696
|
}
|
|
1228
1697
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1698
|
+
if (middlewareFns.length > 0 || loadingFn) {
|
|
1699
|
+
if (loadingFn) {
|
|
1700
|
+
showLoadingInContainer(container, loadingFn, match.params);
|
|
1701
|
+
}
|
|
1702
|
+
Promise.resolve().then(() => {
|
|
1703
|
+
try {
|
|
1704
|
+
if (middlewareFns.length > 0) {
|
|
1705
|
+
runMwChain(0);
|
|
1706
|
+
}
|
|
1707
|
+
else {
|
|
1708
|
+
doRenderWithChunks();
|
|
1709
|
+
}
|
|
1238
1710
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1711
|
+
finally {
|
|
1712
|
+
fetchData();
|
|
1241
1713
|
}
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
else {
|
|
1717
|
+
try {
|
|
1718
|
+
doRenderWithChunks();
|
|
1242
1719
|
}
|
|
1243
1720
|
finally {
|
|
1244
1721
|
fetchData();
|
|
1245
1722
|
}
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
// Navigation guards — sync fast path for sync guard functions.
|
|
1726
|
+
const guardResult = router._runGuards(url.pathname);
|
|
1727
|
+
if (guardResult instanceof Promise) {
|
|
1728
|
+
return guardResult.then((decision) => {
|
|
1729
|
+
if (decision === false)
|
|
1730
|
+
return;
|
|
1731
|
+
if (typeof decision === 'string' && decision !== url.pathname && (router._guardDepth || 0) < 5) {
|
|
1732
|
+
router._guardDepth = (router._guardDepth || 0) + 1;
|
|
1733
|
+
try {
|
|
1734
|
+
router.navigate(decision, { replace: true });
|
|
1735
|
+
}
|
|
1736
|
+
finally {
|
|
1737
|
+
router._guardDepth = 0;
|
|
1738
|
+
}
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
afterGuards();
|
|
1246
1742
|
});
|
|
1247
1743
|
}
|
|
1248
|
-
|
|
1744
|
+
if (guardResult === false)
|
|
1745
|
+
return;
|
|
1746
|
+
if (typeof guardResult === 'string' && guardResult !== url.pathname && (router._guardDepth || 0) < 5) {
|
|
1747
|
+
router._guardDepth = (router._guardDepth || 0) + 1;
|
|
1249
1748
|
try {
|
|
1250
|
-
|
|
1749
|
+
router.navigate(guardResult, { replace: true });
|
|
1251
1750
|
}
|
|
1252
1751
|
finally {
|
|
1253
|
-
|
|
1752
|
+
router._guardDepth = 0;
|
|
1254
1753
|
}
|
|
1754
|
+
return;
|
|
1255
1755
|
}
|
|
1756
|
+
afterGuards();
|
|
1256
1757
|
},
|
|
1257
1758
|
prefetch(path) {
|
|
1258
1759
|
const url = new URL(path, window.location.origin);
|
|
@@ -1274,6 +1775,10 @@ export function createFileRouter(routeTree, options = {}) {
|
|
|
1274
1775
|
get currentPath() {
|
|
1275
1776
|
return get(_state.path);
|
|
1276
1777
|
},
|
|
1778
|
+
get isLoading() {
|
|
1779
|
+
const cells = getLoadingState();
|
|
1780
|
+
return Boolean(get(cells.isLoading));
|
|
1781
|
+
},
|
|
1277
1782
|
hmrUpdate() {
|
|
1278
1783
|
const updated = globalThis.__updatedComponents;
|
|
1279
1784
|
if (!updated || updated.size === 0)
|
|
@@ -1282,43 +1787,15 @@ export function createFileRouter(routeTree, options = {}) {
|
|
|
1282
1787
|
if (typeof router.__updateComponents === 'function') {
|
|
1283
1788
|
router.__updateComponents(router.routeTree);
|
|
1284
1789
|
}
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
const isPage = inst.type === 'page';
|
|
1293
|
-
const newFn = isPage ? inst.node.page : inst.node.layout;
|
|
1294
|
-
if (!newFn)
|
|
1295
|
-
continue;
|
|
1296
|
-
const walker = createHydrateWalker(document.createDocumentFragment(), []);
|
|
1297
|
-
let newDom;
|
|
1298
|
-
root(() => {
|
|
1299
|
-
newDom = newFn(inst.props, new Map(), walker);
|
|
1300
|
-
});
|
|
1301
|
-
if (newDom && newDom.nodeType === 1 && inst.root && inst.root.parentNode) {
|
|
1302
|
-
inst.root.parentNode.replaceChild(newDom, inst.root);
|
|
1303
|
-
inst.root = newDom;
|
|
1304
|
-
didUpdate = true;
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
catch (e) {
|
|
1308
|
-
console.error('HMR update error:', e);
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
if (!didUpdate) {
|
|
1314
|
-
const path = window.location.pathname + window.location.search;
|
|
1315
|
-
router.navigate(path, { replace: true });
|
|
1790
|
+
const path = window.location.pathname + window.location.search;
|
|
1791
|
+
const match = matchRoute(router.routeTree, path);
|
|
1792
|
+
if (match) {
|
|
1793
|
+
match.pathname = path;
|
|
1794
|
+
const result = renderMatch(router, match, container);
|
|
1795
|
+
if (result && typeof result.then === 'function') {
|
|
1796
|
+
result.catch((e) => console.error('HMR renderMatch error:', e));
|
|
1316
1797
|
}
|
|
1317
1798
|
}
|
|
1318
|
-
else {
|
|
1319
|
-
const path = window.location.pathname + window.location.search;
|
|
1320
|
-
router.navigate(path, { replace: true });
|
|
1321
|
-
}
|
|
1322
1799
|
},
|
|
1323
1800
|
};
|
|
1324
1801
|
return router;
|
|
@@ -1343,6 +1820,8 @@ export function buildRouteTree(definitions) {
|
|
|
1343
1820
|
loading: def.loading || null,
|
|
1344
1821
|
error: def.error || null,
|
|
1345
1822
|
notFound: def.notFound || null,
|
|
1823
|
+
offline: def.offline ?? null,
|
|
1824
|
+
network: def.network ?? null,
|
|
1346
1825
|
children: (def.children || []).map(c => {
|
|
1347
1826
|
const cParts = (c.path || '').split('/').filter(Boolean);
|
|
1348
1827
|
return {
|
|
@@ -1355,11 +1834,11 @@ export function buildRouteTree(definitions) {
|
|
|
1355
1834
|
loading: null,
|
|
1356
1835
|
error: null,
|
|
1357
1836
|
notFound: null,
|
|
1358
|
-
segmentCount:
|
|
1837
|
+
segmentCount: cParts.length || 1,
|
|
1359
1838
|
children: [],
|
|
1360
1839
|
};
|
|
1361
1840
|
}),
|
|
1362
|
-
segmentCount:
|
|
1841
|
+
segmentCount: parts.length,
|
|
1363
1842
|
};
|
|
1364
1843
|
tree.push(node);
|
|
1365
1844
|
}
|