@moku-labs/web 2.4.14 → 2.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/browser.d.mts +60 -7
- package/dist/browser.mjs +272 -3492
- package/dist/convention-tZrMQCJS.mjs +20 -0
- package/dist/index.cjs +510 -121
- package/dist/index.d.cts +60 -7
- package/dist/index.d.mts +60 -7
- package/dist/index.mjs +510 -121
- package/dist/render-DcOi8CU9.mjs +12 -0
- package/dist/testing.mjs +2 -1
- package/dist/writer-CIlDKFE1.mjs +48 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -4622,9 +4622,8 @@ function dataApi(ctx) {
|
|
|
4622
4622
|
return {
|
|
4623
4623
|
/**
|
|
4624
4624
|
* READ (browser) — fetch (and cache) the persisted data for a page path.
|
|
4625
|
-
* Concurrent calls for one path share one in-flight request.
|
|
4626
|
-
*
|
|
4627
|
-
* only if the bundle did not bring the path.
|
|
4625
|
+
* Concurrent calls for one path share one in-flight request. It never waits
|
|
4626
|
+
* for an in-flight preload bundle: an uncached path is fetched at once.
|
|
4628
4627
|
* Returns the raw JSON as `unknown`, which the route uses directly as `ctx.data`
|
|
4629
4628
|
* (no route `.parse()`); returns `null` if the fetch or JSON parse fails (so
|
|
4630
4629
|
* `spa` can fall back to HTML).
|
|
@@ -4638,10 +4637,6 @@ function dataApi(ctx) {
|
|
|
4638
4637
|
*/
|
|
4639
4638
|
async at(path) {
|
|
4640
4639
|
if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
|
|
4641
|
-
if (ctx.state.bundle) {
|
|
4642
|
-
await ctx.state.bundle;
|
|
4643
|
-
if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
|
|
4644
|
-
}
|
|
4645
4640
|
const inFlight = ctx.state.pending.get(path);
|
|
4646
4641
|
if (inFlight) return inFlight;
|
|
4647
4642
|
const request = fetchAndCache(ctx, path);
|
|
@@ -4716,7 +4711,7 @@ const defaultDataConfig = {
|
|
|
4716
4711
|
//#region src/plugins/data/preload.ts
|
|
4717
4712
|
/**
|
|
4718
4713
|
* Fetches the bundle and caches every path not already cached. Never rejects:
|
|
4719
|
-
* a failed fetch leaves lazy `at()` working. Clears
|
|
4714
|
+
* a failed fetch leaves lazy `at()` working. Clears `cancelPreload` when done.
|
|
4720
4715
|
*
|
|
4721
4716
|
* @param ctx - The data plugin context.
|
|
4722
4717
|
* @param signal - Aborts the fetch on stop.
|
|
@@ -4732,7 +4727,7 @@ async function fill(ctx, signal) {
|
|
|
4732
4727
|
const bundle = response.ok ? await response.json() : {};
|
|
4733
4728
|
for (const [path, data] of Object.entries(bundle)) if (!signal.aborted && !ctx.state.cache.has(path)) ctx.state.cache.set(path, data);
|
|
4734
4729
|
} catch {} finally {
|
|
4735
|
-
ctx.state.
|
|
4730
|
+
ctx.state.cancelPreload = null;
|
|
4736
4731
|
}
|
|
4737
4732
|
}
|
|
4738
4733
|
/**
|
|
@@ -4750,7 +4745,7 @@ function startPreload(ctx) {
|
|
|
4750
4745
|
if (ctx.config.preload !== "all" || typeof document === "undefined" || ctx.state.cancelPreload !== null || nav?.connection?.saveData === true) return;
|
|
4751
4746
|
const controller = new AbortController();
|
|
4752
4747
|
const run = () => {
|
|
4753
|
-
|
|
4748
|
+
fill(ctx, controller.signal);
|
|
4754
4749
|
};
|
|
4755
4750
|
const idle = typeof requestIdleCallback === "function";
|
|
4756
4751
|
const handle = idle ? requestIdleCallback(run, { timeout: 2e3 }) : setTimeout(run, 0);
|
|
@@ -4779,7 +4774,7 @@ function stopPreload(ctx) {
|
|
|
4779
4774
|
* Creates initial data state: a null `lastWrite` slot (populated by the Node
|
|
4780
4775
|
* `write()` side), an empty `cache` (populated lazily by the browser `at(path)`
|
|
4781
4776
|
* side on first fetch), an empty `pending` map of in-flight fetches, and no
|
|
4782
|
-
* preload
|
|
4777
|
+
* preload scheduled.
|
|
4783
4778
|
*
|
|
4784
4779
|
* @param _ctx - Minimal context with global and config.
|
|
4785
4780
|
* @param _ctx.global - Global framework configuration.
|
|
@@ -4795,7 +4790,6 @@ function createDataState(_ctx) {
|
|
|
4795
4790
|
lastWrite: null,
|
|
4796
4791
|
cache: /* @__PURE__ */ new Map(),
|
|
4797
4792
|
pending: /* @__PURE__ */ new Map(),
|
|
4798
|
-
bundle: null,
|
|
4799
4793
|
cancelPreload: null
|
|
4800
4794
|
};
|
|
4801
4795
|
}
|
|
@@ -10517,89 +10511,6 @@ function notifyNavEnd(state, route = EMPTY_ROUTE) {
|
|
|
10517
10511
|
}
|
|
10518
10512
|
}
|
|
10519
10513
|
//#endregion
|
|
10520
|
-
//#region src/plugins/spa/progress.ts
|
|
10521
|
-
/** Delay before the bar appears, so fast navigations show no indicator. */
|
|
10522
|
-
const START_DELAY_MS = 150;
|
|
10523
|
-
/** Interval between trickle increments while loading. */
|
|
10524
|
-
const TRICKLE_MS = 300;
|
|
10525
|
-
/** Linger before the completed bar is reset/hidden. */
|
|
10526
|
-
const DONE_LINGER_MS = 200;
|
|
10527
|
-
/** Ceiling the bar trickles to while still loading (never reaches 100% until done). */
|
|
10528
|
-
const TRICKLE_CEIL = 90;
|
|
10529
|
-
/** No-op progress bar used when disabled or in a headless context. */
|
|
10530
|
-
const NOOP_BAR = {
|
|
10531
|
-
start() {},
|
|
10532
|
-
done() {}
|
|
10533
|
-
};
|
|
10534
|
-
/**
|
|
10535
|
-
* Creates the in-house progress bar (150ms delay + trickle). A no-op shell when
|
|
10536
|
-
* progress is disabled or no DOM is present. The progress element is created
|
|
10537
|
-
* once (prepended to `<body>` as `<div data-progress>`) and reused across navs.
|
|
10538
|
-
*
|
|
10539
|
-
* @param enabled - Whether the progress bar is active.
|
|
10540
|
-
* @returns A {@link ProgressBar} with `start`/`done`. Disabled/headless → no-ops.
|
|
10541
|
-
* @example
|
|
10542
|
-
* const bar = createProgressBar(true);
|
|
10543
|
-
* bar.start();
|
|
10544
|
-
* bar.done();
|
|
10545
|
-
*/
|
|
10546
|
-
function createProgressBar(enabled) {
|
|
10547
|
-
if (!enabled || typeof document === "undefined") return NOOP_BAR;
|
|
10548
|
-
const element = document.createElement("div");
|
|
10549
|
-
element.dataset.progress = "";
|
|
10550
|
-
document.body.prepend(element);
|
|
10551
|
-
let delayTimer;
|
|
10552
|
-
let trickleTimer;
|
|
10553
|
-
let width = 0;
|
|
10554
|
-
/**
|
|
10555
|
-
* Step the trickle upward toward the ceiling and reschedule.
|
|
10556
|
-
*
|
|
10557
|
-
* @example
|
|
10558
|
-
* trickle();
|
|
10559
|
-
*/
|
|
10560
|
-
const trickle = () => {
|
|
10561
|
-
if (width >= TRICKLE_CEIL) return;
|
|
10562
|
-
width = Math.min(TRICKLE_CEIL, width + 5 + Math.random() * 10);
|
|
10563
|
-
element.style.width = `${String(width)}%`;
|
|
10564
|
-
trickleTimer = setTimeout(trickle, TRICKLE_MS);
|
|
10565
|
-
};
|
|
10566
|
-
/**
|
|
10567
|
-
* Show the bar after the start delay and begin trickling.
|
|
10568
|
-
*
|
|
10569
|
-
* @example
|
|
10570
|
-
* start();
|
|
10571
|
-
*/
|
|
10572
|
-
const start = () => {
|
|
10573
|
-
delayTimer = setTimeout(() => {
|
|
10574
|
-
width = 15;
|
|
10575
|
-
element.style.width = "15%";
|
|
10576
|
-
element.dataset.active = "";
|
|
10577
|
-
trickle();
|
|
10578
|
-
}, START_DELAY_MS);
|
|
10579
|
-
};
|
|
10580
|
-
/**
|
|
10581
|
-
* Complete the bar to 100%, then reset/hide it after a short linger.
|
|
10582
|
-
*
|
|
10583
|
-
* @example
|
|
10584
|
-
* done();
|
|
10585
|
-
*/
|
|
10586
|
-
const done = () => {
|
|
10587
|
-
clearTimeout(delayTimer);
|
|
10588
|
-
clearTimeout(trickleTimer);
|
|
10589
|
-
element.style.width = "100%";
|
|
10590
|
-
element.dataset.active = "";
|
|
10591
|
-
setTimeout(() => {
|
|
10592
|
-
delete element.dataset.active;
|
|
10593
|
-
element.style.width = "0%";
|
|
10594
|
-
width = 0;
|
|
10595
|
-
}, DONE_LINGER_MS);
|
|
10596
|
-
};
|
|
10597
|
-
return {
|
|
10598
|
-
start,
|
|
10599
|
-
done
|
|
10600
|
-
};
|
|
10601
|
-
}
|
|
10602
|
-
//#endregion
|
|
10603
10514
|
//#region src/plugins/spa/router.ts
|
|
10604
10515
|
/**
|
|
10605
10516
|
* Read the Navigation API global, or `undefined` when unsupported.
|
|
@@ -10627,9 +10538,10 @@ function isInternalLink(url) {
|
|
|
10627
10538
|
/**
|
|
10628
10539
|
* The navigable path of a URL or Location: pathname plus query string. The query
|
|
10629
10540
|
* is part of page identity (the kernel's `currentUrl` is pathname + search), so
|
|
10630
|
-
* same-page checks, history entries, fetches,
|
|
10631
|
-
* comparing pathnames alone would treat `/search?q=a` →
|
|
10632
|
-
* and the History fallback would drop the query from the
|
|
10541
|
+
* same-page checks, history entries, fetches, scroll keys and the preload cache key
|
|
10542
|
+
* must all carry it — comparing pathnames alone would treat `/search?q=a` →
|
|
10543
|
+
* `/search?q=b` as same-page and the History fallback would drop the query from the
|
|
10544
|
+
* address bar. The hash is not part of it.
|
|
10633
10545
|
*
|
|
10634
10546
|
* @param target - The URL or Location to read.
|
|
10635
10547
|
* @param target.pathname - The path component.
|
|
@@ -10667,26 +10579,42 @@ function restoreScrollPosition(path) {
|
|
|
10667
10579
|
} catch {}
|
|
10668
10580
|
}
|
|
10669
10581
|
/**
|
|
10670
|
-
* Fetch a page
|
|
10582
|
+
* Fetch a page's HTML. The one fetch-and-check shared by navigation and preload
|
|
10583
|
+
* (`prefetch.ts`): a response that is not ok is an error, never HTML to swap in.
|
|
10584
|
+
*
|
|
10585
|
+
* @param pathname - The page path (pathname + search).
|
|
10586
|
+
* @param signal - Aborts the request.
|
|
10587
|
+
* @returns The page HTML.
|
|
10588
|
+
* @throws {Error} When the response is not ok (`HTTP <status>`), or the fetch fails or aborts.
|
|
10589
|
+
* @example
|
|
10590
|
+
* await fetchPage("/about"); // "<!doctype html>…" — rejects with "HTTP 404" on a missing page
|
|
10591
|
+
*/
|
|
10592
|
+
async function fetchPage(pathname, signal) {
|
|
10593
|
+
const response = await (signal ? fetch(pathname, { signal }) : fetch(pathname));
|
|
10594
|
+
if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
|
|
10595
|
+
return response.text();
|
|
10596
|
+
}
|
|
10597
|
+
/**
|
|
10598
|
+
* Load a page and hand its HTML to the handlers; on any error fall back to a
|
|
10671
10599
|
* full browser navigation (`location.href = pathname`).
|
|
10672
10600
|
*
|
|
10673
|
-
* When `signal` aborts (this navigation was superseded by a newer one)
|
|
10674
|
-
*
|
|
10675
|
-
*
|
|
10601
|
+
* When `signal` aborts (this navigation was superseded by a newer one) NOTHING is
|
|
10602
|
+
* applied: no swap (onEnd) and no fallback reload — the live navigation owns the
|
|
10603
|
+
* document from that point on.
|
|
10676
10604
|
*
|
|
10677
10605
|
* @param pathname - The destination pathname.
|
|
10678
10606
|
* @param handlers - The navigation lifecycle callbacks.
|
|
10679
10607
|
* @param signal - Aborts when this navigation is superseded (`navEvent.signal`).
|
|
10608
|
+
* @param load - Where the HTML comes from: {@link fetchPage} by default; the kernel
|
|
10609
|
+
* passes a loader that serves a preloaded page first (`loadWithPrefetch`).
|
|
10680
10610
|
* @returns A promise that resolves once the swap (or fallback) is dispatched.
|
|
10681
10611
|
* @example
|
|
10682
10612
|
* await performNavigation("/about", handlers, navEvent.signal);
|
|
10683
10613
|
*/
|
|
10684
|
-
async function performNavigation(pathname, handlers, signal) {
|
|
10614
|
+
async function performNavigation(pathname, handlers, signal, load = fetchPage) {
|
|
10685
10615
|
handlers.onStart(pathname);
|
|
10686
10616
|
try {
|
|
10687
|
-
const
|
|
10688
|
-
if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
|
|
10689
|
-
const html = await response.text();
|
|
10617
|
+
const html = await load(pathname, signal);
|
|
10690
10618
|
if (signal?.aborted) return;
|
|
10691
10619
|
handlers.onEnd(html, pathname);
|
|
10692
10620
|
} catch {
|
|
@@ -10794,18 +10722,16 @@ function swapRegion(doc, swapSelector, transition, onSwapped, beforeCapture) {
|
|
|
10794
10722
|
return true;
|
|
10795
10723
|
}
|
|
10796
10724
|
/**
|
|
10797
|
-
* Resolve
|
|
10798
|
-
*
|
|
10725
|
+
* Resolve the internal URL a link leads to, or `undefined` when the SPA leaves it
|
|
10726
|
+
* to the browser (no anchor, `target="_blank"`, another origin, a static asset). The
|
|
10727
|
+
* one link rule shared by the click interceptor and the preloader (`prefetch.ts`).
|
|
10799
10728
|
*
|
|
10800
|
-
* @param
|
|
10801
|
-
* @returns The internal URL
|
|
10729
|
+
* @param anchor - The link element, or nothing.
|
|
10730
|
+
* @returns The internal URL, or `undefined`.
|
|
10802
10731
|
* @example
|
|
10803
|
-
*
|
|
10732
|
+
* resolveLinkTarget(document.querySelector("a[href='/about']"))?.pathname; // "/about"
|
|
10804
10733
|
*/
|
|
10805
|
-
function
|
|
10806
|
-
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return void 0;
|
|
10807
|
-
if (event.defaultPrevented) return void 0;
|
|
10808
|
-
const anchor = event.target?.closest("a");
|
|
10734
|
+
function resolveLinkTarget(anchor) {
|
|
10809
10735
|
if (!anchor || anchor.target === "_blank") return void 0;
|
|
10810
10736
|
let url;
|
|
10811
10737
|
try {
|
|
@@ -10816,6 +10742,20 @@ function resolveClickTarget(event) {
|
|
|
10816
10742
|
return isInternalLink(url) ? url : void 0;
|
|
10817
10743
|
}
|
|
10818
10744
|
/**
|
|
10745
|
+
* Resolve a navigable internal URL from a click event, or `undefined` when the
|
|
10746
|
+
* click should not be intercepted (modifier keys, non-anchor, external, new-tab).
|
|
10747
|
+
*
|
|
10748
|
+
* @param event - The click event to inspect.
|
|
10749
|
+
* @returns The internal URL to navigate to, or `undefined`.
|
|
10750
|
+
* @example
|
|
10751
|
+
* const url = resolveClickTarget(event);
|
|
10752
|
+
*/
|
|
10753
|
+
function resolveClickTarget(event) {
|
|
10754
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return void 0;
|
|
10755
|
+
if (event.defaultPrevented) return void 0;
|
|
10756
|
+
return resolveLinkTarget(event.target?.closest("a"));
|
|
10757
|
+
}
|
|
10758
|
+
/**
|
|
10819
10759
|
* Attach the History-API click/popstate interception path (used when the
|
|
10820
10760
|
* Navigation API is unavailable).
|
|
10821
10761
|
*
|
|
@@ -10948,11 +10888,412 @@ function attachRouter(handlers, navigate, isOwnPush) {
|
|
|
10948
10888
|
return navigation ? attachNavigationApi(navigation, handlers, navigate, isOwnPush) : attachHistoryFallback(handlers, navigate);
|
|
10949
10889
|
}
|
|
10950
10890
|
//#endregion
|
|
10891
|
+
//#region src/plugins/spa/prefetch.ts
|
|
10892
|
+
/**
|
|
10893
|
+
* @file spa plugin — opt-in in-memory HTML preload cache (`spa.prefetch`).
|
|
10894
|
+
* @see README.md
|
|
10895
|
+
*/
|
|
10896
|
+
const HOVER_DELAY_MS = 65;
|
|
10897
|
+
const TTL_MS = 3e4;
|
|
10898
|
+
const MAX_ENTRIES = 20;
|
|
10899
|
+
const VISIBLE_CONCURRENCY = 2;
|
|
10900
|
+
const IDLE_FALLBACK_MS = 200;
|
|
10901
|
+
const SLOW_CONNECTION = /(^|-)2g$/;
|
|
10902
|
+
const PRESS = {
|
|
10903
|
+
capture: true,
|
|
10904
|
+
passive: true
|
|
10905
|
+
};
|
|
10906
|
+
const HOVER = { passive: true };
|
|
10907
|
+
/**
|
|
10908
|
+
* Read `navigator.connection` (mirrors `getNavigation()` in `router.ts`).
|
|
10909
|
+
*
|
|
10910
|
+
* @returns The connection info, or `undefined` where the browser has none.
|
|
10911
|
+
* @example
|
|
10912
|
+
* getConnection()?.saveData; // true with data saving on
|
|
10913
|
+
*/
|
|
10914
|
+
function getConnection() {
|
|
10915
|
+
return navigator.connection;
|
|
10916
|
+
}
|
|
10917
|
+
/**
|
|
10918
|
+
* Whether no preload may start now: data saving on, or a 2G connection.
|
|
10919
|
+
*
|
|
10920
|
+
* @returns True when preloads must wait.
|
|
10921
|
+
* @example
|
|
10922
|
+
* isConstrained(); // true on effectiveType "slow-2g"
|
|
10923
|
+
*/
|
|
10924
|
+
function isConstrained() {
|
|
10925
|
+
const connection = getConnection();
|
|
10926
|
+
return connection?.saveData === true || SLOW_CONNECTION.test(connection?.effectiveType ?? "");
|
|
10927
|
+
}
|
|
10928
|
+
/**
|
|
10929
|
+
* The link an event happened on.
|
|
10930
|
+
*
|
|
10931
|
+
* @param event - A press or mouse event.
|
|
10932
|
+
* @returns The closest `<a>`, or nothing.
|
|
10933
|
+
* @example
|
|
10934
|
+
* anchorOf(event); // <a href="/about"> for a tap on the <span> inside it
|
|
10935
|
+
*/
|
|
10936
|
+
function anchorOf(event) {
|
|
10937
|
+
return event.target instanceof Element ? event.target.closest("a") : void 0;
|
|
10938
|
+
}
|
|
10939
|
+
/**
|
|
10940
|
+
* Run `task` in idle time, or after 200 ms where `requestIdleCallback` is missing.
|
|
10941
|
+
*
|
|
10942
|
+
* @param task - The work to run.
|
|
10943
|
+
* @returns Cancels the task.
|
|
10944
|
+
* @example
|
|
10945
|
+
* const cancel = whenIdle(drain); // cancel() before idle time: drain never runs
|
|
10946
|
+
*/
|
|
10947
|
+
function whenIdle(task) {
|
|
10948
|
+
if (typeof requestIdleCallback === "function") {
|
|
10949
|
+
const handle = requestIdleCallback(task);
|
|
10950
|
+
return () => cancelIdleCallback(handle);
|
|
10951
|
+
}
|
|
10952
|
+
const handle = setTimeout(task, IDLE_FALLBACK_MS);
|
|
10953
|
+
return () => clearTimeout(handle);
|
|
10954
|
+
}
|
|
10955
|
+
/**
|
|
10956
|
+
* Build the preloader: press and hover listeners on `document` at once, and for
|
|
10957
|
+
* `"visible"` one IntersectionObserver that {@link Prefetcher.observe} feeds.
|
|
10958
|
+
*
|
|
10959
|
+
* @param options - The mode, the HTML-path check and the clock.
|
|
10960
|
+
* @returns The live preloader.
|
|
10961
|
+
* @example
|
|
10962
|
+
* createPrefetcher({ mode: "intent", usesHtml: () => true }).take("/about"); // undefined
|
|
10963
|
+
*/
|
|
10964
|
+
function createPrefetcher(options) {
|
|
10965
|
+
const now = options.now ?? Date.now;
|
|
10966
|
+
const cache = /* @__PURE__ */ new Map();
|
|
10967
|
+
const hoverTimers = /* @__PURE__ */ new Map();
|
|
10968
|
+
const queue = [];
|
|
10969
|
+
let visibleInFlight = 0;
|
|
10970
|
+
let cancelDrain;
|
|
10971
|
+
/**
|
|
10972
|
+
* Whether an entry is older than the TTL.
|
|
10973
|
+
*
|
|
10974
|
+
* @param entry - A cache entry.
|
|
10975
|
+
* @returns True when the entry must not be used.
|
|
10976
|
+
* @example
|
|
10977
|
+
* isExpired(entry); // true 30_001 ms after it started
|
|
10978
|
+
*/
|
|
10979
|
+
const isExpired = (entry) => now() - entry.startedAt > TTL_MS;
|
|
10980
|
+
/**
|
|
10981
|
+
* Abort an entry's request and remove it.
|
|
10982
|
+
*
|
|
10983
|
+
* @param key - The entry's path.
|
|
10984
|
+
* @param entry - The entry.
|
|
10985
|
+
* @example
|
|
10986
|
+
* drop("/a/", entry);
|
|
10987
|
+
*/
|
|
10988
|
+
const drop = (key, entry) => {
|
|
10989
|
+
entry.controller.abort();
|
|
10990
|
+
cache.delete(key);
|
|
10991
|
+
};
|
|
10992
|
+
/**
|
|
10993
|
+
* The path a link would preload, or `undefined` when it is not eligible.
|
|
10994
|
+
*
|
|
10995
|
+
* @param anchor - The link, or nothing.
|
|
10996
|
+
* @returns The path (pathname + search), or `undefined`.
|
|
10997
|
+
* @example
|
|
10998
|
+
* pathOf(document.querySelector("a[href='/about?x=1#top']")); // "/about?x=1"
|
|
10999
|
+
*/
|
|
11000
|
+
const pathOf = (anchor) => {
|
|
11001
|
+
const url = resolveLinkTarget(anchor);
|
|
11002
|
+
if (!url || anchor?.hasAttribute("download")) return void 0;
|
|
11003
|
+
const path = pathWithSearch(url);
|
|
11004
|
+
return path !== pathWithSearch(location) && options.usesHtml(path) ? path : void 0;
|
|
11005
|
+
};
|
|
11006
|
+
/**
|
|
11007
|
+
* Start one preload, unless the connection forbids it or the path is already cached.
|
|
11008
|
+
*
|
|
11009
|
+
* @param path - The page path.
|
|
11010
|
+
* @returns The new entry, or `undefined` when nothing started.
|
|
11011
|
+
* @example
|
|
11012
|
+
* start("/about"); // { promise, startedAt, controller }
|
|
11013
|
+
*/
|
|
11014
|
+
const start = (path) => {
|
|
11015
|
+
if (isConstrained()) return void 0;
|
|
11016
|
+
for (const [key, entry] of cache) if (isExpired(entry)) drop(key, entry);
|
|
11017
|
+
if (cache.has(path)) return void 0;
|
|
11018
|
+
for (const [key, entry] of cache) {
|
|
11019
|
+
if (cache.size < MAX_ENTRIES) break;
|
|
11020
|
+
drop(key, entry);
|
|
11021
|
+
}
|
|
11022
|
+
const controller = new AbortController();
|
|
11023
|
+
const promise = fetchPage(path, controller.signal);
|
|
11024
|
+
const entry = {
|
|
11025
|
+
promise,
|
|
11026
|
+
startedAt: now(),
|
|
11027
|
+
controller
|
|
11028
|
+
};
|
|
11029
|
+
cache.set(path, entry);
|
|
11030
|
+
promise.catch(() => {
|
|
11031
|
+
if (cache.get(path) === entry) cache.delete(path);
|
|
11032
|
+
});
|
|
11033
|
+
return entry;
|
|
11034
|
+
};
|
|
11035
|
+
/**
|
|
11036
|
+
* Preload a link when it is eligible.
|
|
11037
|
+
*
|
|
11038
|
+
* @param anchor - The link, or nothing.
|
|
11039
|
+
* @example
|
|
11040
|
+
* preloadLink(document.querySelector("a"));
|
|
11041
|
+
*/
|
|
11042
|
+
const preloadLink = (anchor) => {
|
|
11043
|
+
const path = pathOf(anchor);
|
|
11044
|
+
if (path !== void 0) start(path);
|
|
11045
|
+
};
|
|
11046
|
+
/**
|
|
11047
|
+
* Start queued `"visible"` preloads while fewer than 2 are in flight.
|
|
11048
|
+
*
|
|
11049
|
+
* @example
|
|
11050
|
+
* cancelDrain = whenIdle(drain);
|
|
11051
|
+
*/
|
|
11052
|
+
const drain = () => {
|
|
11053
|
+
cancelDrain = void 0;
|
|
11054
|
+
while (visibleInFlight < VISIBLE_CONCURRENCY) {
|
|
11055
|
+
const path = queue.shift();
|
|
11056
|
+
if (path === void 0) return;
|
|
11057
|
+
const entry = start(path);
|
|
11058
|
+
if (!entry) continue;
|
|
11059
|
+
visibleInFlight += 1;
|
|
11060
|
+
entry.promise.catch(() => "").then(() => {
|
|
11061
|
+
visibleInFlight -= 1;
|
|
11062
|
+
scheduleDrain();
|
|
11063
|
+
});
|
|
11064
|
+
}
|
|
11065
|
+
};
|
|
11066
|
+
/**
|
|
11067
|
+
* Drain the queue in idle time, once.
|
|
11068
|
+
*
|
|
11069
|
+
* @example
|
|
11070
|
+
* scheduleDrain();
|
|
11071
|
+
*/
|
|
11072
|
+
const scheduleDrain = () => {
|
|
11073
|
+
if (cancelDrain === void 0 && queue.length > 0) cancelDrain = whenIdle(drain);
|
|
11074
|
+
};
|
|
11075
|
+
/**
|
|
11076
|
+
* `pointerdown` / `touchstart`: preload at once.
|
|
11077
|
+
*
|
|
11078
|
+
* @param event - The press.
|
|
11079
|
+
* @example
|
|
11080
|
+
* document.addEventListener("pointerdown", onPress, PRESS);
|
|
11081
|
+
*/
|
|
11082
|
+
const onPress = (event) => {
|
|
11083
|
+
preloadLink(anchorOf(event));
|
|
11084
|
+
};
|
|
11085
|
+
/**
|
|
11086
|
+
* `mouseover`: preload after 65 ms on the link.
|
|
11087
|
+
*
|
|
11088
|
+
* @param event - The mouse event.
|
|
11089
|
+
* @example
|
|
11090
|
+
* document.addEventListener("mouseover", onOver, HOVER);
|
|
11091
|
+
*/
|
|
11092
|
+
const onOver = (event) => {
|
|
11093
|
+
const anchor = anchorOf(event);
|
|
11094
|
+
if (!anchor || hoverTimers.has(anchor)) return;
|
|
11095
|
+
const timer = setTimeout(() => {
|
|
11096
|
+
hoverTimers.delete(anchor);
|
|
11097
|
+
preloadLink(anchor);
|
|
11098
|
+
}, HOVER_DELAY_MS);
|
|
11099
|
+
hoverTimers.set(anchor, timer);
|
|
11100
|
+
};
|
|
11101
|
+
/**
|
|
11102
|
+
* `mouseout`: cancel the hover timer, unless the mouse only moved inside the same link.
|
|
11103
|
+
*
|
|
11104
|
+
* @param event - The mouse event.
|
|
11105
|
+
* @example
|
|
11106
|
+
* document.addEventListener("mouseout", onOut, HOVER);
|
|
11107
|
+
*/
|
|
11108
|
+
const onOut = (event) => {
|
|
11109
|
+
const anchor = anchorOf(event);
|
|
11110
|
+
const into = event.relatedTarget;
|
|
11111
|
+
if (!anchor || into instanceof Node && anchor.contains(into)) return;
|
|
11112
|
+
clearTimeout(hoverTimers.get(anchor));
|
|
11113
|
+
hoverTimers.delete(anchor);
|
|
11114
|
+
};
|
|
11115
|
+
const observer = options.mode === "visible" && typeof IntersectionObserver === "function" ? new IntersectionObserver((entries, self) => {
|
|
11116
|
+
for (const entry of entries) {
|
|
11117
|
+
if (!entry.isIntersecting) continue;
|
|
11118
|
+
self.unobserve(entry.target);
|
|
11119
|
+
const path = pathOf(entry.target.closest("a"));
|
|
11120
|
+
if (path !== void 0) queue.push(path);
|
|
11121
|
+
}
|
|
11122
|
+
scheduleDrain();
|
|
11123
|
+
}) : void 0;
|
|
11124
|
+
document.addEventListener("pointerdown", onPress, PRESS);
|
|
11125
|
+
document.addEventListener("touchstart", onPress, PRESS);
|
|
11126
|
+
document.addEventListener("mouseover", onOver, HOVER);
|
|
11127
|
+
document.addEventListener("mouseout", onOut, HOVER);
|
|
11128
|
+
return {
|
|
11129
|
+
/**
|
|
11130
|
+
* Take the preload for `path` and remove it (see {@link Prefetcher.take}).
|
|
11131
|
+
*
|
|
11132
|
+
* @param path - The page path.
|
|
11133
|
+
* @returns The page HTML promise, or `undefined`.
|
|
11134
|
+
* @example
|
|
11135
|
+
* prefetcher.take("/about");
|
|
11136
|
+
*/
|
|
11137
|
+
take(path) {
|
|
11138
|
+
const entry = cache.get(path);
|
|
11139
|
+
if (!entry) return void 0;
|
|
11140
|
+
if (isExpired(entry)) {
|
|
11141
|
+
drop(path, entry);
|
|
11142
|
+
return;
|
|
11143
|
+
}
|
|
11144
|
+
cache.delete(path);
|
|
11145
|
+
return entry.promise;
|
|
11146
|
+
},
|
|
11147
|
+
/**
|
|
11148
|
+
* Watch every eligible link in the document (see {@link Prefetcher.observe}).
|
|
11149
|
+
*
|
|
11150
|
+
* @example
|
|
11151
|
+
* prefetcher.observe();
|
|
11152
|
+
*/
|
|
11153
|
+
observe() {
|
|
11154
|
+
if (!observer) return;
|
|
11155
|
+
observer.disconnect();
|
|
11156
|
+
queue.length = 0;
|
|
11157
|
+
for (const anchor of document.querySelectorAll("a[href]")) if (pathOf(anchor) !== void 0) observer.observe(anchor);
|
|
11158
|
+
},
|
|
11159
|
+
/**
|
|
11160
|
+
* Release everything (see {@link Prefetcher.dispose}).
|
|
11161
|
+
*
|
|
11162
|
+
* @example
|
|
11163
|
+
* prefetcher.dispose();
|
|
11164
|
+
*/
|
|
11165
|
+
dispose() {
|
|
11166
|
+
document.removeEventListener("pointerdown", onPress, PRESS);
|
|
11167
|
+
document.removeEventListener("touchstart", onPress, PRESS);
|
|
11168
|
+
document.removeEventListener("mouseover", onOver, HOVER);
|
|
11169
|
+
document.removeEventListener("mouseout", onOut, HOVER);
|
|
11170
|
+
observer?.disconnect();
|
|
11171
|
+
for (const timer of hoverTimers.values()) clearTimeout(timer);
|
|
11172
|
+
hoverTimers.clear();
|
|
11173
|
+
cancelDrain?.();
|
|
11174
|
+
cancelDrain = void 0;
|
|
11175
|
+
queue.length = 0;
|
|
11176
|
+
for (const [key, entry] of cache) drop(key, entry);
|
|
11177
|
+
}
|
|
11178
|
+
};
|
|
11179
|
+
}
|
|
11180
|
+
/**
|
|
11181
|
+
* The HTML loader the kernel hands to `performNavigation`: a preloaded page when there is
|
|
11182
|
+
* one, else a normal fetch. A failed preload falls through to one real fetch, so it never
|
|
11183
|
+
* leads to a full reload; only a failed real fetch does, as without preloading.
|
|
11184
|
+
*
|
|
11185
|
+
* @param prefetcher - The live preloader, or `null` (mode `false`, or stopped).
|
|
11186
|
+
* @param path - The page path (pathname + search).
|
|
11187
|
+
* @param signal - The navigation's abort signal; never passed to the preload.
|
|
11188
|
+
* @returns The page HTML.
|
|
11189
|
+
* @throws {Error} When the real fetch fails, or the navigation was aborted after its preload failed.
|
|
11190
|
+
* @example
|
|
11191
|
+
* await loadWithPrefetch(state.prefetch, "/about", signal); // the preloaded HTML, no new request
|
|
11192
|
+
*/
|
|
11193
|
+
async function loadWithPrefetch(prefetcher, path, signal) {
|
|
11194
|
+
const hit = prefetcher?.take(path);
|
|
11195
|
+
if (hit) try {
|
|
11196
|
+
return await hit;
|
|
11197
|
+
} catch {
|
|
11198
|
+
signal?.throwIfAborted();
|
|
11199
|
+
}
|
|
11200
|
+
return fetchPage(path, signal);
|
|
11201
|
+
}
|
|
11202
|
+
//#endregion
|
|
11203
|
+
//#region src/plugins/spa/progress.ts
|
|
11204
|
+
/** Delay before the bar appears, so fast navigations show no indicator. */
|
|
11205
|
+
const START_DELAY_MS = 150;
|
|
11206
|
+
/** Interval between trickle increments while loading. */
|
|
11207
|
+
const TRICKLE_MS = 300;
|
|
11208
|
+
/** Linger before the completed bar is reset/hidden. */
|
|
11209
|
+
const DONE_LINGER_MS = 200;
|
|
11210
|
+
/** Ceiling the bar trickles to while still loading (never reaches 100% until done). */
|
|
11211
|
+
const TRICKLE_CEIL = 90;
|
|
11212
|
+
/** No-op progress bar used when disabled or in a headless context. */
|
|
11213
|
+
const NOOP_BAR = {
|
|
11214
|
+
start() {},
|
|
11215
|
+
done() {}
|
|
11216
|
+
};
|
|
11217
|
+
/**
|
|
11218
|
+
* Creates the in-house progress bar (150ms delay + trickle). A no-op shell when
|
|
11219
|
+
* progress is disabled or no DOM is present. The progress element is created
|
|
11220
|
+
* once (prepended to `<body>` as `<div data-progress>`) and reused across navs.
|
|
11221
|
+
*
|
|
11222
|
+
* @param enabled - Whether the progress bar is active.
|
|
11223
|
+
* @returns A {@link ProgressBar} with `start`/`done`. Disabled/headless → no-ops.
|
|
11224
|
+
* @example
|
|
11225
|
+
* const bar = createProgressBar(true);
|
|
11226
|
+
* bar.start();
|
|
11227
|
+
* bar.done();
|
|
11228
|
+
*/
|
|
11229
|
+
function createProgressBar(enabled) {
|
|
11230
|
+
if (!enabled || typeof document === "undefined") return NOOP_BAR;
|
|
11231
|
+
const element = document.createElement("div");
|
|
11232
|
+
element.dataset.progress = "";
|
|
11233
|
+
document.body.prepend(element);
|
|
11234
|
+
let delayTimer;
|
|
11235
|
+
let trickleTimer;
|
|
11236
|
+
let width = 0;
|
|
11237
|
+
/**
|
|
11238
|
+
* Step the trickle upward toward the ceiling and reschedule.
|
|
11239
|
+
*
|
|
11240
|
+
* @example
|
|
11241
|
+
* trickle();
|
|
11242
|
+
*/
|
|
11243
|
+
const trickle = () => {
|
|
11244
|
+
if (width >= TRICKLE_CEIL) return;
|
|
11245
|
+
width = Math.min(TRICKLE_CEIL, width + 5 + Math.random() * 10);
|
|
11246
|
+
element.style.width = `${String(width)}%`;
|
|
11247
|
+
trickleTimer = setTimeout(trickle, TRICKLE_MS);
|
|
11248
|
+
};
|
|
11249
|
+
/**
|
|
11250
|
+
* Show the bar after the start delay and begin trickling.
|
|
11251
|
+
*
|
|
11252
|
+
* @example
|
|
11253
|
+
* start();
|
|
11254
|
+
*/
|
|
11255
|
+
const start = () => {
|
|
11256
|
+
delayTimer = setTimeout(() => {
|
|
11257
|
+
width = 15;
|
|
11258
|
+
element.style.width = "15%";
|
|
11259
|
+
element.dataset.active = "";
|
|
11260
|
+
trickle();
|
|
11261
|
+
}, START_DELAY_MS);
|
|
11262
|
+
};
|
|
11263
|
+
/**
|
|
11264
|
+
* Complete the bar to 100%, then reset/hide it after a short linger.
|
|
11265
|
+
*
|
|
11266
|
+
* @example
|
|
11267
|
+
* done();
|
|
11268
|
+
*/
|
|
11269
|
+
const done = () => {
|
|
11270
|
+
clearTimeout(delayTimer);
|
|
11271
|
+
clearTimeout(trickleTimer);
|
|
11272
|
+
element.style.width = "100%";
|
|
11273
|
+
element.dataset.active = "";
|
|
11274
|
+
setTimeout(() => {
|
|
11275
|
+
delete element.dataset.active;
|
|
11276
|
+
element.style.width = "0%";
|
|
11277
|
+
width = 0;
|
|
11278
|
+
}, DONE_LINGER_MS);
|
|
11279
|
+
};
|
|
11280
|
+
return {
|
|
11281
|
+
start,
|
|
11282
|
+
done
|
|
11283
|
+
};
|
|
11284
|
+
}
|
|
11285
|
+
//#endregion
|
|
10951
11286
|
//#region src/plugins/spa/state.ts
|
|
10952
11287
|
/** Error prefix for spa config-validation failures (spec/11 Part-3). */
|
|
10953
11288
|
const ERROR_PREFIX$1 = "[web]";
|
|
10954
11289
|
/** Last-resort `swapSelector` when neither config nor defaults supply one. */
|
|
10955
11290
|
const FALLBACK_SWAP_SELECTOR = "main > section";
|
|
11291
|
+
/** Every value `spa.prefetch` accepts. */
|
|
11292
|
+
const PREFETCH_MODES = new Set([
|
|
11293
|
+
false,
|
|
11294
|
+
"intent",
|
|
11295
|
+
"visible"
|
|
11296
|
+
]);
|
|
10956
11297
|
/**
|
|
10957
11298
|
* Resolve the `viewTransitions` config value into the default {@link TransitionMode}:
|
|
10958
11299
|
* `true` → `"crossfade"`, `false` → `"none"`, a named mode → itself.
|
|
@@ -10967,13 +11308,28 @@ function resolveDefaultTransition(raw) {
|
|
|
10967
11308
|
if (raw === false) return "none";
|
|
10968
11309
|
return raw;
|
|
10969
11310
|
}
|
|
11311
|
+
/**
|
|
11312
|
+
* Validate `spa.prefetch` (spec/11 Part-3 error). An untyped config can carry any value,
|
|
11313
|
+
* so a typo like `"hover"` fails at init instead of silently disabling the preload.
|
|
11314
|
+
*
|
|
11315
|
+
* @param mode - The raw `prefetch` config value (absent ⇒ `false`).
|
|
11316
|
+
* @returns The validated mode.
|
|
11317
|
+
* @throws {Error} When the value is not `false`, `"intent"` or `"visible"`.
|
|
11318
|
+
* @example
|
|
11319
|
+
* resolvePrefetchMode(undefined); // false
|
|
11320
|
+
*/
|
|
11321
|
+
function resolvePrefetchMode(mode = false) {
|
|
11322
|
+
if (!PREFETCH_MODES.has(mode)) throw new Error(`${ERROR_PREFIX$1} spa.prefetch must be false, "intent" or "visible". Got ${JSON.stringify(mode)}.\n Set spa.prefetch to one of the allowed values, or leave it out.`);
|
|
11323
|
+
return mode;
|
|
11324
|
+
}
|
|
10970
11325
|
/** Default SPA config (declared as a value — no inline assertion). */
|
|
10971
11326
|
const defaultSpaConfig = {
|
|
10972
11327
|
swapSelector: "main > section",
|
|
10973
11328
|
viewTransitions: false,
|
|
10974
11329
|
scrollRestoration: "top",
|
|
10975
11330
|
progressBar: true,
|
|
10976
|
-
islands: []
|
|
11331
|
+
islands: [],
|
|
11332
|
+
prefetch: false
|
|
10977
11333
|
};
|
|
10978
11334
|
/**
|
|
10979
11335
|
* Whether a selector is syntactically valid (parseable by the DOM). Falls back
|
|
@@ -11000,7 +11356,8 @@ function isValidSelector(selector) {
|
|
|
11000
11356
|
*
|
|
11001
11357
|
* @param config - The raw spa config to validate.
|
|
11002
11358
|
* @returns The fully-resolved config with defaults applied.
|
|
11003
|
-
* @throws {Error} When `swapSelector` is empty or not a valid CSS selector
|
|
11359
|
+
* @throws {Error} When `swapSelector` is empty or not a valid CSS selector, or `prefetch`
|
|
11360
|
+
* is not one of its three values.
|
|
11004
11361
|
* @example
|
|
11005
11362
|
* const resolved = resolveSpaConfig({ swapSelector: "main > section" });
|
|
11006
11363
|
*/
|
|
@@ -11015,7 +11372,8 @@ function resolveSpaConfig(config) {
|
|
|
11015
11372
|
defaultTransition,
|
|
11016
11373
|
scrollRestoration: config.scrollRestoration ?? "top",
|
|
11017
11374
|
progressBar: config.progressBar ?? true,
|
|
11018
|
-
islands: config.islands ?? []
|
|
11375
|
+
islands: config.islands ?? [],
|
|
11376
|
+
prefetch: resolvePrefetchMode(config.prefetch)
|
|
11019
11377
|
};
|
|
11020
11378
|
}
|
|
11021
11379
|
/**
|
|
@@ -11039,7 +11397,8 @@ function createState(_ctx) {
|
|
|
11039
11397
|
destroyRouter: null,
|
|
11040
11398
|
started: false,
|
|
11041
11399
|
navigate: null,
|
|
11042
|
-
kernel: null
|
|
11400
|
+
kernel: null,
|
|
11401
|
+
prefetch: null
|
|
11043
11402
|
};
|
|
11044
11403
|
}
|
|
11045
11404
|
//#endregion
|
|
@@ -11077,6 +11436,21 @@ function currentLocationUrl() {
|
|
|
11077
11436
|
return location.pathname + location.search;
|
|
11078
11437
|
}
|
|
11079
11438
|
/**
|
|
11439
|
+
* Whether a navigation to `path` renders from fetched HTML, so a preload of it is used.
|
|
11440
|
+
*
|
|
11441
|
+
* @param deps - The router and the optional data reader.
|
|
11442
|
+
* @param path - The page path (pathname + search).
|
|
11443
|
+
* @returns False when the client DATA path renders the page.
|
|
11444
|
+
* @example
|
|
11445
|
+
* usesHtml({ router, head }, "/en/about/"); // true: no data reader, every page is HTML
|
|
11446
|
+
*/
|
|
11447
|
+
function usesHtml(deps, path) {
|
|
11448
|
+
if (deps.router.mode() === "ssg") return true;
|
|
11449
|
+
const route = deps.router.match(path.split("?")[0] ?? path)?.route;
|
|
11450
|
+
if (!route?._handlers.render) return true;
|
|
11451
|
+
return !isClientOnlyRoute(deps.router.mode(), route) && !deps.dataAt;
|
|
11452
|
+
}
|
|
11453
|
+
/**
|
|
11080
11454
|
* Apply the matched route's `head` config to the live document (minimal client
|
|
11081
11455
|
* head-sync for the DATA path: title only — the full meta sync runs on the
|
|
11082
11456
|
* HTML-over-fetch path from the fetched `<head>`). The title is resolved through
|
|
@@ -11236,6 +11610,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11236
11610
|
const routeSlice = islandRouteContext(pathname);
|
|
11237
11611
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
11238
11612
|
notifyNavEnd(state, routeSlice);
|
|
11613
|
+
state.prefetch?.observe();
|
|
11239
11614
|
}, applyPendingScroll)) {
|
|
11240
11615
|
handleError();
|
|
11241
11616
|
location.href = pathname;
|
|
@@ -11350,6 +11725,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11350
11725
|
renderVNode(vnode, region);
|
|
11351
11726
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
11352
11727
|
notifyNavEnd(state, routeSlice);
|
|
11728
|
+
state.prefetch?.observe();
|
|
11353
11729
|
};
|
|
11354
11730
|
runSwap(renderAndMount, pendingTransition, applyPendingScroll);
|
|
11355
11731
|
state.currentUrl = pathname;
|
|
@@ -11398,12 +11774,14 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11398
11774
|
const resolvedRender = await resolveDataRender(pathname);
|
|
11399
11775
|
if (resolvedRender === false) {
|
|
11400
11776
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
11777
|
+
state.prefetch?.observe();
|
|
11401
11778
|
return;
|
|
11402
11779
|
}
|
|
11403
11780
|
const { vnode, region } = resolvedRender;
|
|
11404
11781
|
const { renderVNode } = await import("./render-CkdqOmsm.mjs");
|
|
11405
11782
|
renderVNode(vnode, region);
|
|
11406
11783
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
11784
|
+
state.prefetch?.observe();
|
|
11407
11785
|
};
|
|
11408
11786
|
/**
|
|
11409
11787
|
* Unified navigation: try the client DATA path first (only when the `data`
|
|
@@ -11427,7 +11805,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11427
11805
|
handleStart(pathname);
|
|
11428
11806
|
if (deps.router.mode() !== "ssg" && await tryDataRender(pathname, signal)) return;
|
|
11429
11807
|
if (signal?.aborted) return;
|
|
11430
|
-
await performNavigation(pathname, handlers, signal);
|
|
11808
|
+
await performNavigation(pathname, handlers, signal, (path, loadSignal) => loadWithPrefetch(state.prefetch, path, loadSignal));
|
|
11431
11809
|
};
|
|
11432
11810
|
/**
|
|
11433
11811
|
* Programmatic navigation entry (`app.spa.navigate` / `ctx.navigate`). A link click or the
|
|
@@ -11495,10 +11873,17 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11495
11873
|
supersedeProgrammatic();
|
|
11496
11874
|
return navigate(pathname, scrollToTop, signal, scrollOverride);
|
|
11497
11875
|
}, () => pushingOwnUrl);
|
|
11876
|
+
if (resolved.prefetch !== false) state.prefetch = createPrefetcher({
|
|
11877
|
+
mode: resolved.prefetch,
|
|
11878
|
+
usesHtml: (path) => usesHtml(deps, path)
|
|
11879
|
+
});
|
|
11498
11880
|
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
11499
11881
|
const hit = deps.router.match(matchPath);
|
|
11500
11882
|
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
|
11501
|
-
else
|
|
11883
|
+
else {
|
|
11884
|
+
scanAndMount(state, emit, resolved.swapSelector, islandRouteContext(state.currentUrl));
|
|
11885
|
+
state.prefetch?.observe();
|
|
11886
|
+
}
|
|
11502
11887
|
state.started = true;
|
|
11503
11888
|
},
|
|
11504
11889
|
/**
|
|
@@ -11538,6 +11923,8 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11538
11923
|
if (typeof document === "undefined") return;
|
|
11539
11924
|
state.destroyRouter?.();
|
|
11540
11925
|
state.destroyRouter = null;
|
|
11926
|
+
state.prefetch?.dispose();
|
|
11927
|
+
state.prefetch = null;
|
|
11541
11928
|
location.assign(url);
|
|
11542
11929
|
},
|
|
11543
11930
|
/**
|
|
@@ -11558,6 +11945,8 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11558
11945
|
dispose() {
|
|
11559
11946
|
state.destroyRouter?.();
|
|
11560
11947
|
state.destroyRouter = null;
|
|
11948
|
+
state.prefetch?.dispose();
|
|
11949
|
+
state.prefetch = null;
|
|
11561
11950
|
unmountAll(state, emit);
|
|
11562
11951
|
progress = void 0;
|
|
11563
11952
|
state.started = false;
|