@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.cjs
CHANGED
|
@@ -4666,9 +4666,8 @@ function dataApi(ctx) {
|
|
|
4666
4666
|
return {
|
|
4667
4667
|
/**
|
|
4668
4668
|
* READ (browser) — fetch (and cache) the persisted data for a page path.
|
|
4669
|
-
* Concurrent calls for one path share one in-flight request.
|
|
4670
|
-
*
|
|
4671
|
-
* only if the bundle did not bring the path.
|
|
4669
|
+
* Concurrent calls for one path share one in-flight request. It never waits
|
|
4670
|
+
* for an in-flight preload bundle: an uncached path is fetched at once.
|
|
4672
4671
|
* Returns the raw JSON as `unknown`, which the route uses directly as `ctx.data`
|
|
4673
4672
|
* (no route `.parse()`); returns `null` if the fetch or JSON parse fails (so
|
|
4674
4673
|
* `spa` can fall back to HTML).
|
|
@@ -4682,10 +4681,6 @@ function dataApi(ctx) {
|
|
|
4682
4681
|
*/
|
|
4683
4682
|
async at(path) {
|
|
4684
4683
|
if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
|
|
4685
|
-
if (ctx.state.bundle) {
|
|
4686
|
-
await ctx.state.bundle;
|
|
4687
|
-
if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
|
|
4688
|
-
}
|
|
4689
4684
|
const inFlight = ctx.state.pending.get(path);
|
|
4690
4685
|
if (inFlight) return inFlight;
|
|
4691
4686
|
const request = fetchAndCache(ctx, path);
|
|
@@ -4760,7 +4755,7 @@ const defaultDataConfig = {
|
|
|
4760
4755
|
//#region src/plugins/data/preload.ts
|
|
4761
4756
|
/**
|
|
4762
4757
|
* Fetches the bundle and caches every path not already cached. Never rejects:
|
|
4763
|
-
* a failed fetch leaves lazy `at()` working. Clears
|
|
4758
|
+
* a failed fetch leaves lazy `at()` working. Clears `cancelPreload` when done.
|
|
4764
4759
|
*
|
|
4765
4760
|
* @param ctx - The data plugin context.
|
|
4766
4761
|
* @param signal - Aborts the fetch on stop.
|
|
@@ -4776,7 +4771,7 @@ async function fill(ctx, signal) {
|
|
|
4776
4771
|
const bundle = response.ok ? await response.json() : {};
|
|
4777
4772
|
for (const [path, data] of Object.entries(bundle)) if (!signal.aborted && !ctx.state.cache.has(path)) ctx.state.cache.set(path, data);
|
|
4778
4773
|
} catch {} finally {
|
|
4779
|
-
ctx.state.
|
|
4774
|
+
ctx.state.cancelPreload = null;
|
|
4780
4775
|
}
|
|
4781
4776
|
}
|
|
4782
4777
|
/**
|
|
@@ -4794,7 +4789,7 @@ function startPreload(ctx) {
|
|
|
4794
4789
|
if (ctx.config.preload !== "all" || typeof document === "undefined" || ctx.state.cancelPreload !== null || nav?.connection?.saveData === true) return;
|
|
4795
4790
|
const controller = new AbortController();
|
|
4796
4791
|
const run = () => {
|
|
4797
|
-
|
|
4792
|
+
fill(ctx, controller.signal);
|
|
4798
4793
|
};
|
|
4799
4794
|
const idle = typeof requestIdleCallback === "function";
|
|
4800
4795
|
const handle = idle ? requestIdleCallback(run, { timeout: 2e3 }) : setTimeout(run, 0);
|
|
@@ -4823,7 +4818,7 @@ function stopPreload(ctx) {
|
|
|
4823
4818
|
* Creates initial data state: a null `lastWrite` slot (populated by the Node
|
|
4824
4819
|
* `write()` side), an empty `cache` (populated lazily by the browser `at(path)`
|
|
4825
4820
|
* side on first fetch), an empty `pending` map of in-flight fetches, and no
|
|
4826
|
-
* preload
|
|
4821
|
+
* preload scheduled.
|
|
4827
4822
|
*
|
|
4828
4823
|
* @param _ctx - Minimal context with global and config.
|
|
4829
4824
|
* @param _ctx.global - Global framework configuration.
|
|
@@ -4839,7 +4834,6 @@ function createDataState(_ctx) {
|
|
|
4839
4834
|
lastWrite: null,
|
|
4840
4835
|
cache: /* @__PURE__ */ new Map(),
|
|
4841
4836
|
pending: /* @__PURE__ */ new Map(),
|
|
4842
|
-
bundle: null,
|
|
4843
4837
|
cancelPreload: null
|
|
4844
4838
|
};
|
|
4845
4839
|
}
|
|
@@ -10561,89 +10555,6 @@ function notifyNavEnd(state, route = EMPTY_ROUTE) {
|
|
|
10561
10555
|
}
|
|
10562
10556
|
}
|
|
10563
10557
|
//#endregion
|
|
10564
|
-
//#region src/plugins/spa/progress.ts
|
|
10565
|
-
/** Delay before the bar appears, so fast navigations show no indicator. */
|
|
10566
|
-
const START_DELAY_MS = 150;
|
|
10567
|
-
/** Interval between trickle increments while loading. */
|
|
10568
|
-
const TRICKLE_MS = 300;
|
|
10569
|
-
/** Linger before the completed bar is reset/hidden. */
|
|
10570
|
-
const DONE_LINGER_MS = 200;
|
|
10571
|
-
/** Ceiling the bar trickles to while still loading (never reaches 100% until done). */
|
|
10572
|
-
const TRICKLE_CEIL = 90;
|
|
10573
|
-
/** No-op progress bar used when disabled or in a headless context. */
|
|
10574
|
-
const NOOP_BAR = {
|
|
10575
|
-
start() {},
|
|
10576
|
-
done() {}
|
|
10577
|
-
};
|
|
10578
|
-
/**
|
|
10579
|
-
* Creates the in-house progress bar (150ms delay + trickle). A no-op shell when
|
|
10580
|
-
* progress is disabled or no DOM is present. The progress element is created
|
|
10581
|
-
* once (prepended to `<body>` as `<div data-progress>`) and reused across navs.
|
|
10582
|
-
*
|
|
10583
|
-
* @param enabled - Whether the progress bar is active.
|
|
10584
|
-
* @returns A {@link ProgressBar} with `start`/`done`. Disabled/headless → no-ops.
|
|
10585
|
-
* @example
|
|
10586
|
-
* const bar = createProgressBar(true);
|
|
10587
|
-
* bar.start();
|
|
10588
|
-
* bar.done();
|
|
10589
|
-
*/
|
|
10590
|
-
function createProgressBar(enabled) {
|
|
10591
|
-
if (!enabled || typeof document === "undefined") return NOOP_BAR;
|
|
10592
|
-
const element = document.createElement("div");
|
|
10593
|
-
element.dataset.progress = "";
|
|
10594
|
-
document.body.prepend(element);
|
|
10595
|
-
let delayTimer;
|
|
10596
|
-
let trickleTimer;
|
|
10597
|
-
let width = 0;
|
|
10598
|
-
/**
|
|
10599
|
-
* Step the trickle upward toward the ceiling and reschedule.
|
|
10600
|
-
*
|
|
10601
|
-
* @example
|
|
10602
|
-
* trickle();
|
|
10603
|
-
*/
|
|
10604
|
-
const trickle = () => {
|
|
10605
|
-
if (width >= TRICKLE_CEIL) return;
|
|
10606
|
-
width = Math.min(TRICKLE_CEIL, width + 5 + Math.random() * 10);
|
|
10607
|
-
element.style.width = `${String(width)}%`;
|
|
10608
|
-
trickleTimer = setTimeout(trickle, TRICKLE_MS);
|
|
10609
|
-
};
|
|
10610
|
-
/**
|
|
10611
|
-
* Show the bar after the start delay and begin trickling.
|
|
10612
|
-
*
|
|
10613
|
-
* @example
|
|
10614
|
-
* start();
|
|
10615
|
-
*/
|
|
10616
|
-
const start = () => {
|
|
10617
|
-
delayTimer = setTimeout(() => {
|
|
10618
|
-
width = 15;
|
|
10619
|
-
element.style.width = "15%";
|
|
10620
|
-
element.dataset.active = "";
|
|
10621
|
-
trickle();
|
|
10622
|
-
}, START_DELAY_MS);
|
|
10623
|
-
};
|
|
10624
|
-
/**
|
|
10625
|
-
* Complete the bar to 100%, then reset/hide it after a short linger.
|
|
10626
|
-
*
|
|
10627
|
-
* @example
|
|
10628
|
-
* done();
|
|
10629
|
-
*/
|
|
10630
|
-
const done = () => {
|
|
10631
|
-
clearTimeout(delayTimer);
|
|
10632
|
-
clearTimeout(trickleTimer);
|
|
10633
|
-
element.style.width = "100%";
|
|
10634
|
-
element.dataset.active = "";
|
|
10635
|
-
setTimeout(() => {
|
|
10636
|
-
delete element.dataset.active;
|
|
10637
|
-
element.style.width = "0%";
|
|
10638
|
-
width = 0;
|
|
10639
|
-
}, DONE_LINGER_MS);
|
|
10640
|
-
};
|
|
10641
|
-
return {
|
|
10642
|
-
start,
|
|
10643
|
-
done
|
|
10644
|
-
};
|
|
10645
|
-
}
|
|
10646
|
-
//#endregion
|
|
10647
10558
|
//#region src/plugins/spa/router.ts
|
|
10648
10559
|
/**
|
|
10649
10560
|
* Read the Navigation API global, or `undefined` when unsupported.
|
|
@@ -10671,9 +10582,10 @@ function isInternalLink(url) {
|
|
|
10671
10582
|
/**
|
|
10672
10583
|
* The navigable path of a URL or Location: pathname plus query string. The query
|
|
10673
10584
|
* is part of page identity (the kernel's `currentUrl` is pathname + search), so
|
|
10674
|
-
* same-page checks, history entries, fetches,
|
|
10675
|
-
* comparing pathnames alone would treat `/search?q=a` →
|
|
10676
|
-
* and the History fallback would drop the query from the
|
|
10585
|
+
* same-page checks, history entries, fetches, scroll keys and the preload cache key
|
|
10586
|
+
* must all carry it — comparing pathnames alone would treat `/search?q=a` →
|
|
10587
|
+
* `/search?q=b` as same-page and the History fallback would drop the query from the
|
|
10588
|
+
* address bar. The hash is not part of it.
|
|
10677
10589
|
*
|
|
10678
10590
|
* @param target - The URL or Location to read.
|
|
10679
10591
|
* @param target.pathname - The path component.
|
|
@@ -10711,26 +10623,42 @@ function restoreScrollPosition(path) {
|
|
|
10711
10623
|
} catch {}
|
|
10712
10624
|
}
|
|
10713
10625
|
/**
|
|
10714
|
-
* Fetch a page
|
|
10626
|
+
* Fetch a page's HTML. The one fetch-and-check shared by navigation and preload
|
|
10627
|
+
* (`prefetch.ts`): a response that is not ok is an error, never HTML to swap in.
|
|
10628
|
+
*
|
|
10629
|
+
* @param pathname - The page path (pathname + search).
|
|
10630
|
+
* @param signal - Aborts the request.
|
|
10631
|
+
* @returns The page HTML.
|
|
10632
|
+
* @throws {Error} When the response is not ok (`HTTP <status>`), or the fetch fails or aborts.
|
|
10633
|
+
* @example
|
|
10634
|
+
* await fetchPage("/about"); // "<!doctype html>…" — rejects with "HTTP 404" on a missing page
|
|
10635
|
+
*/
|
|
10636
|
+
async function fetchPage(pathname, signal) {
|
|
10637
|
+
const response = await (signal ? fetch(pathname, { signal }) : fetch(pathname));
|
|
10638
|
+
if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
|
|
10639
|
+
return response.text();
|
|
10640
|
+
}
|
|
10641
|
+
/**
|
|
10642
|
+
* Load a page and hand its HTML to the handlers; on any error fall back to a
|
|
10715
10643
|
* full browser navigation (`location.href = pathname`).
|
|
10716
10644
|
*
|
|
10717
|
-
* When `signal` aborts (this navigation was superseded by a newer one)
|
|
10718
|
-
*
|
|
10719
|
-
*
|
|
10645
|
+
* When `signal` aborts (this navigation was superseded by a newer one) NOTHING is
|
|
10646
|
+
* applied: no swap (onEnd) and no fallback reload — the live navigation owns the
|
|
10647
|
+
* document from that point on.
|
|
10720
10648
|
*
|
|
10721
10649
|
* @param pathname - The destination pathname.
|
|
10722
10650
|
* @param handlers - The navigation lifecycle callbacks.
|
|
10723
10651
|
* @param signal - Aborts when this navigation is superseded (`navEvent.signal`).
|
|
10652
|
+
* @param load - Where the HTML comes from: {@link fetchPage} by default; the kernel
|
|
10653
|
+
* passes a loader that serves a preloaded page first (`loadWithPrefetch`).
|
|
10724
10654
|
* @returns A promise that resolves once the swap (or fallback) is dispatched.
|
|
10725
10655
|
* @example
|
|
10726
10656
|
* await performNavigation("/about", handlers, navEvent.signal);
|
|
10727
10657
|
*/
|
|
10728
|
-
async function performNavigation(pathname, handlers, signal) {
|
|
10658
|
+
async function performNavigation(pathname, handlers, signal, load = fetchPage) {
|
|
10729
10659
|
handlers.onStart(pathname);
|
|
10730
10660
|
try {
|
|
10731
|
-
const
|
|
10732
|
-
if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
|
|
10733
|
-
const html = await response.text();
|
|
10661
|
+
const html = await load(pathname, signal);
|
|
10734
10662
|
if (signal?.aborted) return;
|
|
10735
10663
|
handlers.onEnd(html, pathname);
|
|
10736
10664
|
} catch {
|
|
@@ -10838,18 +10766,16 @@ function swapRegion(doc, swapSelector, transition, onSwapped, beforeCapture) {
|
|
|
10838
10766
|
return true;
|
|
10839
10767
|
}
|
|
10840
10768
|
/**
|
|
10841
|
-
* Resolve
|
|
10842
|
-
*
|
|
10769
|
+
* Resolve the internal URL a link leads to, or `undefined` when the SPA leaves it
|
|
10770
|
+
* to the browser (no anchor, `target="_blank"`, another origin, a static asset). The
|
|
10771
|
+
* one link rule shared by the click interceptor and the preloader (`prefetch.ts`).
|
|
10843
10772
|
*
|
|
10844
|
-
* @param
|
|
10845
|
-
* @returns The internal URL
|
|
10773
|
+
* @param anchor - The link element, or nothing.
|
|
10774
|
+
* @returns The internal URL, or `undefined`.
|
|
10846
10775
|
* @example
|
|
10847
|
-
*
|
|
10776
|
+
* resolveLinkTarget(document.querySelector("a[href='/about']"))?.pathname; // "/about"
|
|
10848
10777
|
*/
|
|
10849
|
-
function
|
|
10850
|
-
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return void 0;
|
|
10851
|
-
if (event.defaultPrevented) return void 0;
|
|
10852
|
-
const anchor = event.target?.closest("a");
|
|
10778
|
+
function resolveLinkTarget(anchor) {
|
|
10853
10779
|
if (!anchor || anchor.target === "_blank") return void 0;
|
|
10854
10780
|
let url;
|
|
10855
10781
|
try {
|
|
@@ -10860,6 +10786,20 @@ function resolveClickTarget(event) {
|
|
|
10860
10786
|
return isInternalLink(url) ? url : void 0;
|
|
10861
10787
|
}
|
|
10862
10788
|
/**
|
|
10789
|
+
* Resolve a navigable internal URL from a click event, or `undefined` when the
|
|
10790
|
+
* click should not be intercepted (modifier keys, non-anchor, external, new-tab).
|
|
10791
|
+
*
|
|
10792
|
+
* @param event - The click event to inspect.
|
|
10793
|
+
* @returns The internal URL to navigate to, or `undefined`.
|
|
10794
|
+
* @example
|
|
10795
|
+
* const url = resolveClickTarget(event);
|
|
10796
|
+
*/
|
|
10797
|
+
function resolveClickTarget(event) {
|
|
10798
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return void 0;
|
|
10799
|
+
if (event.defaultPrevented) return void 0;
|
|
10800
|
+
return resolveLinkTarget(event.target?.closest("a"));
|
|
10801
|
+
}
|
|
10802
|
+
/**
|
|
10863
10803
|
* Attach the History-API click/popstate interception path (used when the
|
|
10864
10804
|
* Navigation API is unavailable).
|
|
10865
10805
|
*
|
|
@@ -10992,11 +10932,412 @@ function attachRouter(handlers, navigate, isOwnPush) {
|
|
|
10992
10932
|
return navigation ? attachNavigationApi(navigation, handlers, navigate, isOwnPush) : attachHistoryFallback(handlers, navigate);
|
|
10993
10933
|
}
|
|
10994
10934
|
//#endregion
|
|
10935
|
+
//#region src/plugins/spa/prefetch.ts
|
|
10936
|
+
/**
|
|
10937
|
+
* @file spa plugin — opt-in in-memory HTML preload cache (`spa.prefetch`).
|
|
10938
|
+
* @see README.md
|
|
10939
|
+
*/
|
|
10940
|
+
const HOVER_DELAY_MS = 65;
|
|
10941
|
+
const TTL_MS = 3e4;
|
|
10942
|
+
const MAX_ENTRIES = 20;
|
|
10943
|
+
const VISIBLE_CONCURRENCY = 2;
|
|
10944
|
+
const IDLE_FALLBACK_MS = 200;
|
|
10945
|
+
const SLOW_CONNECTION = /(^|-)2g$/;
|
|
10946
|
+
const PRESS = {
|
|
10947
|
+
capture: true,
|
|
10948
|
+
passive: true
|
|
10949
|
+
};
|
|
10950
|
+
const HOVER = { passive: true };
|
|
10951
|
+
/**
|
|
10952
|
+
* Read `navigator.connection` (mirrors `getNavigation()` in `router.ts`).
|
|
10953
|
+
*
|
|
10954
|
+
* @returns The connection info, or `undefined` where the browser has none.
|
|
10955
|
+
* @example
|
|
10956
|
+
* getConnection()?.saveData; // true with data saving on
|
|
10957
|
+
*/
|
|
10958
|
+
function getConnection() {
|
|
10959
|
+
return navigator.connection;
|
|
10960
|
+
}
|
|
10961
|
+
/**
|
|
10962
|
+
* Whether no preload may start now: data saving on, or a 2G connection.
|
|
10963
|
+
*
|
|
10964
|
+
* @returns True when preloads must wait.
|
|
10965
|
+
* @example
|
|
10966
|
+
* isConstrained(); // true on effectiveType "slow-2g"
|
|
10967
|
+
*/
|
|
10968
|
+
function isConstrained() {
|
|
10969
|
+
const connection = getConnection();
|
|
10970
|
+
return connection?.saveData === true || SLOW_CONNECTION.test(connection?.effectiveType ?? "");
|
|
10971
|
+
}
|
|
10972
|
+
/**
|
|
10973
|
+
* The link an event happened on.
|
|
10974
|
+
*
|
|
10975
|
+
* @param event - A press or mouse event.
|
|
10976
|
+
* @returns The closest `<a>`, or nothing.
|
|
10977
|
+
* @example
|
|
10978
|
+
* anchorOf(event); // <a href="/about"> for a tap on the <span> inside it
|
|
10979
|
+
*/
|
|
10980
|
+
function anchorOf(event) {
|
|
10981
|
+
return event.target instanceof Element ? event.target.closest("a") : void 0;
|
|
10982
|
+
}
|
|
10983
|
+
/**
|
|
10984
|
+
* Run `task` in idle time, or after 200 ms where `requestIdleCallback` is missing.
|
|
10985
|
+
*
|
|
10986
|
+
* @param task - The work to run.
|
|
10987
|
+
* @returns Cancels the task.
|
|
10988
|
+
* @example
|
|
10989
|
+
* const cancel = whenIdle(drain); // cancel() before idle time: drain never runs
|
|
10990
|
+
*/
|
|
10991
|
+
function whenIdle(task) {
|
|
10992
|
+
if (typeof requestIdleCallback === "function") {
|
|
10993
|
+
const handle = requestIdleCallback(task);
|
|
10994
|
+
return () => cancelIdleCallback(handle);
|
|
10995
|
+
}
|
|
10996
|
+
const handle = setTimeout(task, IDLE_FALLBACK_MS);
|
|
10997
|
+
return () => clearTimeout(handle);
|
|
10998
|
+
}
|
|
10999
|
+
/**
|
|
11000
|
+
* Build the preloader: press and hover listeners on `document` at once, and for
|
|
11001
|
+
* `"visible"` one IntersectionObserver that {@link Prefetcher.observe} feeds.
|
|
11002
|
+
*
|
|
11003
|
+
* @param options - The mode, the HTML-path check and the clock.
|
|
11004
|
+
* @returns The live preloader.
|
|
11005
|
+
* @example
|
|
11006
|
+
* createPrefetcher({ mode: "intent", usesHtml: () => true }).take("/about"); // undefined
|
|
11007
|
+
*/
|
|
11008
|
+
function createPrefetcher(options) {
|
|
11009
|
+
const now = options.now ?? Date.now;
|
|
11010
|
+
const cache = /* @__PURE__ */ new Map();
|
|
11011
|
+
const hoverTimers = /* @__PURE__ */ new Map();
|
|
11012
|
+
const queue = [];
|
|
11013
|
+
let visibleInFlight = 0;
|
|
11014
|
+
let cancelDrain;
|
|
11015
|
+
/**
|
|
11016
|
+
* Whether an entry is older than the TTL.
|
|
11017
|
+
*
|
|
11018
|
+
* @param entry - A cache entry.
|
|
11019
|
+
* @returns True when the entry must not be used.
|
|
11020
|
+
* @example
|
|
11021
|
+
* isExpired(entry); // true 30_001 ms after it started
|
|
11022
|
+
*/
|
|
11023
|
+
const isExpired = (entry) => now() - entry.startedAt > TTL_MS;
|
|
11024
|
+
/**
|
|
11025
|
+
* Abort an entry's request and remove it.
|
|
11026
|
+
*
|
|
11027
|
+
* @param key - The entry's path.
|
|
11028
|
+
* @param entry - The entry.
|
|
11029
|
+
* @example
|
|
11030
|
+
* drop("/a/", entry);
|
|
11031
|
+
*/
|
|
11032
|
+
const drop = (key, entry) => {
|
|
11033
|
+
entry.controller.abort();
|
|
11034
|
+
cache.delete(key);
|
|
11035
|
+
};
|
|
11036
|
+
/**
|
|
11037
|
+
* The path a link would preload, or `undefined` when it is not eligible.
|
|
11038
|
+
*
|
|
11039
|
+
* @param anchor - The link, or nothing.
|
|
11040
|
+
* @returns The path (pathname + search), or `undefined`.
|
|
11041
|
+
* @example
|
|
11042
|
+
* pathOf(document.querySelector("a[href='/about?x=1#top']")); // "/about?x=1"
|
|
11043
|
+
*/
|
|
11044
|
+
const pathOf = (anchor) => {
|
|
11045
|
+
const url = resolveLinkTarget(anchor);
|
|
11046
|
+
if (!url || anchor?.hasAttribute("download")) return void 0;
|
|
11047
|
+
const path = pathWithSearch(url);
|
|
11048
|
+
return path !== pathWithSearch(location) && options.usesHtml(path) ? path : void 0;
|
|
11049
|
+
};
|
|
11050
|
+
/**
|
|
11051
|
+
* Start one preload, unless the connection forbids it or the path is already cached.
|
|
11052
|
+
*
|
|
11053
|
+
* @param path - The page path.
|
|
11054
|
+
* @returns The new entry, or `undefined` when nothing started.
|
|
11055
|
+
* @example
|
|
11056
|
+
* start("/about"); // { promise, startedAt, controller }
|
|
11057
|
+
*/
|
|
11058
|
+
const start = (path) => {
|
|
11059
|
+
if (isConstrained()) return void 0;
|
|
11060
|
+
for (const [key, entry] of cache) if (isExpired(entry)) drop(key, entry);
|
|
11061
|
+
if (cache.has(path)) return void 0;
|
|
11062
|
+
for (const [key, entry] of cache) {
|
|
11063
|
+
if (cache.size < MAX_ENTRIES) break;
|
|
11064
|
+
drop(key, entry);
|
|
11065
|
+
}
|
|
11066
|
+
const controller = new AbortController();
|
|
11067
|
+
const promise = fetchPage(path, controller.signal);
|
|
11068
|
+
const entry = {
|
|
11069
|
+
promise,
|
|
11070
|
+
startedAt: now(),
|
|
11071
|
+
controller
|
|
11072
|
+
};
|
|
11073
|
+
cache.set(path, entry);
|
|
11074
|
+
promise.catch(() => {
|
|
11075
|
+
if (cache.get(path) === entry) cache.delete(path);
|
|
11076
|
+
});
|
|
11077
|
+
return entry;
|
|
11078
|
+
};
|
|
11079
|
+
/**
|
|
11080
|
+
* Preload a link when it is eligible.
|
|
11081
|
+
*
|
|
11082
|
+
* @param anchor - The link, or nothing.
|
|
11083
|
+
* @example
|
|
11084
|
+
* preloadLink(document.querySelector("a"));
|
|
11085
|
+
*/
|
|
11086
|
+
const preloadLink = (anchor) => {
|
|
11087
|
+
const path = pathOf(anchor);
|
|
11088
|
+
if (path !== void 0) start(path);
|
|
11089
|
+
};
|
|
11090
|
+
/**
|
|
11091
|
+
* Start queued `"visible"` preloads while fewer than 2 are in flight.
|
|
11092
|
+
*
|
|
11093
|
+
* @example
|
|
11094
|
+
* cancelDrain = whenIdle(drain);
|
|
11095
|
+
*/
|
|
11096
|
+
const drain = () => {
|
|
11097
|
+
cancelDrain = void 0;
|
|
11098
|
+
while (visibleInFlight < VISIBLE_CONCURRENCY) {
|
|
11099
|
+
const path = queue.shift();
|
|
11100
|
+
if (path === void 0) return;
|
|
11101
|
+
const entry = start(path);
|
|
11102
|
+
if (!entry) continue;
|
|
11103
|
+
visibleInFlight += 1;
|
|
11104
|
+
entry.promise.catch(() => "").then(() => {
|
|
11105
|
+
visibleInFlight -= 1;
|
|
11106
|
+
scheduleDrain();
|
|
11107
|
+
});
|
|
11108
|
+
}
|
|
11109
|
+
};
|
|
11110
|
+
/**
|
|
11111
|
+
* Drain the queue in idle time, once.
|
|
11112
|
+
*
|
|
11113
|
+
* @example
|
|
11114
|
+
* scheduleDrain();
|
|
11115
|
+
*/
|
|
11116
|
+
const scheduleDrain = () => {
|
|
11117
|
+
if (cancelDrain === void 0 && queue.length > 0) cancelDrain = whenIdle(drain);
|
|
11118
|
+
};
|
|
11119
|
+
/**
|
|
11120
|
+
* `pointerdown` / `touchstart`: preload at once.
|
|
11121
|
+
*
|
|
11122
|
+
* @param event - The press.
|
|
11123
|
+
* @example
|
|
11124
|
+
* document.addEventListener("pointerdown", onPress, PRESS);
|
|
11125
|
+
*/
|
|
11126
|
+
const onPress = (event) => {
|
|
11127
|
+
preloadLink(anchorOf(event));
|
|
11128
|
+
};
|
|
11129
|
+
/**
|
|
11130
|
+
* `mouseover`: preload after 65 ms on the link.
|
|
11131
|
+
*
|
|
11132
|
+
* @param event - The mouse event.
|
|
11133
|
+
* @example
|
|
11134
|
+
* document.addEventListener("mouseover", onOver, HOVER);
|
|
11135
|
+
*/
|
|
11136
|
+
const onOver = (event) => {
|
|
11137
|
+
const anchor = anchorOf(event);
|
|
11138
|
+
if (!anchor || hoverTimers.has(anchor)) return;
|
|
11139
|
+
const timer = setTimeout(() => {
|
|
11140
|
+
hoverTimers.delete(anchor);
|
|
11141
|
+
preloadLink(anchor);
|
|
11142
|
+
}, HOVER_DELAY_MS);
|
|
11143
|
+
hoverTimers.set(anchor, timer);
|
|
11144
|
+
};
|
|
11145
|
+
/**
|
|
11146
|
+
* `mouseout`: cancel the hover timer, unless the mouse only moved inside the same link.
|
|
11147
|
+
*
|
|
11148
|
+
* @param event - The mouse event.
|
|
11149
|
+
* @example
|
|
11150
|
+
* document.addEventListener("mouseout", onOut, HOVER);
|
|
11151
|
+
*/
|
|
11152
|
+
const onOut = (event) => {
|
|
11153
|
+
const anchor = anchorOf(event);
|
|
11154
|
+
const into = event.relatedTarget;
|
|
11155
|
+
if (!anchor || into instanceof Node && anchor.contains(into)) return;
|
|
11156
|
+
clearTimeout(hoverTimers.get(anchor));
|
|
11157
|
+
hoverTimers.delete(anchor);
|
|
11158
|
+
};
|
|
11159
|
+
const observer = options.mode === "visible" && typeof IntersectionObserver === "function" ? new IntersectionObserver((entries, self) => {
|
|
11160
|
+
for (const entry of entries) {
|
|
11161
|
+
if (!entry.isIntersecting) continue;
|
|
11162
|
+
self.unobserve(entry.target);
|
|
11163
|
+
const path = pathOf(entry.target.closest("a"));
|
|
11164
|
+
if (path !== void 0) queue.push(path);
|
|
11165
|
+
}
|
|
11166
|
+
scheduleDrain();
|
|
11167
|
+
}) : void 0;
|
|
11168
|
+
document.addEventListener("pointerdown", onPress, PRESS);
|
|
11169
|
+
document.addEventListener("touchstart", onPress, PRESS);
|
|
11170
|
+
document.addEventListener("mouseover", onOver, HOVER);
|
|
11171
|
+
document.addEventListener("mouseout", onOut, HOVER);
|
|
11172
|
+
return {
|
|
11173
|
+
/**
|
|
11174
|
+
* Take the preload for `path` and remove it (see {@link Prefetcher.take}).
|
|
11175
|
+
*
|
|
11176
|
+
* @param path - The page path.
|
|
11177
|
+
* @returns The page HTML promise, or `undefined`.
|
|
11178
|
+
* @example
|
|
11179
|
+
* prefetcher.take("/about");
|
|
11180
|
+
*/
|
|
11181
|
+
take(path) {
|
|
11182
|
+
const entry = cache.get(path);
|
|
11183
|
+
if (!entry) return void 0;
|
|
11184
|
+
if (isExpired(entry)) {
|
|
11185
|
+
drop(path, entry);
|
|
11186
|
+
return;
|
|
11187
|
+
}
|
|
11188
|
+
cache.delete(path);
|
|
11189
|
+
return entry.promise;
|
|
11190
|
+
},
|
|
11191
|
+
/**
|
|
11192
|
+
* Watch every eligible link in the document (see {@link Prefetcher.observe}).
|
|
11193
|
+
*
|
|
11194
|
+
* @example
|
|
11195
|
+
* prefetcher.observe();
|
|
11196
|
+
*/
|
|
11197
|
+
observe() {
|
|
11198
|
+
if (!observer) return;
|
|
11199
|
+
observer.disconnect();
|
|
11200
|
+
queue.length = 0;
|
|
11201
|
+
for (const anchor of document.querySelectorAll("a[href]")) if (pathOf(anchor) !== void 0) observer.observe(anchor);
|
|
11202
|
+
},
|
|
11203
|
+
/**
|
|
11204
|
+
* Release everything (see {@link Prefetcher.dispose}).
|
|
11205
|
+
*
|
|
11206
|
+
* @example
|
|
11207
|
+
* prefetcher.dispose();
|
|
11208
|
+
*/
|
|
11209
|
+
dispose() {
|
|
11210
|
+
document.removeEventListener("pointerdown", onPress, PRESS);
|
|
11211
|
+
document.removeEventListener("touchstart", onPress, PRESS);
|
|
11212
|
+
document.removeEventListener("mouseover", onOver, HOVER);
|
|
11213
|
+
document.removeEventListener("mouseout", onOut, HOVER);
|
|
11214
|
+
observer?.disconnect();
|
|
11215
|
+
for (const timer of hoverTimers.values()) clearTimeout(timer);
|
|
11216
|
+
hoverTimers.clear();
|
|
11217
|
+
cancelDrain?.();
|
|
11218
|
+
cancelDrain = void 0;
|
|
11219
|
+
queue.length = 0;
|
|
11220
|
+
for (const [key, entry] of cache) drop(key, entry);
|
|
11221
|
+
}
|
|
11222
|
+
};
|
|
11223
|
+
}
|
|
11224
|
+
/**
|
|
11225
|
+
* The HTML loader the kernel hands to `performNavigation`: a preloaded page when there is
|
|
11226
|
+
* one, else a normal fetch. A failed preload falls through to one real fetch, so it never
|
|
11227
|
+
* leads to a full reload; only a failed real fetch does, as without preloading.
|
|
11228
|
+
*
|
|
11229
|
+
* @param prefetcher - The live preloader, or `null` (mode `false`, or stopped).
|
|
11230
|
+
* @param path - The page path (pathname + search).
|
|
11231
|
+
* @param signal - The navigation's abort signal; never passed to the preload.
|
|
11232
|
+
* @returns The page HTML.
|
|
11233
|
+
* @throws {Error} When the real fetch fails, or the navigation was aborted after its preload failed.
|
|
11234
|
+
* @example
|
|
11235
|
+
* await loadWithPrefetch(state.prefetch, "/about", signal); // the preloaded HTML, no new request
|
|
11236
|
+
*/
|
|
11237
|
+
async function loadWithPrefetch(prefetcher, path, signal) {
|
|
11238
|
+
const hit = prefetcher?.take(path);
|
|
11239
|
+
if (hit) try {
|
|
11240
|
+
return await hit;
|
|
11241
|
+
} catch {
|
|
11242
|
+
signal?.throwIfAborted();
|
|
11243
|
+
}
|
|
11244
|
+
return fetchPage(path, signal);
|
|
11245
|
+
}
|
|
11246
|
+
//#endregion
|
|
11247
|
+
//#region src/plugins/spa/progress.ts
|
|
11248
|
+
/** Delay before the bar appears, so fast navigations show no indicator. */
|
|
11249
|
+
const START_DELAY_MS = 150;
|
|
11250
|
+
/** Interval between trickle increments while loading. */
|
|
11251
|
+
const TRICKLE_MS = 300;
|
|
11252
|
+
/** Linger before the completed bar is reset/hidden. */
|
|
11253
|
+
const DONE_LINGER_MS = 200;
|
|
11254
|
+
/** Ceiling the bar trickles to while still loading (never reaches 100% until done). */
|
|
11255
|
+
const TRICKLE_CEIL = 90;
|
|
11256
|
+
/** No-op progress bar used when disabled or in a headless context. */
|
|
11257
|
+
const NOOP_BAR = {
|
|
11258
|
+
start() {},
|
|
11259
|
+
done() {}
|
|
11260
|
+
};
|
|
11261
|
+
/**
|
|
11262
|
+
* Creates the in-house progress bar (150ms delay + trickle). A no-op shell when
|
|
11263
|
+
* progress is disabled or no DOM is present. The progress element is created
|
|
11264
|
+
* once (prepended to `<body>` as `<div data-progress>`) and reused across navs.
|
|
11265
|
+
*
|
|
11266
|
+
* @param enabled - Whether the progress bar is active.
|
|
11267
|
+
* @returns A {@link ProgressBar} with `start`/`done`. Disabled/headless → no-ops.
|
|
11268
|
+
* @example
|
|
11269
|
+
* const bar = createProgressBar(true);
|
|
11270
|
+
* bar.start();
|
|
11271
|
+
* bar.done();
|
|
11272
|
+
*/
|
|
11273
|
+
function createProgressBar(enabled) {
|
|
11274
|
+
if (!enabled || typeof document === "undefined") return NOOP_BAR;
|
|
11275
|
+
const element = document.createElement("div");
|
|
11276
|
+
element.dataset.progress = "";
|
|
11277
|
+
document.body.prepend(element);
|
|
11278
|
+
let delayTimer;
|
|
11279
|
+
let trickleTimer;
|
|
11280
|
+
let width = 0;
|
|
11281
|
+
/**
|
|
11282
|
+
* Step the trickle upward toward the ceiling and reschedule.
|
|
11283
|
+
*
|
|
11284
|
+
* @example
|
|
11285
|
+
* trickle();
|
|
11286
|
+
*/
|
|
11287
|
+
const trickle = () => {
|
|
11288
|
+
if (width >= TRICKLE_CEIL) return;
|
|
11289
|
+
width = Math.min(TRICKLE_CEIL, width + 5 + Math.random() * 10);
|
|
11290
|
+
element.style.width = `${String(width)}%`;
|
|
11291
|
+
trickleTimer = setTimeout(trickle, TRICKLE_MS);
|
|
11292
|
+
};
|
|
11293
|
+
/**
|
|
11294
|
+
* Show the bar after the start delay and begin trickling.
|
|
11295
|
+
*
|
|
11296
|
+
* @example
|
|
11297
|
+
* start();
|
|
11298
|
+
*/
|
|
11299
|
+
const start = () => {
|
|
11300
|
+
delayTimer = setTimeout(() => {
|
|
11301
|
+
width = 15;
|
|
11302
|
+
element.style.width = "15%";
|
|
11303
|
+
element.dataset.active = "";
|
|
11304
|
+
trickle();
|
|
11305
|
+
}, START_DELAY_MS);
|
|
11306
|
+
};
|
|
11307
|
+
/**
|
|
11308
|
+
* Complete the bar to 100%, then reset/hide it after a short linger.
|
|
11309
|
+
*
|
|
11310
|
+
* @example
|
|
11311
|
+
* done();
|
|
11312
|
+
*/
|
|
11313
|
+
const done = () => {
|
|
11314
|
+
clearTimeout(delayTimer);
|
|
11315
|
+
clearTimeout(trickleTimer);
|
|
11316
|
+
element.style.width = "100%";
|
|
11317
|
+
element.dataset.active = "";
|
|
11318
|
+
setTimeout(() => {
|
|
11319
|
+
delete element.dataset.active;
|
|
11320
|
+
element.style.width = "0%";
|
|
11321
|
+
width = 0;
|
|
11322
|
+
}, DONE_LINGER_MS);
|
|
11323
|
+
};
|
|
11324
|
+
return {
|
|
11325
|
+
start,
|
|
11326
|
+
done
|
|
11327
|
+
};
|
|
11328
|
+
}
|
|
11329
|
+
//#endregion
|
|
10995
11330
|
//#region src/plugins/spa/state.ts
|
|
10996
11331
|
/** Error prefix for spa config-validation failures (spec/11 Part-3). */
|
|
10997
11332
|
const ERROR_PREFIX$1 = "[web]";
|
|
10998
11333
|
/** Last-resort `swapSelector` when neither config nor defaults supply one. */
|
|
10999
11334
|
const FALLBACK_SWAP_SELECTOR = "main > section";
|
|
11335
|
+
/** Every value `spa.prefetch` accepts. */
|
|
11336
|
+
const PREFETCH_MODES = new Set([
|
|
11337
|
+
false,
|
|
11338
|
+
"intent",
|
|
11339
|
+
"visible"
|
|
11340
|
+
]);
|
|
11000
11341
|
/**
|
|
11001
11342
|
* Resolve the `viewTransitions` config value into the default {@link TransitionMode}:
|
|
11002
11343
|
* `true` → `"crossfade"`, `false` → `"none"`, a named mode → itself.
|
|
@@ -11011,13 +11352,28 @@ function resolveDefaultTransition(raw) {
|
|
|
11011
11352
|
if (raw === false) return "none";
|
|
11012
11353
|
return raw;
|
|
11013
11354
|
}
|
|
11355
|
+
/**
|
|
11356
|
+
* Validate `spa.prefetch` (spec/11 Part-3 error). An untyped config can carry any value,
|
|
11357
|
+
* so a typo like `"hover"` fails at init instead of silently disabling the preload.
|
|
11358
|
+
*
|
|
11359
|
+
* @param mode - The raw `prefetch` config value (absent ⇒ `false`).
|
|
11360
|
+
* @returns The validated mode.
|
|
11361
|
+
* @throws {Error} When the value is not `false`, `"intent"` or `"visible"`.
|
|
11362
|
+
* @example
|
|
11363
|
+
* resolvePrefetchMode(undefined); // false
|
|
11364
|
+
*/
|
|
11365
|
+
function resolvePrefetchMode(mode = false) {
|
|
11366
|
+
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.`);
|
|
11367
|
+
return mode;
|
|
11368
|
+
}
|
|
11014
11369
|
/** Default SPA config (declared as a value — no inline assertion). */
|
|
11015
11370
|
const defaultSpaConfig = {
|
|
11016
11371
|
swapSelector: "main > section",
|
|
11017
11372
|
viewTransitions: false,
|
|
11018
11373
|
scrollRestoration: "top",
|
|
11019
11374
|
progressBar: true,
|
|
11020
|
-
islands: []
|
|
11375
|
+
islands: [],
|
|
11376
|
+
prefetch: false
|
|
11021
11377
|
};
|
|
11022
11378
|
/**
|
|
11023
11379
|
* Whether a selector is syntactically valid (parseable by the DOM). Falls back
|
|
@@ -11044,7 +11400,8 @@ function isValidSelector(selector) {
|
|
|
11044
11400
|
*
|
|
11045
11401
|
* @param config - The raw spa config to validate.
|
|
11046
11402
|
* @returns The fully-resolved config with defaults applied.
|
|
11047
|
-
* @throws {Error} When `swapSelector` is empty or not a valid CSS selector
|
|
11403
|
+
* @throws {Error} When `swapSelector` is empty or not a valid CSS selector, or `prefetch`
|
|
11404
|
+
* is not one of its three values.
|
|
11048
11405
|
* @example
|
|
11049
11406
|
* const resolved = resolveSpaConfig({ swapSelector: "main > section" });
|
|
11050
11407
|
*/
|
|
@@ -11059,7 +11416,8 @@ function resolveSpaConfig(config) {
|
|
|
11059
11416
|
defaultTransition,
|
|
11060
11417
|
scrollRestoration: config.scrollRestoration ?? "top",
|
|
11061
11418
|
progressBar: config.progressBar ?? true,
|
|
11062
|
-
islands: config.islands ?? []
|
|
11419
|
+
islands: config.islands ?? [],
|
|
11420
|
+
prefetch: resolvePrefetchMode(config.prefetch)
|
|
11063
11421
|
};
|
|
11064
11422
|
}
|
|
11065
11423
|
/**
|
|
@@ -11083,7 +11441,8 @@ function createState(_ctx) {
|
|
|
11083
11441
|
destroyRouter: null,
|
|
11084
11442
|
started: false,
|
|
11085
11443
|
navigate: null,
|
|
11086
|
-
kernel: null
|
|
11444
|
+
kernel: null,
|
|
11445
|
+
prefetch: null
|
|
11087
11446
|
};
|
|
11088
11447
|
}
|
|
11089
11448
|
//#endregion
|
|
@@ -11121,6 +11480,21 @@ function currentLocationUrl() {
|
|
|
11121
11480
|
return location.pathname + location.search;
|
|
11122
11481
|
}
|
|
11123
11482
|
/**
|
|
11483
|
+
* Whether a navigation to `path` renders from fetched HTML, so a preload of it is used.
|
|
11484
|
+
*
|
|
11485
|
+
* @param deps - The router and the optional data reader.
|
|
11486
|
+
* @param path - The page path (pathname + search).
|
|
11487
|
+
* @returns False when the client DATA path renders the page.
|
|
11488
|
+
* @example
|
|
11489
|
+
* usesHtml({ router, head }, "/en/about/"); // true: no data reader, every page is HTML
|
|
11490
|
+
*/
|
|
11491
|
+
function usesHtml(deps, path) {
|
|
11492
|
+
if (deps.router.mode() === "ssg") return true;
|
|
11493
|
+
const route = deps.router.match(path.split("?")[0] ?? path)?.route;
|
|
11494
|
+
if (!route?._handlers.render) return true;
|
|
11495
|
+
return !isClientOnlyRoute(deps.router.mode(), route) && !deps.dataAt;
|
|
11496
|
+
}
|
|
11497
|
+
/**
|
|
11124
11498
|
* Apply the matched route's `head` config to the live document (minimal client
|
|
11125
11499
|
* head-sync for the DATA path: title only — the full meta sync runs on the
|
|
11126
11500
|
* HTML-over-fetch path from the fetched `<head>`). The title is resolved through
|
|
@@ -11280,6 +11654,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11280
11654
|
const routeSlice = islandRouteContext(pathname);
|
|
11281
11655
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
11282
11656
|
notifyNavEnd(state, routeSlice);
|
|
11657
|
+
state.prefetch?.observe();
|
|
11283
11658
|
}, applyPendingScroll)) {
|
|
11284
11659
|
handleError();
|
|
11285
11660
|
location.href = pathname;
|
|
@@ -11394,6 +11769,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11394
11769
|
renderVNode(vnode, region);
|
|
11395
11770
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
11396
11771
|
notifyNavEnd(state, routeSlice);
|
|
11772
|
+
state.prefetch?.observe();
|
|
11397
11773
|
};
|
|
11398
11774
|
runSwap(renderAndMount, pendingTransition, applyPendingScroll);
|
|
11399
11775
|
state.currentUrl = pathname;
|
|
@@ -11442,12 +11818,14 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11442
11818
|
const resolvedRender = await resolveDataRender(pathname);
|
|
11443
11819
|
if (resolvedRender === false) {
|
|
11444
11820
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
11821
|
+
state.prefetch?.observe();
|
|
11445
11822
|
return;
|
|
11446
11823
|
}
|
|
11447
11824
|
const { vnode, region } = resolvedRender;
|
|
11448
11825
|
const { renderVNode } = await Promise.resolve().then(() => require("./render-rf8FAg_Z.cjs"));
|
|
11449
11826
|
renderVNode(vnode, region);
|
|
11450
11827
|
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
11828
|
+
state.prefetch?.observe();
|
|
11451
11829
|
};
|
|
11452
11830
|
/**
|
|
11453
11831
|
* Unified navigation: try the client DATA path first (only when the `data`
|
|
@@ -11471,7 +11849,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11471
11849
|
handleStart(pathname);
|
|
11472
11850
|
if (deps.router.mode() !== "ssg" && await tryDataRender(pathname, signal)) return;
|
|
11473
11851
|
if (signal?.aborted) return;
|
|
11474
|
-
await performNavigation(pathname, handlers, signal);
|
|
11852
|
+
await performNavigation(pathname, handlers, signal, (path, loadSignal) => loadWithPrefetch(state.prefetch, path, loadSignal));
|
|
11475
11853
|
};
|
|
11476
11854
|
/**
|
|
11477
11855
|
* Programmatic navigation entry (`app.spa.navigate` / `ctx.navigate`). A link click or the
|
|
@@ -11539,10 +11917,17 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11539
11917
|
supersedeProgrammatic();
|
|
11540
11918
|
return navigate(pathname, scrollToTop, signal, scrollOverride);
|
|
11541
11919
|
}, () => pushingOwnUrl);
|
|
11920
|
+
if (resolved.prefetch !== false) state.prefetch = createPrefetcher({
|
|
11921
|
+
mode: resolved.prefetch,
|
|
11922
|
+
usesHtml: (path) => usesHtml(deps, path)
|
|
11923
|
+
});
|
|
11542
11924
|
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
11543
11925
|
const hit = deps.router.match(matchPath);
|
|
11544
11926
|
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
|
11545
|
-
else
|
|
11927
|
+
else {
|
|
11928
|
+
scanAndMount(state, emit, resolved.swapSelector, islandRouteContext(state.currentUrl));
|
|
11929
|
+
state.prefetch?.observe();
|
|
11930
|
+
}
|
|
11546
11931
|
state.started = true;
|
|
11547
11932
|
},
|
|
11548
11933
|
/**
|
|
@@ -11582,6 +11967,8 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11582
11967
|
if (typeof document === "undefined") return;
|
|
11583
11968
|
state.destroyRouter?.();
|
|
11584
11969
|
state.destroyRouter = null;
|
|
11970
|
+
state.prefetch?.dispose();
|
|
11971
|
+
state.prefetch = null;
|
|
11585
11972
|
location.assign(url);
|
|
11586
11973
|
},
|
|
11587
11974
|
/**
|
|
@@ -11602,6 +11989,8 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
11602
11989
|
dispose() {
|
|
11603
11990
|
state.destroyRouter?.();
|
|
11604
11991
|
state.destroyRouter = null;
|
|
11992
|
+
state.prefetch?.dispose();
|
|
11993
|
+
state.prefetch = null;
|
|
11605
11994
|
unmountAll(state, emit);
|
|
11606
11995
|
progress = void 0;
|
|
11607
11996
|
state.started = false;
|