@vesk/runtime 0.2.21 → 0.2.23
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 +51 -10
- package/dist/router-components.d.ts.map +1 -1
- package/dist/router-components.js +63 -11
- 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.map +1 -1
- package/dist/router.js +163 -22
- package/package.json +27 -2
package/dist/router.js
CHANGED
|
@@ -9,32 +9,59 @@ export { Outlet, Link, NavLink, useNavigate, useParams, usePathname, useSearchPa
|
|
|
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
|
|
@@ -1197,11 +1343,7 @@ export function createRouter(routes, options = {}) {
|
|
|
1197
1343
|
this.navigate(href);
|
|
1198
1344
|
});
|
|
1199
1345
|
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 });
|
|
1346
|
+
installLinkPrefetch((href) => warmRouteChunks(this.routeTree, href));
|
|
1205
1347
|
}
|
|
1206
1348
|
const path = routeFromLocation(hashMode);
|
|
1207
1349
|
if (container.children.length > 0 && !hashMode) {
|
|
@@ -1573,11 +1715,7 @@ export function createFileRouter(routeTree, options = {}) {
|
|
|
1573
1715
|
router.navigate(href);
|
|
1574
1716
|
});
|
|
1575
1717
|
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 });
|
|
1718
|
+
installLinkPrefetch((href) => warmRouteChunks(router.routeTree, href));
|
|
1581
1719
|
}
|
|
1582
1720
|
const path = routeFromLocation(hashMode).split('?')[0];
|
|
1583
1721
|
if (container.children.length > 0 && !hashMode) {
|
|
@@ -1788,6 +1926,7 @@ export function createFileRouter(routeTree, options = {}) {
|
|
|
1788
1926
|
router.navigate(e.url, { replace: true });
|
|
1789
1927
|
return;
|
|
1790
1928
|
}
|
|
1929
|
+
handleNavFailure(e);
|
|
1791
1930
|
}
|
|
1792
1931
|
}
|
|
1793
1932
|
if (middlewareFns.length > 0 || loadingFn) {
|
|
@@ -1919,6 +2058,7 @@ export function buildRouteTree(definitions) {
|
|
|
1919
2058
|
notFound: def.notFound || null,
|
|
1920
2059
|
offline: def.offline ?? null,
|
|
1921
2060
|
network: def.network ?? null,
|
|
2061
|
+
standalone: def.standalone ? true : undefined,
|
|
1922
2062
|
children: (def.children || []).map(c => {
|
|
1923
2063
|
const cParts = (c.path || '').split('/').filter(Boolean);
|
|
1924
2064
|
return {
|
|
@@ -1931,6 +2071,7 @@ export function buildRouteTree(definitions) {
|
|
|
1931
2071
|
loading: null,
|
|
1932
2072
|
error: null,
|
|
1933
2073
|
notFound: null,
|
|
2074
|
+
standalone: c.standalone ? true : undefined,
|
|
1934
2075
|
segmentCount: cParts.length || 1,
|
|
1935
2076
|
children: [],
|
|
1936
2077
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vesk/runtime",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23",
|
|
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.23"
|
|
30
55
|
},
|
|
31
56
|
"scripts": {
|
|
32
57
|
"typecheck": "tsc --noEmit",
|