@vesk/runtime 0.2.22 → 0.2.24
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/hmr-client.d.ts +3 -3
- package/dist/hmr-client.d.ts.map +1 -1
- package/dist/hmr-client.js +17 -5
- package/dist/hydrate.d.ts +30 -0
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +253 -51
- package/dist/index-client.d.ts +2 -2
- package/dist/index-client.d.ts.map +1 -1
- package/dist/index-client.js +2 -2
- package/dist/reconcile.d.ts +20 -1
- package/dist/reconcile.d.ts.map +1 -1
- package/dist/reconcile.js +48 -4
- package/dist/resource.d.ts +1 -1
- package/dist/resource.d.ts.map +1 -1
- package/dist/resource.js +58 -10
- package/dist/router-components.d.ts +16 -1
- package/dist/router-components.d.ts.map +1 -1
- package/dist/router-components.js +88 -17
- package/dist/router-match.d.ts +1 -0
- package/dist/router-match.d.ts.map +1 -1
- package/dist/router-match.js +13 -3
- package/dist/router.d.ts +1 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +168 -25
- package/package.json +27 -2
package/dist/router.js
CHANGED
|
@@ -2,39 +2,66 @@ 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, findOfflineComponent, findNetworkComponent, getCurrentRouter, 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, captureInitialScroll, showLoadingInContainer, handleScroll, applyHead, findLoadingComponent, findErrorComponent, findNotFoundComponent, findOfflineComponent, findNetworkComponent, getCurrentRouter, Outlet, Link, NavLink, useNavigate, useParams, usePathname, useSearchParams, useRouter, Redirect, redirect, permanentRedirect, NotFoundError, notFound, } from '@vesk/runtime/src/router-components';
|
|
6
6
|
import { getNetworkState, watchNetwork } from '@vesk/runtime/src/network';
|
|
7
7
|
import { loadingStart, loadingFinish, getLoadingState } from '@vesk/runtime/src/loading-indicator';
|
|
8
8
|
export { Outlet, Link, NavLink, useNavigate, useParams, usePathname, useSearchParams, useRouter, Redirect, redirect, permanentRedirect, NotFoundError, notFound, ensureChunk, };
|
|
9
9
|
export { matchRoute } from '@vesk/runtime/src/router-match';
|
|
10
10
|
const loadedChunks = new Set();
|
|
11
11
|
const failedChunks = new Map();
|
|
12
|
+
const inFlightChunks = new Map();
|
|
12
13
|
function chunkLoadError(chunkUrl) {
|
|
13
14
|
return failedChunks.get(chunkUrl);
|
|
14
15
|
}
|
|
15
16
|
function ensureChunk(chunkUrl) {
|
|
16
17
|
if (!chunkUrl || loadedChunks.has(chunkUrl))
|
|
17
18
|
return Promise.resolve();
|
|
18
|
-
|
|
19
|
+
const inFlight = inFlightChunks.get(chunkUrl);
|
|
20
|
+
if (inFlight)
|
|
21
|
+
return inFlight;
|
|
19
22
|
if (typeof document === 'undefined' || typeof document.createElement !== 'function') {
|
|
20
23
|
return Promise.resolve();
|
|
21
24
|
}
|
|
22
|
-
|
|
25
|
+
const promise = new Promise((resolve, reject) => {
|
|
23
26
|
const s = document.createElement('script');
|
|
24
27
|
s.src = chunkUrl;
|
|
28
|
+
let settled = false;
|
|
29
|
+
const fail = (err) => {
|
|
30
|
+
if (settled)
|
|
31
|
+
return;
|
|
32
|
+
settled = true;
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
inFlightChunks.delete(chunkUrl);
|
|
35
|
+
failedChunks.set(chunkUrl, err);
|
|
36
|
+
reject(err);
|
|
37
|
+
};
|
|
25
38
|
s.onload = () => {
|
|
39
|
+
if (settled)
|
|
40
|
+
return;
|
|
41
|
+
settled = true;
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
loadedChunks.add(chunkUrl);
|
|
44
|
+
inFlightChunks.delete(chunkUrl);
|
|
26
45
|
failedChunks.delete(chunkUrl);
|
|
27
46
|
resolve();
|
|
28
47
|
};
|
|
29
48
|
s.onerror = () => {
|
|
30
|
-
|
|
31
|
-
const err = new Error(`Failed to load chunk: ${chunkUrl}`);
|
|
32
|
-
failedChunks.set(chunkUrl, err);
|
|
33
|
-
reject(err);
|
|
49
|
+
fail(new Error(`Failed to load chunk: ${chunkUrl}`));
|
|
34
50
|
};
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
fail(new Error(`Timed out loading chunk: ${chunkUrl}`));
|
|
53
|
+
}, CHUNK_LOAD_TIMEOUT_MS);
|
|
35
54
|
document.head.appendChild(s);
|
|
36
55
|
});
|
|
56
|
+
inFlightChunks.set(chunkUrl, promise);
|
|
57
|
+
return promise;
|
|
37
58
|
}
|
|
59
|
+
// A chunk script that never fires load/error (dev server slow to respond,
|
|
60
|
+
// throttled connection) must not strand the navigation forever — the URL is
|
|
61
|
+
// already updated before chunks load, so a silent stall would leave the new
|
|
62
|
+
// URL showing the old page. After this timeout the load rejects and the
|
|
63
|
+
// navigation renders its error fallback instead.
|
|
64
|
+
const CHUNK_LOAD_TIMEOUT_MS = 10000;
|
|
38
65
|
/** Loads pending chunks without letting a single failed chunk abort the flow. */
|
|
39
66
|
function loadChunksQuietly(urls) {
|
|
40
67
|
navDebug('loadChunks start', urls);
|
|
@@ -52,15 +79,134 @@ function navDebug(...parts) {
|
|
|
52
79
|
}
|
|
53
80
|
function hasPendingChunks(nodes) {
|
|
54
81
|
const urls = [];
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
// Only the chunks backing this navigation's match chain (layouts + page
|
|
84
|
+
// node) are needed. Recursing into a node's children would pull in every
|
|
85
|
+
// sibling route's chunk — e.g. one nav from the root layout would inject
|
|
86
|
+
// all 27 page bundles — stalling the navigation while unrelated chunks
|
|
87
|
+
// load and marking routes that never loaded as "pending".
|
|
88
|
+
for (const n of nodes) {
|
|
89
|
+
if (n._chunk && !seen.has(n._chunk)) {
|
|
90
|
+
seen.add(n._chunk);
|
|
91
|
+
if (!loadedChunks.has(n._chunk)) {
|
|
92
|
+
urls.push(n._chunk);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
60
95
|
}
|
|
61
|
-
nodes.forEach(walk);
|
|
62
96
|
return urls;
|
|
63
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Delegated route-chunk preloading for code-split routes.
|
|
100
|
+
*
|
|
101
|
+
* Uses bubbling events (`pointerover`/`focusin`) instead of `mouseenter` —
|
|
102
|
+
* `mouseenter` does NOT bubble, so a listener on `document` only fires once
|
|
103
|
+
* when the pointer enters the page and never for a specific link. That made
|
|
104
|
+
* the historic hover-prefetch dead code: the first click on a route always
|
|
105
|
+
* paid the chunk download (the paint gate) and only later visits were warm.
|
|
106
|
+
*
|
|
107
|
+
* Warming loads route chunks early so the chunk roundtrip never blocks first
|
|
108
|
+
* paint. Route DATA is deliberately left untouched here — it is fetched at
|
|
109
|
+
* navigation time exactly as before (once per visit), so SSR/initial-load and
|
|
110
|
+
* data-freshness semantics are unchanged.
|
|
111
|
+
*
|
|
112
|
+
* Also warms links already in the DOM: as a link scrolls near the viewport
|
|
113
|
+
* (IntersectionObserver, 300px margin) its chunk is preloaded for the
|
|
114
|
+
* first click. Falls back to warming the first 16 links at idle when
|
|
115
|
+
* IntersectionObserver is unavailable.
|
|
116
|
+
*/
|
|
117
|
+
function installLinkPrefetch(warmChunks) {
|
|
118
|
+
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function')
|
|
119
|
+
return;
|
|
120
|
+
const recent = new Map();
|
|
121
|
+
const over = (e) => {
|
|
122
|
+
const target = e.target;
|
|
123
|
+
if (!target || target.nodeType !== 1)
|
|
124
|
+
return;
|
|
125
|
+
const link = target.closest ? target.closest('a[href]') : null;
|
|
126
|
+
if (!link)
|
|
127
|
+
return;
|
|
128
|
+
const href = link.getAttribute('href');
|
|
129
|
+
if (!href)
|
|
130
|
+
return;
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
if (now - (recent.get(href) || 0) < 500)
|
|
133
|
+
return;
|
|
134
|
+
recent.set(href, now);
|
|
135
|
+
warmChunks(href);
|
|
136
|
+
};
|
|
137
|
+
document.addEventListener('pointerover', over, { passive: true });
|
|
138
|
+
document.addEventListener('focusin', over, { passive: true });
|
|
139
|
+
const schedule = (cb) => {
|
|
140
|
+
const ric = typeof window !== 'undefined' && typeof window.requestIdleCallback === 'function'
|
|
141
|
+
? window.requestIdleCallback.bind(window)
|
|
142
|
+
: (fn) => setTimeout(fn, 250);
|
|
143
|
+
ric(() => { try {
|
|
144
|
+
cb();
|
|
145
|
+
}
|
|
146
|
+
catch { /* best-effort warmup */ } }, { timeout: 2000 });
|
|
147
|
+
};
|
|
148
|
+
schedule(() => {
|
|
149
|
+
if (typeof document.querySelectorAll !== 'function')
|
|
150
|
+
return;
|
|
151
|
+
const anchors = Array.from(document.querySelectorAll('a[href]'));
|
|
152
|
+
if (anchors.length === 0)
|
|
153
|
+
return;
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
let budget = 16;
|
|
156
|
+
const warm = (a) => {
|
|
157
|
+
const href = a.getAttribute('href');
|
|
158
|
+
if (!href || seen.has(href) || budget-- <= 0)
|
|
159
|
+
return;
|
|
160
|
+
seen.add(href);
|
|
161
|
+
warmChunks(href);
|
|
162
|
+
};
|
|
163
|
+
if (typeof IntersectionObserver === 'function') {
|
|
164
|
+
const io = new IntersectionObserver((entries) => {
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
if (!entry.isIntersecting)
|
|
167
|
+
continue;
|
|
168
|
+
warm(entry.target);
|
|
169
|
+
io.unobserve(entry.target);
|
|
170
|
+
}
|
|
171
|
+
if (budget <= 0)
|
|
172
|
+
io.disconnect();
|
|
173
|
+
}, { rootMargin: '300px' });
|
|
174
|
+
for (const a of anchors)
|
|
175
|
+
io.observe(a);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
for (const a of anchors)
|
|
179
|
+
warm(a);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Warms the chunks backing a route link without touching route data — the
|
|
185
|
+
* data fetch stays at navigation time (once per visit), preserving SSR
|
|
186
|
+
* initial-load and freshness semantics.
|
|
187
|
+
*/
|
|
188
|
+
function warmRouteChunks(routeTree, href) {
|
|
189
|
+
if (!href)
|
|
190
|
+
return;
|
|
191
|
+
let url;
|
|
192
|
+
try {
|
|
193
|
+
url = new URL(href, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const pathname = url.pathname;
|
|
199
|
+
if (typeof window !== 'undefined' && pathname === window.location.pathname && !url.search && !url.hash)
|
|
200
|
+
return;
|
|
201
|
+
const match = matchRoute(routeTree, pathname);
|
|
202
|
+
if (!match)
|
|
203
|
+
return;
|
|
204
|
+
const urls = hasPendingChunks(match.matchChain);
|
|
205
|
+
if (urls.length === 0)
|
|
206
|
+
return;
|
|
207
|
+
for (const u of urls)
|
|
208
|
+
ensureChunk(u);
|
|
209
|
+
}
|
|
64
210
|
/**
|
|
65
211
|
* Classifies a data-fetch failure as a connectivity problem rather than a
|
|
66
212
|
* server problem: fetch rejects with TypeError on network failure, and
|
|
@@ -1114,6 +1260,7 @@ export function createRouter(routes, options = {}) {
|
|
|
1114
1260
|
start() {
|
|
1115
1261
|
setCurrentRouter(this);
|
|
1116
1262
|
if (typeof window !== 'undefined' && 'scrollRestoration' in window.history) {
|
|
1263
|
+
captureInitialScroll();
|
|
1117
1264
|
window.history.scrollRestoration = 'manual';
|
|
1118
1265
|
}
|
|
1119
1266
|
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
|
@@ -1197,11 +1344,7 @@ export function createRouter(routes, options = {}) {
|
|
|
1197
1344
|
this.navigate(href);
|
|
1198
1345
|
});
|
|
1199
1346
|
if (prefetch) {
|
|
1200
|
-
|
|
1201
|
-
const link = e.target?.nodeType === 1 ? e.target.closest('a[href]') : null;
|
|
1202
|
-
if (link)
|
|
1203
|
-
this.prefetch(link.getAttribute('href'));
|
|
1204
|
-
}, { passive: true });
|
|
1347
|
+
installLinkPrefetch((href) => warmRouteChunks(this.routeTree, href));
|
|
1205
1348
|
}
|
|
1206
1349
|
const path = routeFromLocation(hashMode);
|
|
1207
1350
|
if (container.children.length > 0 && !hashMode) {
|
|
@@ -1265,7 +1408,7 @@ export function createRouter(routes, options = {}) {
|
|
|
1265
1408
|
}
|
|
1266
1409
|
this._currentMatch = match;
|
|
1267
1410
|
clearOfflineFlag(this, container);
|
|
1268
|
-
handleScroll(url.pathname, opts.replace);
|
|
1411
|
+
handleScroll(url.pathname, opts.replace, opts.scrollBehavior);
|
|
1269
1412
|
};
|
|
1270
1413
|
const swapView = (fn) => {
|
|
1271
1414
|
const d = document;
|
|
@@ -1498,6 +1641,7 @@ export function createFileRouter(routeTree, options = {}) {
|
|
|
1498
1641
|
start() {
|
|
1499
1642
|
setCurrentRouter(this);
|
|
1500
1643
|
if (typeof window !== 'undefined' && 'scrollRestoration' in window.history) {
|
|
1644
|
+
captureInitialScroll();
|
|
1501
1645
|
window.history.scrollRestoration = 'manual';
|
|
1502
1646
|
}
|
|
1503
1647
|
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
|
@@ -1573,11 +1717,7 @@ export function createFileRouter(routeTree, options = {}) {
|
|
|
1573
1717
|
router.navigate(href);
|
|
1574
1718
|
});
|
|
1575
1719
|
if (options.prefetch !== false) {
|
|
1576
|
-
|
|
1577
|
-
const link = e.target?.nodeType === 1 ? e.target.closest('a[href]') : null;
|
|
1578
|
-
if (link)
|
|
1579
|
-
router.prefetch(link.getAttribute('href'));
|
|
1580
|
-
}, { passive: true });
|
|
1720
|
+
installLinkPrefetch((href) => warmRouteChunks(router.routeTree, href));
|
|
1581
1721
|
}
|
|
1582
1722
|
const path = routeFromLocation(hashMode).split('?')[0];
|
|
1583
1723
|
if (container.children.length > 0 && !hashMode) {
|
|
@@ -1674,7 +1814,7 @@ export function createFileRouter(routeTree, options = {}) {
|
|
|
1674
1814
|
}
|
|
1675
1815
|
router._currentMatch = match;
|
|
1676
1816
|
clearOfflineFlag(router, container);
|
|
1677
|
-
handleScroll(url.pathname, opts.replace);
|
|
1817
|
+
handleScroll(url.pathname, opts.replace, opts.scrollBehavior);
|
|
1678
1818
|
};
|
|
1679
1819
|
const doRender = () => {
|
|
1680
1820
|
navDebug('doRender', url.pathname, 'token=' + navToken);
|
|
@@ -1788,6 +1928,7 @@ export function createFileRouter(routeTree, options = {}) {
|
|
|
1788
1928
|
router.navigate(e.url, { replace: true });
|
|
1789
1929
|
return;
|
|
1790
1930
|
}
|
|
1931
|
+
handleNavFailure(e);
|
|
1791
1932
|
}
|
|
1792
1933
|
}
|
|
1793
1934
|
if (middlewareFns.length > 0 || loadingFn) {
|
|
@@ -1919,6 +2060,7 @@ export function buildRouteTree(definitions) {
|
|
|
1919
2060
|
notFound: def.notFound || null,
|
|
1920
2061
|
offline: def.offline ?? null,
|
|
1921
2062
|
network: def.network ?? null,
|
|
2063
|
+
standalone: def.standalone ? true : undefined,
|
|
1922
2064
|
children: (def.children || []).map(c => {
|
|
1923
2065
|
const cParts = (c.path || '').split('/').filter(Boolean);
|
|
1924
2066
|
return {
|
|
@@ -1931,6 +2073,7 @@ export function buildRouteTree(definitions) {
|
|
|
1931
2073
|
loading: null,
|
|
1932
2074
|
error: null,
|
|
1933
2075
|
notFound: null,
|
|
2076
|
+
standalone: c.standalone ? true : undefined,
|
|
1934
2077
|
segmentCount: cParts.length || 1,
|
|
1935
2078
|
children: [],
|
|
1936
2079
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vesk/runtime",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vesk runtime — reactivity, blocks, DOM operations, hydration",
|
|
6
6
|
"main": "./dist/index-client.js",
|
|
@@ -25,8 +25,33 @@
|
|
|
25
25
|
"./src/*": "./dist/*.js",
|
|
26
26
|
"./package.json": "./package.json"
|
|
27
27
|
},
|
|
28
|
+
"typesVersions": {
|
|
29
|
+
"*": {
|
|
30
|
+
"router": [
|
|
31
|
+
"dist/index-router.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"client": [
|
|
34
|
+
"dist/index-client.d.ts"
|
|
35
|
+
],
|
|
36
|
+
"server": [
|
|
37
|
+
"dist/index-server.d.ts"
|
|
38
|
+
],
|
|
39
|
+
"src/*": [
|
|
40
|
+
"dist/*"
|
|
41
|
+
],
|
|
42
|
+
"router/*": [
|
|
43
|
+
"dist/*"
|
|
44
|
+
],
|
|
45
|
+
"client/*": [
|
|
46
|
+
"dist/*"
|
|
47
|
+
],
|
|
48
|
+
"server/*": [
|
|
49
|
+
"dist/*"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
28
53
|
"dependencies": {
|
|
29
|
-
"@vesk/compiler": "^0.2.
|
|
54
|
+
"@vesk/compiler": "^0.2.24"
|
|
30
55
|
},
|
|
31
56
|
"scripts": {
|
|
32
57
|
"typecheck": "tsc --noEmit",
|