@ilha/router 0.11.9 → 0.11.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +30 -1
- package/dist/index.js +111 -45
- package/dist/{plugin-Cn_H92L0.js → plugin-FVaWBiNQ.js} +6 -3
- package/dist/rsbuild.js +1 -1
- package/dist/server-island.js +13 -15
- package/dist/vite.js +1 -1
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -90,7 +90,9 @@ export interface RenderPageOptions {
|
|
|
90
90
|
markers?: boolean;
|
|
91
91
|
}
|
|
92
92
|
export interface RouterBuilder {
|
|
93
|
-
route: (pattern: string, page: Page
|
|
93
|
+
route: (pattern: string, page: Page, opts?: {
|
|
94
|
+
layouts?: LayoutHandler[];
|
|
95
|
+
}) => RouterBuilder;
|
|
94
96
|
errorBoundary: (pattern: string, handler: ErrorHandler) => RouterBuilder;
|
|
95
97
|
routes: () => RouteRecord[];
|
|
96
98
|
prime: () => void;
|
|
@@ -134,6 +136,33 @@ export declare const prime: () => void;
|
|
|
134
136
|
export declare const beforeNavigate: (fn: BeforeNavigateHook) => (() => void);
|
|
135
137
|
export declare const afterNavigate: (fn: AfterNavigateHook) => (() => void);
|
|
136
138
|
export declare const navigate: (to: string, opts?: NavigateOptions) => void;
|
|
139
|
+
export interface SearchParamOptions<T> {
|
|
140
|
+
/** Value when the param is absent. Writing it removes the param. */
|
|
141
|
+
default: T;
|
|
142
|
+
/** Decode the raw param. Default: the raw string. */
|
|
143
|
+
parse?: (raw: string) => T;
|
|
144
|
+
/** Encode a value for the URL. Default: `String(value)`. */
|
|
145
|
+
serialize?: (value: T) => string;
|
|
146
|
+
}
|
|
147
|
+
export interface SearchParam<T> {
|
|
148
|
+
(): T;
|
|
149
|
+
set: (next: T) => void;
|
|
150
|
+
update: (f: (current: T) => T) => void;
|
|
151
|
+
}
|
|
152
|
+
export interface SearchParamFn {
|
|
153
|
+
/** String param: `parse` is optional. */
|
|
154
|
+
(name: string, opts: SearchParamOptions<string>): SearchParam<string>;
|
|
155
|
+
/** Non-string param: `parse` is required to decode the raw string. */
|
|
156
|
+
<T>(name: string, opts: SearchParamOptions<T> & {
|
|
157
|
+
parse: (raw: string) => T;
|
|
158
|
+
}): SearchParam<T>;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Read and write one query param through the router. Writes `replace` the
|
|
162
|
+
* history entry without scrolling, so the mounted router re-renders the
|
|
163
|
+
* current route in place instead of remounting it.
|
|
164
|
+
*/
|
|
165
|
+
export declare const searchParam: SearchParamFn;
|
|
137
166
|
export type LinkInterceptionOptions = Record<string, never>;
|
|
138
167
|
export declare const enableLinkInterception: (root?: Element | Document, _options?: LinkInterceptionOptions) => (() => void);
|
|
139
168
|
export interface IsActiveOptions {
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as REQUEST_ALS_KEY } from "./als-key-CVxbuM3z.js";
|
|
2
2
|
import { a as withHeadStore, i as serializeHead, n as head, r as openBrowserHead, t as applyHeadEntriesToDocument } from "./head-WG_AtbdX.js";
|
|
3
|
-
import { h, mount, renderToString } from "ilha";
|
|
3
|
+
import { ErrorBoundary, atom, h, mount, renderToString } from "ilha";
|
|
4
4
|
|
|
5
5
|
//#region src/hash.ts
|
|
6
6
|
const isBrowser$1 = globalThis.window !== void 0 && globalThis.document !== void 0;
|
|
@@ -448,6 +448,36 @@ const navigate = (to, opts = {}) => {
|
|
|
448
448
|
type
|
|
449
449
|
});
|
|
450
450
|
};
|
|
451
|
+
/**
|
|
452
|
+
* Read and write one query param through the router. Writes `replace` the
|
|
453
|
+
* history entry without scrolling, so the mounted router re-renders the
|
|
454
|
+
* current route in place instead of remounting it.
|
|
455
|
+
*/
|
|
456
|
+
const searchParam = (name, opts) => {
|
|
457
|
+
const serialize = opts.serialize ?? String;
|
|
458
|
+
const read = () => {
|
|
459
|
+
const raw = new URLSearchParams(_search).get(name);
|
|
460
|
+
if (raw === null) return opts.default;
|
|
461
|
+
return opts.parse ? opts.parse(raw) : raw;
|
|
462
|
+
};
|
|
463
|
+
const set = (next) => {
|
|
464
|
+
const params = new URLSearchParams(_search);
|
|
465
|
+
const encoded = serialize(next);
|
|
466
|
+
if (encoded === serialize(opts.default)) params.delete(name);
|
|
467
|
+
else params.set(name, encoded);
|
|
468
|
+
const qs = params.toString();
|
|
469
|
+
navigate(`${_path}${qs ? `?${qs}` : ""}${_hash}`, {
|
|
470
|
+
replace: true,
|
|
471
|
+
scroll: false
|
|
472
|
+
});
|
|
473
|
+
};
|
|
474
|
+
return Object.assign(read, {
|
|
475
|
+
set,
|
|
476
|
+
update: (f) => {
|
|
477
|
+
set(f(read()));
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
};
|
|
451
481
|
const NON_NAV_SCHEME = /^(?:mailto|tel|javascript):/iu;
|
|
452
482
|
const resolveClickTarget = (_href, url) => {
|
|
453
483
|
if (getHistoryMode() === "hash") {
|
|
@@ -456,31 +486,32 @@ const resolveClickTarget = (_href, url) => {
|
|
|
456
486
|
}
|
|
457
487
|
return url.pathname + url.search + url.hash;
|
|
458
488
|
};
|
|
489
|
+
const onLinkClick = (event) => {
|
|
490
|
+
const mouseEvent = event;
|
|
491
|
+
if (mouseEvent.defaultPrevented || mouseEvent.button !== 0 || mouseEvent.metaKey || mouseEvent.ctrlKey || mouseEvent.shiftKey || mouseEvent.altKey) return;
|
|
492
|
+
const { target } = mouseEvent;
|
|
493
|
+
if (!(target instanceof Element)) return;
|
|
494
|
+
const anchor = target.closest("a");
|
|
495
|
+
if (!anchor || anchor.hasAttribute("download") || anchor.getAttribute("target") === "_blank" || anchor.matches("[data-no-intercept]")) return;
|
|
496
|
+
const href = anchor.getAttribute("href");
|
|
497
|
+
if (!href || NON_NAV_SCHEME.test(href)) return;
|
|
498
|
+
if (href.startsWith("#") && getHistoryMode() !== "hash") return;
|
|
499
|
+
if (href.startsWith("#") && !href.startsWith("#/")) return;
|
|
500
|
+
let url;
|
|
501
|
+
try {
|
|
502
|
+
url = new URL(href, location.href);
|
|
503
|
+
} catch {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (url.origin !== location.origin) return;
|
|
507
|
+
mouseEvent.preventDefault();
|
|
508
|
+
navigate(resolveClickTarget(href, url));
|
|
509
|
+
};
|
|
459
510
|
const enableLinkInterception = (root = document, _options = {}) => {
|
|
460
511
|
if (!isBrowser) return noop;
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const { target } = mouseEvent;
|
|
465
|
-
if (!(target instanceof Element)) return;
|
|
466
|
-
const anchor = target.closest("a");
|
|
467
|
-
if (!anchor || anchor.hasAttribute("download") || anchor.getAttribute("target") === "_blank" || anchor.matches("[data-no-intercept]")) return;
|
|
468
|
-
const href = anchor.getAttribute("href");
|
|
469
|
-
if (!href || NON_NAV_SCHEME.test(href)) return;
|
|
470
|
-
if (href.startsWith("#") && getHistoryMode() !== "hash") return;
|
|
471
|
-
if (href.startsWith("#") && !href.startsWith("#/")) return;
|
|
472
|
-
let url;
|
|
473
|
-
try {
|
|
474
|
-
url = new URL(href, location.href);
|
|
475
|
-
} catch {
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
if (url.origin !== location.origin) return;
|
|
479
|
-
mouseEvent.preventDefault();
|
|
480
|
-
navigate(resolveClickTarget(href, url));
|
|
481
|
-
};
|
|
482
|
-
root.addEventListener("click", clickHandler);
|
|
483
|
-
return () => root.removeEventListener("click", clickHandler);
|
|
512
|
+
const listener = { handleEvent: onLinkClick };
|
|
513
|
+
root.addEventListener("click", listener);
|
|
514
|
+
return () => root.removeEventListener("click", listener);
|
|
484
515
|
};
|
|
485
516
|
const isActive = (pattern, options = {}) => {
|
|
486
517
|
if (options.end) return _path === pattern;
|
|
@@ -516,10 +547,16 @@ const resolveRequestUrl = (urlOrRequest) => {
|
|
|
516
547
|
return new URL("http://localhost/");
|
|
517
548
|
}
|
|
518
549
|
};
|
|
519
|
-
const
|
|
550
|
+
const composedPageForPath = (pathname, routes, notFound) => {
|
|
520
551
|
const match = matchRoute(routes, pathname);
|
|
521
|
-
if (match) return
|
|
522
|
-
|
|
552
|
+
if (!match) return notFound;
|
|
553
|
+
const { layouts } = match.data;
|
|
554
|
+
let { page } = match.data;
|
|
555
|
+
for (let j = layouts.length - 1; j >= 0; j -= 1) {
|
|
556
|
+
const layout = layouts[j];
|
|
557
|
+
if (layout) page = wrapLayout(layout, page);
|
|
558
|
+
}
|
|
559
|
+
return page;
|
|
523
560
|
};
|
|
524
561
|
const renderPage = (page, opts) => renderToString(page, opts);
|
|
525
562
|
const buildRespondBody = (result, respondOpts, head) => {
|
|
@@ -577,25 +614,51 @@ const router = (options = {}) => {
|
|
|
577
614
|
let mounted = true;
|
|
578
615
|
const prevScroll = "scrollRestoration" in history ? history.scrollRestoration : null;
|
|
579
616
|
if (prevScroll !== null) history.scrollRestoration = "manual";
|
|
580
|
-
let
|
|
617
|
+
let unmountShell = null;
|
|
581
618
|
let headSession = null;
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
619
|
+
let bumpShell = null;
|
|
620
|
+
const Shell = () => {
|
|
621
|
+
const tick = atom(0);
|
|
622
|
+
bumpShell = () => {
|
|
623
|
+
tick.update((t) => t + 1);
|
|
624
|
+
};
|
|
625
|
+
tick();
|
|
626
|
+
const match = matchRoute(routes, _path);
|
|
627
|
+
if (!match) return notFound ? h(notFound, null) : "";
|
|
628
|
+
const { errorHandler, layouts, page } = match.data;
|
|
629
|
+
let view = h(page, null);
|
|
630
|
+
for (let j = layouts.length - 1; j >= 0; j -= 1) {
|
|
631
|
+
const layout = layouts[j];
|
|
632
|
+
if (layout) view = h(layout, null, view);
|
|
633
|
+
}
|
|
634
|
+
if (errorHandler) {
|
|
635
|
+
const onBoundary = errorHandler;
|
|
636
|
+
view = h(ErrorBoundary, { fallback: ({ error }) => onBoundary({ message: error.message }, snapshot()) }, view);
|
|
591
637
|
}
|
|
638
|
+
return view;
|
|
639
|
+
};
|
|
640
|
+
const mountShell = () => {
|
|
641
|
+
headSession?.close();
|
|
592
642
|
const session = openBrowserHead();
|
|
593
643
|
headSession = session;
|
|
594
|
-
|
|
644
|
+
unmountShell = mount(host, Shell, { hydrate });
|
|
595
645
|
queueMicrotask(() => {
|
|
596
646
|
session.flush();
|
|
597
647
|
});
|
|
598
648
|
};
|
|
649
|
+
const refresh = () => {
|
|
650
|
+
if (bumpShell) {
|
|
651
|
+
headSession?.close();
|
|
652
|
+
const session = openBrowserHead();
|
|
653
|
+
headSession = session;
|
|
654
|
+
bumpShell();
|
|
655
|
+
queueMicrotask(() => {
|
|
656
|
+
session.flush();
|
|
657
|
+
});
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
mountShell();
|
|
661
|
+
};
|
|
599
662
|
const popHandler = () => {
|
|
600
663
|
if (!mounted) return;
|
|
601
664
|
const prevPath = _path + _search + _hash;
|
|
@@ -611,18 +674,20 @@ const router = (options = {}) => {
|
|
|
611
674
|
to: _path + _search + _hash,
|
|
612
675
|
type: "pop"
|
|
613
676
|
});
|
|
614
|
-
|
|
677
|
+
refresh();
|
|
615
678
|
};
|
|
616
679
|
navChangeCleanup = getAdapter().onChange(popHandler);
|
|
617
680
|
linkCleanup = mountInterceptLinks ?? defaultInterceptLinks ? enableLinkInterception(document) : null;
|
|
618
|
-
|
|
681
|
+
mountShell();
|
|
619
682
|
const offNav = afterNavigate(() => {
|
|
620
|
-
if (mounted)
|
|
683
|
+
if (mounted) refresh();
|
|
621
684
|
});
|
|
622
685
|
return () => {
|
|
623
686
|
mounted = false;
|
|
624
687
|
offNav();
|
|
625
|
-
|
|
688
|
+
bumpShell = null;
|
|
689
|
+
unmountShell?.();
|
|
690
|
+
unmountShell = null;
|
|
626
691
|
headSession?.close();
|
|
627
692
|
headSession = null;
|
|
628
693
|
navChangeCleanup?.();
|
|
@@ -642,7 +707,7 @@ const router = (options = {}) => {
|
|
|
642
707
|
syncFromParts(parsed.pathname, parsed.search, parsed.hash, routes);
|
|
643
708
|
const store = { entries: [] };
|
|
644
709
|
try {
|
|
645
|
-
const page =
|
|
710
|
+
const page = composedPageForPath(parsed.pathname, routes, notFound);
|
|
646
711
|
if (!page) throw new RouteError(404, "Not found");
|
|
647
712
|
let captured;
|
|
648
713
|
const wrapped = async () => {
|
|
@@ -711,8 +776,9 @@ const router = (options = {}) => {
|
|
|
711
776
|
status: result.kind === "error" ? result.status : result.status ?? 200
|
|
712
777
|
});
|
|
713
778
|
},
|
|
714
|
-
route(pattern, page) {
|
|
779
|
+
route(pattern, page, opts = {}) {
|
|
715
780
|
const data = {
|
|
781
|
+
layouts: opts.layouts ?? [],
|
|
716
782
|
page,
|
|
717
783
|
pattern
|
|
718
784
|
};
|
|
@@ -732,4 +798,4 @@ const router = (options = {}) => {
|
|
|
732
798
|
};
|
|
733
799
|
|
|
734
800
|
//#endregion
|
|
735
|
-
export { EMPTY_HEAD, Redirect, RouteError, afterNavigate, beforeNavigate, defineLayout, enableLinkInterception, throwAppError as error, getHistoryMode, head, httpResponse, isActive, navigate, navigating, prime, redirect, resolveRedirectTarget, routeHash, routeParams, routePath, routeSearch, router, serializeHead, setHistoryMode, useContext, useRoute, wrapError, wrapLayout };
|
|
801
|
+
export { EMPTY_HEAD, Redirect, RouteError, afterNavigate, beforeNavigate, defineLayout, enableLinkInterception, throwAppError as error, getHistoryMode, head, httpResponse, isActive, navigate, navigating, prime, redirect, resolveRedirectTarget, routeHash, routeParams, routePath, routeSearch, router, searchParam, serializeHead, setHistoryMode, useContext, useRoute, wrapError, wrapLayout };
|
|
@@ -465,10 +465,13 @@ const pushLayoutErrorImports = (imports, entry, index, importPath) => {
|
|
|
465
465
|
for (const [j, l] of entry.layouts.entries()) imports.push(`import { default as _layout${index}_${j} } from ${JSON.stringify(importPath(l))};`);
|
|
466
466
|
for (const [j, e] of entry.errors.entries()) imports.push(`import { default as _error${index}_${j} } from ${JSON.stringify(importPath(e))};`);
|
|
467
467
|
};
|
|
468
|
-
const pushRegistryAndRoutes = (entry, index, entriesLength, wrappedId, registryLines, routeLines, includeRoutes) => {
|
|
468
|
+
const pushRegistryAndRoutes = (entry, index, entriesLength, wrappedId, registryLines, routeLines, includeRoutes, decomposeLayouts = false) => {
|
|
469
469
|
registryLines.push(` ${JSON.stringify(entry.name)}: ${wrappedId}${index < entriesLength - 1 ? "," : ""}`);
|
|
470
470
|
if (!includeRoutes) return;
|
|
471
|
-
|
|
471
|
+
if (decomposeLayouts && entry.layouts.length > 0) {
|
|
472
|
+
const layouts = entry.layouts.map((_, j) => `_layout${index}_${j}`).join(", ");
|
|
473
|
+
routeLines.push(` .route(${JSON.stringify(entry.pattern)}, _page${index}, { layouts: [${layouts}] })`);
|
|
474
|
+
} else routeLines.push(` .route(${JSON.stringify(entry.pattern)}, ${wrappedId})`);
|
|
472
475
|
if (entry.errors.length > 0) routeLines.push(` .errorBoundary(${JSON.stringify(entry.pattern)}, _error${index}_${entry.errors.length - 1})`);
|
|
473
476
|
};
|
|
474
477
|
const buildServerFile = (entries, serverFile) => {
|
|
@@ -510,7 +513,7 @@ const emitClientEntry = (entry, index, entriesLength, isStatic, clientImport, im
|
|
|
510
513
|
const expr = wrapIslandExpr(`_page${index}`, entry.layouts.length, entry.errors.length, index);
|
|
511
514
|
const wrappedId = `_wrapped${index}`;
|
|
512
515
|
wrappedIslandLines.push(`const ${wrappedId} = ${expr};`);
|
|
513
|
-
pushRegistryAndRoutes(entry, index, entriesLength, wrappedId, registryLines, routeLines, !isStatic);
|
|
516
|
+
pushRegistryAndRoutes(entry, index, entriesLength, wrappedId, registryLines, routeLines, !isStatic, true);
|
|
514
517
|
};
|
|
515
518
|
const buildClientFile = (entries, clientFile, opts) => {
|
|
516
519
|
const { isStatic, interceptLinks } = opts;
|
package/dist/rsbuild.js
CHANGED
package/dist/server-island.js
CHANGED
|
@@ -178,15 +178,15 @@ const hydrateServerIsland = (host, id, wiring, props, moduleKey) => {
|
|
|
178
178
|
const actionFn = actionKey === null || actionKey === void 0 ? void 0 : wiring.actions?.[actionKey];
|
|
179
179
|
if (!actionFn) continue;
|
|
180
180
|
const boundKey = actionKey;
|
|
181
|
+
const run = async () => {
|
|
182
|
+
try {
|
|
183
|
+
await Promise.resolve(actionFn(...callArgs));
|
|
184
|
+
hooks.repaintModule();
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error(`[ilha-router] action "${String(boundKey)}" failed:`, error);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
181
189
|
const listener = () => {
|
|
182
|
-
const run = async () => {
|
|
183
|
-
try {
|
|
184
|
-
await Promise.resolve(actionFn(...callArgs));
|
|
185
|
-
hooks.repaintModule();
|
|
186
|
-
} catch (error) {
|
|
187
|
-
console.error(`[ilha-router] action "${String(boundKey)}" failed:`, error);
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
190
|
run();
|
|
191
191
|
};
|
|
192
192
|
el.addEventListener(eventType, listener);
|
|
@@ -209,14 +209,12 @@ const hydrateServerIsland = (host, id, wiring, props, moduleKey) => {
|
|
|
209
209
|
if (!actionFn) continue;
|
|
210
210
|
let args = [];
|
|
211
211
|
if (Array.isArray(marker.a)) args = marker.a;
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return result;
|
|
217
|
-
};
|
|
218
|
-
return run();
|
|
212
|
+
const run = async () => {
|
|
213
|
+
const result = await Promise.resolve(actionFn(...args));
|
|
214
|
+
hooks.repaintModule();
|
|
215
|
+
return result;
|
|
219
216
|
};
|
|
217
|
+
const revived = (..._runtimeArgs) => run();
|
|
220
218
|
Object.assign(next, { [key]: revived });
|
|
221
219
|
}
|
|
222
220
|
return next;
|
package/dist/vite.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ilha/router",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.11",
|
|
4
4
|
"description": "A tiny SPA router for Ilha",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontend",
|
|
@@ -69,17 +69,17 @@
|
|
|
69
69
|
"test": "bun test"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"effect": "4.0.0-rc.
|
|
72
|
+
"effect": "4.0.0-rc.118",
|
|
73
73
|
"unplugin": "3.3.0"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"ilha": "^0.14.
|
|
77
|
-
"oxidejs": "^0.
|
|
78
|
-
"vite": "^8.
|
|
76
|
+
"ilha": "^0.14.8",
|
|
77
|
+
"oxidejs": "^0.5.7",
|
|
78
|
+
"vite": "^8.3.1"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
|
-
"ilha": ">=0.14.
|
|
82
|
-
"oxidejs": ">=0.
|
|
81
|
+
"ilha": ">=0.14.8",
|
|
82
|
+
"oxidejs": ">=0.5.7"
|
|
83
83
|
},
|
|
84
84
|
"peerDependenciesMeta": {
|
|
85
85
|
"oxidejs": {
|