veryfront 0.1.850 → 0.1.852
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/esm/deno.js +1 -1
- package/esm/src/html/hydration-script-builder/templates/router.d.ts.map +1 -1
- package/esm/src/html/hydration-script-builder/templates/router.js +263 -4
- package/esm/src/react/runtime/core.js +1 -1
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +1 -1
package/esm/deno.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../../../../src/src/html/hydration-script-builder/templates/router.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../../../../src/src/html/hydration-script-builder/templates/router.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,cAklC3B,CAAC"}
|
|
@@ -36,7 +36,12 @@ export const getRouterScript = () => `
|
|
|
36
36
|
const BACKGROUND_REFRESH_INTERVAL_MS = 30 * 1000;
|
|
37
37
|
const PREFETCH_DELAY_MS = 100;
|
|
38
38
|
const MAX_PREFETCH_PATHS = 100;
|
|
39
|
+
const IDLE_PREFETCH_DELAY_MS = 1200;
|
|
40
|
+
const IDLE_PREFETCH_MAX_LINKS = 4;
|
|
41
|
+
const VIEWPORT_PREFETCH_MAX_LINKS = 8;
|
|
42
|
+
const VIEWPORT_PREFETCH_ROOT_MARGIN = '200px';
|
|
39
43
|
const MAX_ROUTE_TIMINGS = 100;
|
|
44
|
+
const MAX_SERVER_TIMING_LENGTH = 1024;
|
|
40
45
|
|
|
41
46
|
// ============================================
|
|
42
47
|
// Debug logging (production-safe)
|
|
@@ -159,6 +164,139 @@ export const getRouterScript = () => `
|
|
|
159
164
|
return entry;
|
|
160
165
|
}
|
|
161
166
|
|
|
167
|
+
function sanitizeServerTimingHeader(value) {
|
|
168
|
+
if (!value) return null;
|
|
169
|
+
|
|
170
|
+
const metrics = [];
|
|
171
|
+
const printable = String(value).replace(/[^\\x20-\\x7E]/g, ' ').trim();
|
|
172
|
+
if (!printable) return null;
|
|
173
|
+
|
|
174
|
+
for (const item of printable.split(',')) {
|
|
175
|
+
const segments = item.split(';').map((segment) => segment.trim()).filter(Boolean);
|
|
176
|
+
const name = sanitizeServerTimingMetricName(segments[0]);
|
|
177
|
+
if (!name) continue;
|
|
178
|
+
|
|
179
|
+
for (const segment of segments.slice(1)) {
|
|
180
|
+
const [key, rawValue = ''] = segment.split('=');
|
|
181
|
+
if (key.trim().toLowerCase() !== 'dur') continue;
|
|
182
|
+
|
|
183
|
+
const duration = Number(rawValue.trim().replace(/^"|"$/g, ''));
|
|
184
|
+
if (!Number.isFinite(duration) || duration < 0) continue;
|
|
185
|
+
|
|
186
|
+
metrics.push(name + ';dur=' + (Math.round(duration * 100) / 100).toFixed(2));
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const sanitized = metrics.join(', ');
|
|
192
|
+
return sanitized ? sanitized.slice(0, MAX_SERVER_TIMING_LENGTH) : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function sanitizeServerTimingMetricName(name) {
|
|
196
|
+
return String(name || '').trim().replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 128);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseServerTimingMetrics(value) {
|
|
200
|
+
const header = sanitizeServerTimingHeader(value);
|
|
201
|
+
if (!header) return null;
|
|
202
|
+
|
|
203
|
+
const metrics = {};
|
|
204
|
+
for (const item of header.split(',')) {
|
|
205
|
+
const segments = item.split(';').map((segment) => segment.trim()).filter(Boolean);
|
|
206
|
+
const name = sanitizeServerTimingMetricName(segments[0]);
|
|
207
|
+
if (!name) continue;
|
|
208
|
+
|
|
209
|
+
for (const segment of segments.slice(1)) {
|
|
210
|
+
const [key, rawValue = ''] = segment.split('=');
|
|
211
|
+
if (key.trim().toLowerCase() !== 'dur') continue;
|
|
212
|
+
|
|
213
|
+
const duration = Number(rawValue.trim().replace(/^"|"$/g, ''));
|
|
214
|
+
if (Number.isFinite(duration) && duration >= 0) {
|
|
215
|
+
metrics[name] = Math.round(duration * 100) / 100;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return Object.keys(metrics).length ? metrics : null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function readResponseServerTiming(response) {
|
|
224
|
+
try {
|
|
225
|
+
return sanitizeServerTimingHeader(response.headers?.get('server-timing'));
|
|
226
|
+
} catch (_) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function roundRouteTimingValue(value) {
|
|
232
|
+
return Math.round(value * 100) / 100;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function extractResourceTiming(entry) {
|
|
236
|
+
const fields = [
|
|
237
|
+
'startTime',
|
|
238
|
+
'requestStart',
|
|
239
|
+
'responseStart',
|
|
240
|
+
'responseEnd',
|
|
241
|
+
'duration',
|
|
242
|
+
'transferSize',
|
|
243
|
+
'encodedBodySize',
|
|
244
|
+
'decodedBodySize'
|
|
245
|
+
];
|
|
246
|
+
const timing = {};
|
|
247
|
+
|
|
248
|
+
for (const field of fields) {
|
|
249
|
+
const value = entry?.[field];
|
|
250
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
|
|
251
|
+
timing[field] = roundRouteTimingValue(value);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return Object.keys(timing).length ? timing : null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function getPageDataResourceTiming(endpoint, fetchStartedAt) {
|
|
259
|
+
try {
|
|
260
|
+
if (typeof performance === 'undefined' || typeof performance.getEntriesByName !== 'function') {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const href = new URL(endpoint, window.location.href).href;
|
|
265
|
+
const entries = performance.getEntriesByName(href, 'resource');
|
|
266
|
+
if (!entries.length) return null;
|
|
267
|
+
|
|
268
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
269
|
+
const entry = entries[index];
|
|
270
|
+
if (
|
|
271
|
+
typeof entry?.responseEnd === 'number' &&
|
|
272
|
+
Number.isFinite(entry.responseEnd) &&
|
|
273
|
+
entry.responseEnd + 1 >= fetchStartedAt
|
|
274
|
+
) {
|
|
275
|
+
return extractResourceTiming(entry);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return null;
|
|
280
|
+
} catch (_) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function buildPageDataTimingDetail(response, endpoint, fetchStartedAt, source) {
|
|
286
|
+
const detail = { source, status: response.status };
|
|
287
|
+
const serverTiming = readResponseServerTiming(response);
|
|
288
|
+
if (serverTiming) {
|
|
289
|
+
detail.serverTiming = serverTiming;
|
|
290
|
+
const serverTimingMetrics = parseServerTimingMetrics(serverTiming);
|
|
291
|
+
if (serverTimingMetrics) detail.serverTimingMetrics = serverTimingMetrics;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const resourceTiming = getPageDataResourceTiming(response.url || endpoint, fetchStartedAt);
|
|
295
|
+
if (resourceTiming) detail.resourceTiming = resourceTiming;
|
|
296
|
+
|
|
297
|
+
return detail;
|
|
298
|
+
}
|
|
299
|
+
|
|
162
300
|
// ============================================
|
|
163
301
|
// LRU Cache with TTL (single Map to prevent sync issues)
|
|
164
302
|
// ============================================
|
|
@@ -320,7 +458,12 @@ export const getRouterScript = () => `
|
|
|
320
458
|
if (!response.ok) {
|
|
321
459
|
perfEnd('fetch:' + path);
|
|
322
460
|
if (recordRouteTiming) {
|
|
323
|
-
emitRouteTiming(
|
|
461
|
+
emitRouteTiming(
|
|
462
|
+
'page-data',
|
|
463
|
+
path,
|
|
464
|
+
startedAt,
|
|
465
|
+
buildPageDataTimingDetail(response, endpoint, startedAt, timingSource)
|
|
466
|
+
);
|
|
324
467
|
}
|
|
325
468
|
const error = new Error('Failed to fetch page data: ' + response.status);
|
|
326
469
|
error.status = response.status;
|
|
@@ -332,7 +475,12 @@ export const getRouterScript = () => `
|
|
|
332
475
|
perfEnd('parse:' + path);
|
|
333
476
|
perfEnd('fetch:' + path);
|
|
334
477
|
if (recordRouteTiming) {
|
|
335
|
-
emitRouteTiming(
|
|
478
|
+
emitRouteTiming(
|
|
479
|
+
'page-data',
|
|
480
|
+
path,
|
|
481
|
+
startedAt,
|
|
482
|
+
buildPageDataTimingDetail(response, endpoint, startedAt, timingSource)
|
|
483
|
+
);
|
|
336
484
|
}
|
|
337
485
|
|
|
338
486
|
if (triggerReloadOnVersionMismatch) {
|
|
@@ -614,6 +762,7 @@ export const getRouterScript = () => `
|
|
|
614
762
|
container.__reactRoot.render(tree);
|
|
615
763
|
perfEnd('render:reactRender');
|
|
616
764
|
log('Page re-rendered via SPA');
|
|
765
|
+
scheduleRoutePrefetchRefresh();
|
|
617
766
|
return;
|
|
618
767
|
}
|
|
619
768
|
|
|
@@ -629,6 +778,9 @@ export const getRouterScript = () => `
|
|
|
629
778
|
// ============================================
|
|
630
779
|
let prefetchTimeout = null;
|
|
631
780
|
let currentHoverLink = null;
|
|
781
|
+
let routePrefetchRefreshPending = false;
|
|
782
|
+
let viewportPrefetchObserver = null;
|
|
783
|
+
const observedPrefetchLinks = new WeakSet();
|
|
632
784
|
const prefetchedPaths = new Set();
|
|
633
785
|
const inFlightPrefetches = new Set();
|
|
634
786
|
|
|
@@ -650,6 +802,51 @@ export const getRouterScript = () => `
|
|
|
650
802
|
return allPaths;
|
|
651
803
|
}
|
|
652
804
|
|
|
805
|
+
function getCurrentRouteHref() {
|
|
806
|
+
return window.location.pathname + window.location.search;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function getInternalRouteHrefFromLink(link) {
|
|
810
|
+
if (
|
|
811
|
+
!link ||
|
|
812
|
+
link.target === '_blank' ||
|
|
813
|
+
link.hasAttribute('download') ||
|
|
814
|
+
link.getAttribute('data-prefetch') === 'false'
|
|
815
|
+
) {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const href = link.getAttribute('href');
|
|
820
|
+
if (!href || href.startsWith('#') || href.startsWith('//') || !href.startsWith('/')) return null;
|
|
821
|
+
|
|
822
|
+
try {
|
|
823
|
+
const url = new URL(href, window.location.origin);
|
|
824
|
+
if (url.origin !== window.location.origin) return null;
|
|
825
|
+
|
|
826
|
+
const routeHref = url.pathname + url.search;
|
|
827
|
+
return routeHref === getCurrentRouteHref() ? null : routeHref;
|
|
828
|
+
} catch (_) {
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function getEligiblePrefetchLinks(limit) {
|
|
834
|
+
const links = [];
|
|
835
|
+
const seenHrefs = new Set();
|
|
836
|
+
|
|
837
|
+
for (const link of document.querySelectorAll('a[href]')) {
|
|
838
|
+
const href = getInternalRouteHrefFromLink(link);
|
|
839
|
+
if (!href || seenHrefs.has(href)) continue;
|
|
840
|
+
|
|
841
|
+
seenHrefs.add(href);
|
|
842
|
+
links.push({ link, href });
|
|
843
|
+
|
|
844
|
+
if (links.length >= limit) break;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return links;
|
|
848
|
+
}
|
|
849
|
+
|
|
653
850
|
async function preloadModulesForPageData(pageData, path) {
|
|
654
851
|
if (!pageData) return;
|
|
655
852
|
if (pageData.releaseId && window.__veryfrontSetReleaseId) {
|
|
@@ -698,6 +895,62 @@ export const getRouterScript = () => `
|
|
|
698
895
|
});
|
|
699
896
|
}
|
|
700
897
|
|
|
898
|
+
function prefetchEligibleRouteLinks(limit) {
|
|
899
|
+
for (const { href } of getEligiblePrefetchLinks(limit)) {
|
|
900
|
+
prefetchPage(href);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function ensureViewportPrefetchObserver() {
|
|
905
|
+
if (viewportPrefetchObserver || typeof IntersectionObserver !== 'function') {
|
|
906
|
+
return viewportPrefetchObserver;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
viewportPrefetchObserver = new IntersectionObserver((entries) => {
|
|
910
|
+
for (const entry of entries) {
|
|
911
|
+
if (!entry.isIntersecting) continue;
|
|
912
|
+
|
|
913
|
+
viewportPrefetchObserver?.unobserve(entry.target);
|
|
914
|
+
const href = getInternalRouteHrefFromLink(entry.target);
|
|
915
|
+
if (href) prefetchPage(href);
|
|
916
|
+
}
|
|
917
|
+
}, { rootMargin: VIEWPORT_PREFETCH_ROOT_MARGIN });
|
|
918
|
+
|
|
919
|
+
return viewportPrefetchObserver;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function observeViewportPrefetchLinks() {
|
|
923
|
+
const observer = ensureViewportPrefetchObserver();
|
|
924
|
+
if (!observer) return;
|
|
925
|
+
|
|
926
|
+
for (const { link } of getEligiblePrefetchLinks(VIEWPORT_PREFETCH_MAX_LINKS)) {
|
|
927
|
+
if (observedPrefetchLinks.has(link)) continue;
|
|
928
|
+
|
|
929
|
+
observedPrefetchLinks.add(link);
|
|
930
|
+
observer.observe(link);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function runRoutePrefetchRefresh() {
|
|
935
|
+
routePrefetchRefreshPending = false;
|
|
936
|
+
prefetchEligibleRouteLinks(IDLE_PREFETCH_MAX_LINKS);
|
|
937
|
+
observeViewportPrefetchLinks();
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function scheduleRoutePrefetchRefresh() {
|
|
941
|
+
if (routePrefetchRefreshPending) return;
|
|
942
|
+
|
|
943
|
+
routePrefetchRefreshPending = true;
|
|
944
|
+
setTimeout(() => {
|
|
945
|
+
if (typeof requestIdleCallback === 'function') {
|
|
946
|
+
requestIdleCallback(runRoutePrefetchRefresh, { timeout: IDLE_PREFETCH_DELAY_MS });
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
runRoutePrefetchRefresh();
|
|
951
|
+
}, IDLE_PREFETCH_DELAY_MS);
|
|
952
|
+
}
|
|
953
|
+
|
|
701
954
|
// ============================================
|
|
702
955
|
// Router object
|
|
703
956
|
// ============================================
|
|
@@ -802,8 +1055,8 @@ export const getRouterScript = () => `
|
|
|
802
1055
|
const link = e.target.closest('a[href]');
|
|
803
1056
|
if (!link) return;
|
|
804
1057
|
|
|
805
|
-
const href = link
|
|
806
|
-
if (!href
|
|
1058
|
+
const href = getInternalRouteHrefFromLink(link);
|
|
1059
|
+
if (!href) return;
|
|
807
1060
|
|
|
808
1061
|
if (currentHoverLink === link) return;
|
|
809
1062
|
|
|
@@ -834,6 +1087,12 @@ export const getRouterScript = () => `
|
|
|
834
1087
|
true
|
|
835
1088
|
);
|
|
836
1089
|
|
|
1090
|
+
if (document.readyState === 'loading') {
|
|
1091
|
+
document.addEventListener('DOMContentLoaded', scheduleRoutePrefetchRefresh, { once: true });
|
|
1092
|
+
} else {
|
|
1093
|
+
scheduleRoutePrefetchRefresh();
|
|
1094
|
+
}
|
|
1095
|
+
|
|
837
1096
|
// ============================================
|
|
838
1097
|
// Router hooks
|
|
839
1098
|
// ============================================
|
|
@@ -60,7 +60,7 @@ export function useRouter() {
|
|
|
60
60
|
}
|
|
61
61
|
/** Renders an anchor element annotated for Veryfront prefetch handling. */
|
|
62
62
|
export function Link({ prefetch = true, children, ...rest }) {
|
|
63
|
-
return React.createElement("a", { ...rest, "data-prefetch": prefetch ? "true" :
|
|
63
|
+
return React.createElement("a", { ...rest, "data-prefetch": prefetch ? "true" : "false" }, children);
|
|
64
64
|
}
|
|
65
65
|
/** Provides page context to route and MDX descendants. */
|
|
66
66
|
export function PageContextProvider({ children, pageContext, }) {
|