@mindees/router 0.2.0 → 0.4.0
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/active.js +9 -1
- package/dist/active.js.map +1 -1
- package/dist/components.d.ts +11 -1
- package/dist/components.d.ts.map +1 -1
- package/dist/components.js +12 -0
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +2 -3
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +41 -7
- package/dist/router.js.map +1 -1
- package/package.json +3 -3
package/dist/active.js
CHANGED
|
@@ -8,7 +8,15 @@ function setActiveRouter(router) {
|
|
|
8
8
|
function getActiveRouter() {
|
|
9
9
|
return active;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Clear the active router IF it is `router` (called by `dispose()`), so a disposed router
|
|
13
|
+
* no longer leaks through `useRouter()`/`<Link>`. Guarded by identity so disposing an old
|
|
14
|
+
* router doesn't clobber a newer active one.
|
|
15
|
+
*/
|
|
16
|
+
function clearActiveRouter(router) {
|
|
17
|
+
if (active === router) active = null;
|
|
18
|
+
}
|
|
11
19
|
//#endregion
|
|
12
|
-
export { getActiveRouter, setActiveRouter };
|
|
20
|
+
export { clearActiveRouter, getActiveRouter, setActiveRouter };
|
|
13
21
|
|
|
14
22
|
//# sourceMappingURL=active.js.map
|
package/dist/active.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"active.js","names":[],"sources":["../src/active.ts"],"sourcesContent":["/**\n * The active-router registry. Hooks (`useRouter`, `useParams`, …) and the bound\n * `Link` need the current router without prop-drilling — Expo Router solves this\n * with a single global root router, and so do we: {@link createRouter} registers\n * itself as active, and the hooks read it here.\n *\n * Apps almost always have one root router; if you create several, the most recently\n * created is \"active\" (pass the router explicitly via `createLink(router)` /\n * `RouteComponentProps.router` when you need a specific one).\n *\n * @module\n */\n\nimport type { Router } from './router'\n\nlet active: Router | null = null\n\n/** Register `router` as the active router (called by {@link createRouter}). */\nexport function setActiveRouter(router: Router): void {\n active = router\n}\n\n/** The active router, or `null` if none has been created yet. */\nexport function getActiveRouter(): Router | null {\n return active\n}\n"],"mappings":";AAeA,IAAI,SAAwB;;AAG5B,SAAgB,gBAAgB,QAAsB;CACpD,SAAS;AACX;;AAGA,SAAgB,kBAAiC;CAC/C,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"active.js","names":[],"sources":["../src/active.ts"],"sourcesContent":["/**\n * The active-router registry. Hooks (`useRouter`, `useParams`, …) and the bound\n * `Link` need the current router without prop-drilling — Expo Router solves this\n * with a single global root router, and so do we: {@link createRouter} registers\n * itself as active, and the hooks read it here.\n *\n * Apps almost always have one root router; if you create several, the most recently\n * created is \"active\" (pass the router explicitly via `createLink(router)` /\n * `RouteComponentProps.router` when you need a specific one).\n *\n * @module\n */\n\nimport type { Router } from './router'\n\nlet active: Router | null = null\n\n/** Register `router` as the active router (called by {@link createRouter}). */\nexport function setActiveRouter(router: Router): void {\n active = router\n}\n\n/** The active router, or `null` if none has been created yet. */\nexport function getActiveRouter(): Router | null {\n return active\n}\n\n/**\n * Clear the active router IF it is `router` (called by `dispose()`), so a disposed router\n * no longer leaks through `useRouter()`/`<Link>`. Guarded by identity so disposing an old\n * router doesn't clobber a newer active one.\n */\nexport function clearActiveRouter(router: Router): void {\n if (active === router) active = null\n}\n"],"mappings":";AAeA,IAAI,SAAwB;;AAG5B,SAAgB,gBAAgB,QAAsB;CACpD,SAAS;AACX;;AAGA,SAAgB,kBAAiC;CAC/C,OAAO;AACT;;;;;;AAOA,SAAgB,kBAAkB,QAAsB;CACtD,IAAI,WAAW,QAAQ,SAAS;AAClC"}
|
package/dist/components.d.ts
CHANGED
|
@@ -18,6 +18,14 @@ interface RouterViewOptions {
|
|
|
18
18
|
* render(view, backend, root)
|
|
19
19
|
*/
|
|
20
20
|
declare function createRouterView(router: Router, options?: RouterViewOptions): MindeesNode;
|
|
21
|
+
/**
|
|
22
|
+
* When the link prefetches its target's loaders (warming the SWR cache so the destination
|
|
23
|
+
* resolves instantly). `'intent'` (default) prefetches on hover / press-in / focus — the
|
|
24
|
+
* Quantum differentiator vs Expo Router's manual-only `router.prefetch`. `'render'` prefetches
|
|
25
|
+
* on mount; `false` disables it. Prefetch is deduped per link and is a no-op for routes with no
|
|
26
|
+
* loader.
|
|
27
|
+
*/
|
|
28
|
+
type PrefetchMode = 'intent' | 'render' | false;
|
|
21
29
|
/** Extra (non-target) props accepted by a {@link LinkComponent}. */
|
|
22
30
|
interface LinkOptions {
|
|
23
31
|
/** Replace the current history entry instead of pushing. */
|
|
@@ -28,6 +36,8 @@ interface LinkOptions {
|
|
|
28
36
|
activeClass?: string;
|
|
29
37
|
/** Static class. */
|
|
30
38
|
class?: string;
|
|
39
|
+
/** Auto-prefetch policy (default `'intent'`). */
|
|
40
|
+
prefetch?: PrefetchMode;
|
|
31
41
|
/** Link content. */
|
|
32
42
|
children?: MindeesNode;
|
|
33
43
|
}
|
|
@@ -46,5 +56,5 @@ type LinkComponent = <P extends string>(props: LinkProps<P>) => MindeesElement;
|
|
|
46
56
|
*/
|
|
47
57
|
declare function createLink(router: Router): LinkComponent;
|
|
48
58
|
//#endregion
|
|
49
|
-
export { LinkComponent, LinkOptions, LinkProps, RouterViewOptions, createLink, createRouterView };
|
|
59
|
+
export { LinkComponent, LinkOptions, LinkProps, PrefetchMode, RouterViewOptions, createLink, createRouterView };
|
|
50
60
|
//# sourceMappingURL=components.d.ts.map
|
package/dist/components.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"components.d.ts","names":[],"sources":["../src/components.ts"],"mappings":";;;;;UA6BiB,iBAAA;EAegB;EAb/B,QAAA,GAAW,SAAS;AAAA;;;AAawE;
|
|
1
|
+
{"version":3,"file":"components.d.ts","names":[],"sources":["../src/components.ts"],"mappings":";;;;;UA6BiB,iBAAA;EAegB;EAb/B,QAAA,GAAW,SAAS;AAAA;;;AAawE;AAiD9F;;;;AAAwB;AAGxB;;iBApDgB,gBAAA,CAAiB,MAAA,EAAQ,MAAA,EAAQ,OAAA,GAAS,iBAAA,GAAyB,WAAA;;;;;;;;KAiDvE,YAAA;;UAGK,WAAA;EAYO;EAVtB,OAAA;EAcmB;EAZnB,EAAA;EAYkD;EAVlD,WAAA;EAUuD;EARvD,KAAA;EAQkE;EANlE,QAAA,GAAW,YAAA;EAM6B;EAJxC,QAAA,GAAW,WAAW;AAAA;;KAIZ,SAAA,qBAA8B,SAAA,CAAU,CAAA,IAAK,WAAA;AAGzD;AAAA,KAAY,aAAA,sBAAmC,KAAA,EAAO,SAAA,CAAU,CAAA,MAAO,cAAA;;;;;;;;;;iBAyDvD,UAAA,CAAW,MAAA,EAAQ,MAAA,GAAS,aAAa"}
|
package/dist/components.js
CHANGED
|
@@ -78,8 +78,20 @@ function createLink(router) {
|
|
|
78
78
|
event?.preventDefault?.();
|
|
79
79
|
router.navigate(href, { replace: props.replace === true });
|
|
80
80
|
};
|
|
81
|
+
const prefetch = props.prefetch ?? "intent";
|
|
82
|
+
let prefetched = false;
|
|
83
|
+
const doPrefetch = () => {
|
|
84
|
+
if (prefetched || prefetch === false) return;
|
|
85
|
+
prefetched = true;
|
|
86
|
+
router.preload(href);
|
|
87
|
+
};
|
|
81
88
|
const elementProps = { onClick };
|
|
82
89
|
if (tag === "a") elementProps.href = href;
|
|
90
|
+
if (prefetch === "intent") {
|
|
91
|
+
elementProps.onPointerEnter = doPrefetch;
|
|
92
|
+
elementProps.onPointerDown = doPrefetch;
|
|
93
|
+
elementProps.onFocus = doPrefetch;
|
|
94
|
+
} else if (prefetch === "render") doPrefetch();
|
|
83
95
|
if (props.activeClass !== void 0) {
|
|
84
96
|
const here = buildPath(props.to, props.params ?? {});
|
|
85
97
|
elementProps.class = () => [props.class, router.location().pathname === here ? props.activeClass : void 0].filter((c) => typeof c === "string" && c.length > 0).join(" ");
|
package/dist/components.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"components.js","names":[],"sources":["../src/components.ts"],"sourcesContent":["/**\n * Render integration — `createRouterView` (renders the matched route chain) and\n * `createLink` (typed navigation links). Built on `@mindees/core`'s\n * `createElement` + signals; the renderer turns the returned function nodes into\n * fine-grained reactive regions. See ADR-0004.\n *\n * Nesting uses **explicit composition** (no ambient context): each matched route\n * component receives the next route in the chain as `children` (the outlet), and\n * the router exposes the chain as the reactive `matches` array. Each depth is a\n * reactive region keyed on that depth's route identity, so navigating a leaf\n * re-mounts only the leaf — parent layouts (and their state) are preserved.\n *\n * @module\n */\n\nimport { type Component, createElement, type MindeesElement, type MindeesNode } from '@mindees/core'\nimport type { LoaderData } from './data'\nimport { buildPath } from './pattern'\nimport type { NavTarget, Router } from './router'\nimport { type QueryValue, stringifyQuery } from './search'\n\n/** Shared idle loader state for routes without a loader. */\nconst IDLE_LOADER_DATA: LoaderData = Object.freeze({ status: 'idle' })\n\n// ---------------------------------------------------------------------------\n// RouterView — render the matched route chain\n// ---------------------------------------------------------------------------\n\n/** Options for {@link createRouterView}. */\nexport interface RouterViewOptions {\n /** Rendered when no route matches the current location. */\n notFound?: Component\n}\n\n/**\n * Create the router's view: a node that renders the matched route **chain**\n * top-down, nesting each child into its parent's `children` (the outlet). Render\n * it with the Helix renderer (`render(createRouterView(router), backend, root)`);\n * it re-renders fine-grainedly as navigation changes the matched routes.\n *\n * @example\n * const view = createRouterView(router, { notFound: NotFound })\n * render(view, backend, root)\n */\nexport function createRouterView(router: Router, options: RouterViewOptions = {}): MindeesNode {\n // Each depth is its own reactive region (a function node). A region depends\n // only on a memo of its depth's matched route identity (`Object.is`), so a\n // navigation that doesn't change a given depth's route does NOT re-run that\n // region — parent layouts (and their state) stay mounted.\n //\n // The route memo is created FRESH on every region run, on purpose: a memo\n // cached across runs would be owned by — and disposed with — this region's\n // effect when it re-runs, leaving a dead source (the region would react once\n // and then freeze). A fresh memo each run is always live; the `Object.is`\n // equality on the memo is what still gates re-runs (the memo only re-runs the\n // region when the route at this depth actually changes).\n const outletAt =\n (depth: number): (() => MindeesNode) =>\n (): MindeesNode => {\n const route = router.select((s) => s.matches[depth]?.route ?? null)()\n if (route === null) {\n return depth === 0 && options.notFound ? createElement(options.notFound, {}) : null\n }\n const child = outletAt(depth + 1)\n const component = route.component\n // A component-less (layout/pathless) route passes its child through.\n if (component === undefined) return child\n return createElement(component, {\n router,\n params: router.params,\n search: router.search,\n data: () => {\n const match = router.matches()[depth]\n return match ? router.loaderData(match) : IDLE_LOADER_DATA\n },\n children: child,\n })\n }\n\n return outletAt(0)\n}\n\n// ---------------------------------------------------------------------------\n// Link — typed navigation links\n// ---------------------------------------------------------------------------\n\n/** Extra (non-target) props accepted by a {@link LinkComponent}. */\nexport interface LinkOptions {\n /** Replace the current history entry instead of pushing. */\n replace?: boolean\n /** Host tag to render. Defaults to `'a'` (web). Use e.g. `'view'` on native. */\n as?: string\n /** Class applied (in addition to `class`) when the link's path is the current pathname. */\n activeClass?: string\n /** Static class. */\n class?: string\n /** Link content. */\n children?: MindeesNode\n}\n\n/** Props for a typed link: a {@link NavTarget} plus {@link LinkOptions}. */\nexport type LinkProps<P extends string> = NavTarget<P> & LinkOptions\n\n/** A typed link component — params are required iff the pattern has them. */\nexport type LinkComponent = <P extends string>(props: LinkProps<P>) => MindeesElement\n\n/** The broad runtime shape the Link impl accepts (typed surface is {@link LinkProps}). */\ninterface LinkInput {\n to: string\n params?: Record<string, string | number>\n search?: Record<string, QueryValue>\n hash?: string\n replace?: boolean\n as?: string\n activeClass?: string\n class?: string\n children?: MindeesNode\n}\n\n/** A minimal click-event shape (DOM `MouseEvent` satisfies it; tests can omit it). */\ninterface ClickEventLike {\n preventDefault?: () => void\n defaultPrevented?: boolean\n button?: number\n metaKey?: boolean\n ctrlKey?: boolean\n shiftKey?: boolean\n altKey?: boolean\n}\n\n/** Should a click be left for the browser (modifier/middle-click, already handled)? */\nfunction isModifiedClick(event: ClickEventLike): boolean {\n return (\n event.defaultPrevented === true ||\n (event.button !== undefined && event.button !== 0) ||\n event.metaKey === true ||\n event.ctrlKey === true ||\n event.shiftKey === true ||\n event.altKey === true\n )\n}\n\n/** Assemble an href (path + query + hash) from a link target. */\nfunction hrefFor(props: LinkInput): string {\n const path = buildPath(props.to, props.params ?? {})\n const query = props.search ? stringifyQuery(props.search) : ''\n let hash = props.hash ?? ''\n if (hash.length > 0 && !hash.startsWith('#')) hash = `#${hash}`\n return `${path}${query ? `?${query}` : ''}${hash}`\n}\n\n/**\n * Create a typed `Link` bound to `router`. Calling it returns an element that\n * navigates on activation (default tag `'a'` with `href` + an `onClick` that\n * honors modifier/middle clicks). Params are required iff the pattern has them.\n *\n * @example\n * const Link = createLink(router)\n * Link({ to: '/posts/:postId', params: { postId: '42' }, children: 'Open' })\n */\nexport function createLink(router: Router): LinkComponent {\n const Link = (props: LinkInput): MindeesElement => {\n const tag = props.as ?? 'a'\n const href = hrefFor(props)\n const onClick = (event?: ClickEventLike): void => {\n if (event && isModifiedClick(event)) return\n event?.preventDefault?.()\n // `href` is an absolute path; navigate by string (resolveHref no-ops on absolutes).\n router.navigate(href, { replace: props.replace === true })\n }\n\n const elementProps: Record<string, unknown> = { onClick }\n if (tag === 'a') elementProps.href = href\n\n if (props.activeClass !== undefined) {\n const here = buildPath(props.to, props.params ?? {})\n elementProps.class = (): string =>\n [props.class, router.location().pathname === here ? props.activeClass : undefined]\n .filter((c): c is string => typeof c === 'string' && c.length > 0)\n .join(' ')\n } else if (props.class !== undefined) {\n elementProps.class = props.class\n }\n\n return createElement(tag, elementProps, props.children)\n }\n return Link as LinkComponent\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAsBA,MAAM,mBAA+B,OAAO,OAAO,EAAE,QAAQ,OAAO,CAAC;;;;;;;;;;;AAsBrE,SAAgB,iBAAiB,QAAgB,UAA6B,CAAC,GAAgB;CAY7F,MAAM,YACH,gBACkB;EACjB,MAAM,QAAQ,OAAO,QAAQ,MAAM,EAAE,QAAQ,QAAQ,SAAS,IAAI,EAAE;EACpE,IAAI,UAAU,MACZ,OAAO,UAAU,KAAK,QAAQ,WAAW,cAAc,QAAQ,UAAU,CAAC,CAAC,IAAI;EAEjF,MAAM,QAAQ,SAAS,QAAQ,CAAC;EAChC,MAAM,YAAY,MAAM;EAExB,IAAI,cAAc,KAAA,GAAW,OAAO;EACpC,OAAO,cAAc,WAAW;GAC9B;GACA,QAAQ,OAAO;GACf,QAAQ,OAAO;GACf,YAAY;IACV,MAAM,QAAQ,OAAO,QAAQ,EAAE;IAC/B,OAAO,QAAQ,OAAO,WAAW,KAAK,IAAI;GAC5C;GACA,UAAU;EACZ,CAAC;CACH;CAEF,OAAO,SAAS,CAAC;AACnB;;AAmDA,SAAS,gBAAgB,OAAgC;CACvD,OACE,MAAM,qBAAqB,QAC1B,MAAM,WAAW,KAAA,KAAa,MAAM,WAAW,KAChD,MAAM,YAAY,QAClB,MAAM,YAAY,QAClB,MAAM,aAAa,QACnB,MAAM,WAAW;AAErB;;AAGA,SAAS,QAAQ,OAA0B;CACzC,MAAM,OAAO,UAAU,MAAM,IAAI,MAAM,UAAU,CAAC,CAAC;CACnD,MAAM,QAAQ,MAAM,SAAS,eAAe,MAAM,MAAM,IAAI;CAC5D,IAAI,OAAO,MAAM,QAAQ;CACzB,IAAI,KAAK,SAAS,KAAK,CAAC,KAAK,WAAW,GAAG,GAAG,OAAO,IAAI;CACzD,OAAO,GAAG,OAAO,QAAQ,IAAI,UAAU,KAAK;AAC9C;;;;;;;;;;AAWA,SAAgB,WAAW,QAA+B;CACxD,MAAM,QAAQ,UAAqC;EACjD,MAAM,MAAM,MAAM,MAAM;EACxB,MAAM,OAAO,QAAQ,KAAK;EAC1B,MAAM,WAAW,UAAiC;GAChD,IAAI,SAAS,gBAAgB,KAAK,GAAG;GACrC,OAAO,iBAAiB;GAExB,OAAO,SAAS,MAAM,EAAE,SAAS,MAAM,YAAY,KAAK,CAAC;EAC3D;EAEA,MAAM,eAAwC,EAAE,QAAQ;EACxD,IAAI,QAAQ,KAAK,aAAa,OAAO;EAErC,IAAI,MAAM,gBAAgB,KAAA,GAAW;GACnC,MAAM,OAAO,UAAU,MAAM,IAAI,MAAM,UAAU,CAAC,CAAC;GACnD,aAAa,cACX,CAAC,MAAM,OAAO,OAAO,SAAS,EAAE,aAAa,OAAO,MAAM,cAAc,KAAA,CAAS,EAC9E,QAAQ,MAAmB,OAAO,MAAM,YAAY,EAAE,SAAS,CAAC,EAChE,KAAK,GAAG;EACf,OAAO,IAAI,MAAM,UAAU,KAAA,GACzB,aAAa,QAAQ,MAAM;EAG7B,OAAO,cAAc,KAAK,cAAc,MAAM,QAAQ;CACxD;CACA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"components.js","names":[],"sources":["../src/components.ts"],"sourcesContent":["/**\n * Render integration — `createRouterView` (renders the matched route chain) and\n * `createLink` (typed navigation links). Built on `@mindees/core`'s\n * `createElement` + signals; the renderer turns the returned function nodes into\n * fine-grained reactive regions. See ADR-0004.\n *\n * Nesting uses **explicit composition** (no ambient context): each matched route\n * component receives the next route in the chain as `children` (the outlet), and\n * the router exposes the chain as the reactive `matches` array. Each depth is a\n * reactive region keyed on that depth's route identity, so navigating a leaf\n * re-mounts only the leaf — parent layouts (and their state) are preserved.\n *\n * @module\n */\n\nimport { type Component, createElement, type MindeesElement, type MindeesNode } from '@mindees/core'\nimport type { LoaderData } from './data'\nimport { buildPath } from './pattern'\nimport type { NavTarget, Router } from './router'\nimport { type QueryValue, stringifyQuery } from './search'\n\n/** Shared idle loader state for routes without a loader. */\nconst IDLE_LOADER_DATA: LoaderData = Object.freeze({ status: 'idle' })\n\n// ---------------------------------------------------------------------------\n// RouterView — render the matched route chain\n// ---------------------------------------------------------------------------\n\n/** Options for {@link createRouterView}. */\nexport interface RouterViewOptions {\n /** Rendered when no route matches the current location. */\n notFound?: Component\n}\n\n/**\n * Create the router's view: a node that renders the matched route **chain**\n * top-down, nesting each child into its parent's `children` (the outlet). Render\n * it with the Helix renderer (`render(createRouterView(router), backend, root)`);\n * it re-renders fine-grainedly as navigation changes the matched routes.\n *\n * @example\n * const view = createRouterView(router, { notFound: NotFound })\n * render(view, backend, root)\n */\nexport function createRouterView(router: Router, options: RouterViewOptions = {}): MindeesNode {\n // Each depth is its own reactive region (a function node). A region depends\n // only on a memo of its depth's matched route identity (`Object.is`), so a\n // navigation that doesn't change a given depth's route does NOT re-run that\n // region — parent layouts (and their state) stay mounted.\n //\n // The route memo is created FRESH on every region run, on purpose: a memo\n // cached across runs would be owned by — and disposed with — this region's\n // effect when it re-runs, leaving a dead source (the region would react once\n // and then freeze). A fresh memo each run is always live; the `Object.is`\n // equality on the memo is what still gates re-runs (the memo only re-runs the\n // region when the route at this depth actually changes).\n const outletAt =\n (depth: number): (() => MindeesNode) =>\n (): MindeesNode => {\n const route = router.select((s) => s.matches[depth]?.route ?? null)()\n if (route === null) {\n return depth === 0 && options.notFound ? createElement(options.notFound, {}) : null\n }\n const child = outletAt(depth + 1)\n const component = route.component\n // A component-less (layout/pathless) route passes its child through.\n if (component === undefined) return child\n return createElement(component, {\n router,\n params: router.params,\n search: router.search,\n data: () => {\n const match = router.matches()[depth]\n return match ? router.loaderData(match) : IDLE_LOADER_DATA\n },\n children: child,\n })\n }\n\n return outletAt(0)\n}\n\n// ---------------------------------------------------------------------------\n// Link — typed navigation links\n// ---------------------------------------------------------------------------\n\n/**\n * When the link prefetches its target's loaders (warming the SWR cache so the destination\n * resolves instantly). `'intent'` (default) prefetches on hover / press-in / focus — the\n * Quantum differentiator vs Expo Router's manual-only `router.prefetch`. `'render'` prefetches\n * on mount; `false` disables it. Prefetch is deduped per link and is a no-op for routes with no\n * loader.\n */\nexport type PrefetchMode = 'intent' | 'render' | false\n\n/** Extra (non-target) props accepted by a {@link LinkComponent}. */\nexport interface LinkOptions {\n /** Replace the current history entry instead of pushing. */\n replace?: boolean\n /** Host tag to render. Defaults to `'a'` (web). Use e.g. `'view'` on native. */\n as?: string\n /** Class applied (in addition to `class`) when the link's path is the current pathname. */\n activeClass?: string\n /** Static class. */\n class?: string\n /** Auto-prefetch policy (default `'intent'`). */\n prefetch?: PrefetchMode\n /** Link content. */\n children?: MindeesNode\n}\n\n/** Props for a typed link: a {@link NavTarget} plus {@link LinkOptions}. */\nexport type LinkProps<P extends string> = NavTarget<P> & LinkOptions\n\n/** A typed link component — params are required iff the pattern has them. */\nexport type LinkComponent = <P extends string>(props: LinkProps<P>) => MindeesElement\n\n/** The broad runtime shape the Link impl accepts (typed surface is {@link LinkProps}). */\ninterface LinkInput {\n to: string\n params?: Record<string, string | number>\n search?: Record<string, QueryValue>\n hash?: string\n replace?: boolean\n as?: string\n activeClass?: string\n class?: string\n prefetch?: PrefetchMode\n children?: MindeesNode\n}\n\n/** A minimal click-event shape (DOM `MouseEvent` satisfies it; tests can omit it). */\ninterface ClickEventLike {\n preventDefault?: () => void\n defaultPrevented?: boolean\n button?: number\n metaKey?: boolean\n ctrlKey?: boolean\n shiftKey?: boolean\n altKey?: boolean\n}\n\n/** Should a click be left for the browser (modifier/middle-click, already handled)? */\nfunction isModifiedClick(event: ClickEventLike): boolean {\n return (\n event.defaultPrevented === true ||\n (event.button !== undefined && event.button !== 0) ||\n event.metaKey === true ||\n event.ctrlKey === true ||\n event.shiftKey === true ||\n event.altKey === true\n )\n}\n\n/** Assemble an href (path + query + hash) from a link target. */\nfunction hrefFor(props: LinkInput): string {\n const path = buildPath(props.to, props.params ?? {})\n const query = props.search ? stringifyQuery(props.search) : ''\n let hash = props.hash ?? ''\n if (hash.length > 0 && !hash.startsWith('#')) hash = `#${hash}`\n return `${path}${query ? `?${query}` : ''}${hash}`\n}\n\n/**\n * Create a typed `Link` bound to `router`. Calling it returns an element that\n * navigates on activation (default tag `'a'` with `href` + an `onClick` that\n * honors modifier/middle clicks). Params are required iff the pattern has them.\n *\n * @example\n * const Link = createLink(router)\n * Link({ to: '/posts/:postId', params: { postId: '42' }, children: 'Open' })\n */\nexport function createLink(router: Router): LinkComponent {\n const Link = (props: LinkInput): MindeesElement => {\n const tag = props.as ?? 'a'\n const href = hrefFor(props)\n const onClick = (event?: ClickEventLike): void => {\n if (event && isModifiedClick(event)) return\n event?.preventDefault?.()\n // `href` is an absolute path; navigate by string (resolveHref no-ops on absolutes).\n router.navigate(href, { replace: props.replace === true })\n }\n\n // Auto-prefetch: warm the target's loaders so navigation resolves instantly. Deduped per\n // link (preload itself is also SWR-deduped), and a no-op for loader-less routes.\n const prefetch: PrefetchMode = props.prefetch ?? 'intent'\n let prefetched = false\n const doPrefetch = (): void => {\n if (prefetched || prefetch === false) return\n prefetched = true\n router.preload(href)\n }\n\n const elementProps: Record<string, unknown> = { onClick }\n if (tag === 'a') elementProps.href = href\n if (prefetch === 'intent') {\n // Hover (web), press-in (touch/web), and keyboard focus all signal intent.\n elementProps.onPointerEnter = doPrefetch\n elementProps.onPointerDown = doPrefetch\n elementProps.onFocus = doPrefetch\n } else if (prefetch === 'render') {\n doPrefetch()\n }\n\n if (props.activeClass !== undefined) {\n const here = buildPath(props.to, props.params ?? {})\n elementProps.class = (): string =>\n [props.class, router.location().pathname === here ? props.activeClass : undefined]\n .filter((c): c is string => typeof c === 'string' && c.length > 0)\n .join(' ')\n } else if (props.class !== undefined) {\n elementProps.class = props.class\n }\n\n return createElement(tag, elementProps, props.children)\n }\n return Link as LinkComponent\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAsBA,MAAM,mBAA+B,OAAO,OAAO,EAAE,QAAQ,OAAO,CAAC;;;;;;;;;;;AAsBrE,SAAgB,iBAAiB,QAAgB,UAA6B,CAAC,GAAgB;CAY7F,MAAM,YACH,gBACkB;EACjB,MAAM,QAAQ,OAAO,QAAQ,MAAM,EAAE,QAAQ,QAAQ,SAAS,IAAI,EAAE;EACpE,IAAI,UAAU,MACZ,OAAO,UAAU,KAAK,QAAQ,WAAW,cAAc,QAAQ,UAAU,CAAC,CAAC,IAAI;EAEjF,MAAM,QAAQ,SAAS,QAAQ,CAAC;EAChC,MAAM,YAAY,MAAM;EAExB,IAAI,cAAc,KAAA,GAAW,OAAO;EACpC,OAAO,cAAc,WAAW;GAC9B;GACA,QAAQ,OAAO;GACf,QAAQ,OAAO;GACf,YAAY;IACV,MAAM,QAAQ,OAAO,QAAQ,EAAE;IAC/B,OAAO,QAAQ,OAAO,WAAW,KAAK,IAAI;GAC5C;GACA,UAAU;EACZ,CAAC;CACH;CAEF,OAAO,SAAS,CAAC;AACnB;;AA+DA,SAAS,gBAAgB,OAAgC;CACvD,OACE,MAAM,qBAAqB,QAC1B,MAAM,WAAW,KAAA,KAAa,MAAM,WAAW,KAChD,MAAM,YAAY,QAClB,MAAM,YAAY,QAClB,MAAM,aAAa,QACnB,MAAM,WAAW;AAErB;;AAGA,SAAS,QAAQ,OAA0B;CACzC,MAAM,OAAO,UAAU,MAAM,IAAI,MAAM,UAAU,CAAC,CAAC;CACnD,MAAM,QAAQ,MAAM,SAAS,eAAe,MAAM,MAAM,IAAI;CAC5D,IAAI,OAAO,MAAM,QAAQ;CACzB,IAAI,KAAK,SAAS,KAAK,CAAC,KAAK,WAAW,GAAG,GAAG,OAAO,IAAI;CACzD,OAAO,GAAG,OAAO,QAAQ,IAAI,UAAU,KAAK;AAC9C;;;;;;;;;;AAWA,SAAgB,WAAW,QAA+B;CACxD,MAAM,QAAQ,UAAqC;EACjD,MAAM,MAAM,MAAM,MAAM;EACxB,MAAM,OAAO,QAAQ,KAAK;EAC1B,MAAM,WAAW,UAAiC;GAChD,IAAI,SAAS,gBAAgB,KAAK,GAAG;GACrC,OAAO,iBAAiB;GAExB,OAAO,SAAS,MAAM,EAAE,SAAS,MAAM,YAAY,KAAK,CAAC;EAC3D;EAIA,MAAM,WAAyB,MAAM,YAAY;EACjD,IAAI,aAAa;EACjB,MAAM,mBAAyB;GAC7B,IAAI,cAAc,aAAa,OAAO;GACtC,aAAa;GACb,OAAO,QAAQ,IAAI;EACrB;EAEA,MAAM,eAAwC,EAAE,QAAQ;EACxD,IAAI,QAAQ,KAAK,aAAa,OAAO;EACrC,IAAI,aAAa,UAAU;GAEzB,aAAa,iBAAiB;GAC9B,aAAa,gBAAgB;GAC7B,aAAa,UAAU;EACzB,OAAO,IAAI,aAAa,UACtB,WAAW;EAGb,IAAI,MAAM,gBAAgB,KAAA,GAAW;GACnC,MAAM,OAAO,UAAU,MAAM,IAAI,MAAM,UAAU,CAAC,CAAC;GACnD,aAAa,cACX,CAAC,MAAM,OAAO,OAAO,SAAS,EAAE,aAAa,OAAO,MAAM,cAAc,KAAA,CAAS,EAC9E,QAAQ,MAAmB,OAAO,MAAM,YAAY,EAAE,SAAS,CAAC,EAChE,KAAK,GAAG;EACf,OAAO,IAAI,MAAM,UAAU,KAAA,GACzB,aAAa,QAAQ,MAAM;EAG7B,OAAO,cAAc,KAAK,cAAc,MAAM,QAAQ;CACxD;CACA,OAAO;AACT"}
|
package/dist/hooks.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ declare function useRouter(): Router;
|
|
|
8
8
|
declare function useParams(): () => Record<string, string>;
|
|
9
9
|
/** Reactive accessor for the current (schema-validated) search params. */
|
|
10
10
|
declare function useSearch(): () => Record<string, unknown>;
|
|
11
|
-
/** Reactive accessor for the current pathname. */
|
|
11
|
+
/** Reactive accessor for the current pathname (re-render isolated — only changes when the path does). */
|
|
12
12
|
declare function usePathname(): () => string;
|
|
13
13
|
/**
|
|
14
14
|
* A typed `<Link>` bound to the active router — no need to thread the router through.
|
package/dist/hooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","names":[],"sources":["../src/hooks.ts"],"mappings":";;;;;iBAgBgB,SAAA,IAAa,MAAM;AAgBnC;AAAA,iBALgB,SAAA,UAAmB,MAAM;;iBAKzB,SAAA,UAAmB,MAAM;AAAA;AAAA,iBAKzB,WAAA;;;;AAAW;
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","names":[],"sources":["../src/hooks.ts"],"mappings":";;;;;iBAgBgB,SAAA,IAAa,MAAM;AAgBnC;AAAA,iBALgB,SAAA,UAAmB,MAAM;;iBAKzB,SAAA,UAAmB,MAAM;AAAA;AAAA,iBAKzB,WAAA;;;;AAAW;AAe3B;;;;cAAa,IAAA,EAAM,aAOlB"}
|
package/dist/hooks.js
CHANGED
|
@@ -25,10 +25,9 @@ function useParams() {
|
|
|
25
25
|
function useSearch() {
|
|
26
26
|
return useRouter().search;
|
|
27
27
|
}
|
|
28
|
-
/** Reactive accessor for the current pathname. */
|
|
28
|
+
/** Reactive accessor for the current pathname (re-render isolated — only changes when the path does). */
|
|
29
29
|
function usePathname() {
|
|
30
|
-
|
|
31
|
-
return () => router.location().pathname;
|
|
30
|
+
return useRouter().select((state) => state.pathname);
|
|
32
31
|
}
|
|
33
32
|
let cachedRouter = null;
|
|
34
33
|
let cachedLink = null;
|
package/dist/hooks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.js","names":[],"sources":["../src/hooks.ts"],"sourcesContent":["/**\n * Ergonomic hooks + a bound `Link`, resolving the active router so components don't\n * prop-drill it — the familiar Expo Router surface (`useRouter`, `useLocalSearchParams`,\n * `<Link>`), on Quantum's fine-grained, validated core.\n *\n * The hooks return Quantum's reactive **accessors** (call them inside JSX/effects), so\n * reads stay fine-grained — only what changed re-runs (no whole-stack re-render).\n *\n * @module\n */\n\nimport { getActiveRouter } from './active'\nimport { createLink, type LinkComponent } from './components'\nimport type { Router } from './router'\n\n/** The active router. Throws if none has been created (call `createRouter`/`createFileRouter`). */\nexport function useRouter(): Router {\n const router = getActiveRouter()\n if (!router) {\n throw new Error(\n 'useRouter(): no active router. Create one with createRouter() or createFileRouter() first.',\n )\n }\n return router\n}\n\n/** Reactive accessor for the current path params (`{}` when unmatched). */\nexport function useParams(): () => Record<string, string> {\n return useRouter().params\n}\n\n/** Reactive accessor for the current (schema-validated) search params. */\nexport function useSearch(): () => Record<string, unknown> {\n return useRouter().search\n}\n\n/** Reactive accessor for the current pathname. */\nexport function usePathname(): () => string {\n
|
|
1
|
+
{"version":3,"file":"hooks.js","names":[],"sources":["../src/hooks.ts"],"sourcesContent":["/**\n * Ergonomic hooks + a bound `Link`, resolving the active router so components don't\n * prop-drill it — the familiar Expo Router surface (`useRouter`, `useLocalSearchParams`,\n * `<Link>`), on Quantum's fine-grained, validated core.\n *\n * The hooks return Quantum's reactive **accessors** (call them inside JSX/effects), so\n * reads stay fine-grained — only what changed re-runs (no whole-stack re-render).\n *\n * @module\n */\n\nimport { getActiveRouter } from './active'\nimport { createLink, type LinkComponent } from './components'\nimport type { Router } from './router'\n\n/** The active router. Throws if none has been created (call `createRouter`/`createFileRouter`). */\nexport function useRouter(): Router {\n const router = getActiveRouter()\n if (!router) {\n throw new Error(\n 'useRouter(): no active router. Create one with createRouter() or createFileRouter() first.',\n )\n }\n return router\n}\n\n/** Reactive accessor for the current path params (`{}` when unmatched). */\nexport function useParams(): () => Record<string, string> {\n return useRouter().params\n}\n\n/** Reactive accessor for the current (schema-validated) search params. */\nexport function useSearch(): () => Record<string, unknown> {\n return useRouter().search\n}\n\n/** Reactive accessor for the current pathname (re-render isolated — only changes when the path does). */\nexport function usePathname(): () => string {\n return useRouter().select((state) => state.pathname)\n}\n\nlet cachedRouter: Router | null = null\nlet cachedLink: LinkComponent | null = null\n\n/**\n * A typed `<Link>` bound to the active router — no need to thread the router through.\n * `params` are required iff the target pattern has dynamic segments (inferred, no codegen).\n *\n * @example\n * <Link to=\"/about\">About</Link>\n * <Link to=\"/posts/:id\" params={{ id: '42' }}>Open</Link>\n */\nexport const Link: LinkComponent = (props) => {\n const router = useRouter()\n if (router !== cachedRouter) {\n cachedRouter = router\n cachedLink = createLink(router)\n }\n return (cachedLink as LinkComponent)(props)\n}\n"],"mappings":";;;;;;;;;;;;;;AAgBA,SAAgB,YAAoB;CAClC,MAAM,SAAS,gBAAgB;CAC/B,IAAI,CAAC,QACH,MAAM,IAAI,MACR,4FACF;CAEF,OAAO;AACT;;AAGA,SAAgB,YAA0C;CACxD,OAAO,UAAU,EAAE;AACrB;;AAGA,SAAgB,YAA2C;CACzD,OAAO,UAAU,EAAE;AACrB;;AAGA,SAAgB,cAA4B;CAC1C,OAAO,UAAU,EAAE,QAAQ,UAAU,MAAM,QAAQ;AACrD;AAEA,IAAI,eAA8B;AAClC,IAAI,aAAmC;;;;;;;;;AAUvC,MAAa,QAAuB,UAAU;CAC5C,MAAM,SAAS,UAAU;CACzB,IAAI,WAAW,cAAc;EAC3B,eAAe;EACf,aAAa,WAAW,MAAM;CAChC;CACA,OAAQ,WAA6B,KAAK;AAC5C"}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { HasPathParams, PathParams, buildPath, compareSpecificity, matchPattern,
|
|
|
4
4
|
import { StandardSchemaV1 } from "./standard-schema.js";
|
|
5
5
|
import { QueryValue, ValidationResult, parseQuery, safeValidateSearch, stringifyQuery, validateSearch } from "./search.js";
|
|
6
6
|
import { BeforeNavigate, CreateRouterOptions, NavTarget, NavigateOptions, RouteComponentProps, RouteMatch, RouteRecord, Router, RouterState, createRouter, resolvePath } from "./router.js";
|
|
7
|
-
import { LinkComponent, LinkOptions, LinkProps, RouterViewOptions, createLink, createRouterView } from "./components.js";
|
|
7
|
+
import { LinkComponent, LinkOptions, LinkProps, PrefetchMode, RouterViewOptions, createLink, createRouterView } from "./components.js";
|
|
8
8
|
import { RouterError, RouterErrorCode } from "./errors.js";
|
|
9
9
|
import { RouteModule, createFileRouter, routesFromModules } from "./file-routes.js";
|
|
10
10
|
import { Link, useParams, usePathname, useRouter, useSearch } from "./hooks.js";
|
|
@@ -14,7 +14,7 @@ import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@min
|
|
|
14
14
|
/** The npm package name. */
|
|
15
15
|
declare const name = "@mindees/router";
|
|
16
16
|
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
17
|
-
declare const VERSION = "0.
|
|
17
|
+
declare const VERSION = "0.4.0";
|
|
18
18
|
/**
|
|
19
19
|
* Current maturity. Router I (typed params, Standard-Schema search, history, the
|
|
20
20
|
* signals-native router, selector-isolated state, typed + relative navigation)
|
|
@@ -26,5 +26,5 @@ declare const maturity: Maturity;
|
|
|
26
26
|
/** Static identity + maturity metadata for this package. */
|
|
27
27
|
declare const info: PackageInfo;
|
|
28
28
|
//#endregion
|
|
29
|
-
export { type BeforeNavigate, type CreateRouterOptions, type HasPathParams, type HistoryListener, Link, type LinkComponent, type LinkOptions, type LinkProps, type LoaderContext, type LoaderData, type LoaderDepsFn, type LoaderFn, type LoaderStatus, type Maturity, type MemoryHistoryOptions, type NavTarget, type NavigateOptions, NotImplementedError, type PackageInfo, type PathParams, type QueryValue, type RouteComponentProps, type RouteMatch, type RouteModule, type RouteRecord, type Router, RouterError, type RouterErrorCode, type RouterHistory, type RouterLocation, type RouterState, type RouterViewOptions, type StandardSchemaV1, VERSION, type ValidationResult, buildPath, compareSpecificity, createBrowserHistory, createFileRouter, createHref, createLink, createMemoryHistory, createRouter, createRouterView, info, matchPattern, maturity, name, notImplemented, parseHref, parsePattern, parseQuery, resolvePath, routesFromModules, safeValidateSearch, stringifyQuery, useParams, usePathname, useRouter, useSearch, validateSearch };
|
|
29
|
+
export { type BeforeNavigate, type CreateRouterOptions, type HasPathParams, type HistoryListener, Link, type LinkComponent, type LinkOptions, type LinkProps, type LoaderContext, type LoaderData, type LoaderDepsFn, type LoaderFn, type LoaderStatus, type Maturity, type MemoryHistoryOptions, type NavTarget, type NavigateOptions, NotImplementedError, type PackageInfo, type PathParams, type PrefetchMode, type QueryValue, type RouteComponentProps, type RouteMatch, type RouteModule, type RouteRecord, type Router, RouterError, type RouterErrorCode, type RouterHistory, type RouterLocation, type RouterState, type RouterViewOptions, type StandardSchemaV1, VERSION, type ValidationResult, buildPath, compareSpecificity, createBrowserHistory, createFileRouter, createHref, createLink, createMemoryHistory, createRouter, createRouterView, info, matchPattern, maturity, name, notImplemented, parseHref, parsePattern, parseQuery, resolvePath, routesFromModules, safeValidateSearch, stringifyQuery, useParams, usePathname, useRouter, useSearch, validateSearch };
|
|
30
30
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;;;;;;;cA8Fa,IAAA;;cAGA,OAAA;;;;;;;;cASA,QAAA,EAAU,QAAyB;;cAGnC,IAAA,EAAM,WAAkD"}
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { NotImplementedError, notImplemented } from "@mindees/core";
|
|
|
11
11
|
/** The npm package name. */
|
|
12
12
|
const name = "@mindees/router";
|
|
13
13
|
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
14
|
-
const VERSION = "0.
|
|
14
|
+
const VERSION = "0.4.0";
|
|
15
15
|
/**
|
|
16
16
|
* Current maturity. Router I (typed params, Standard-Schema search, history, the
|
|
17
17
|
* signals-native router, selector-isolated state, typed + relative navigation)
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/router` — **Quantum**, the typed router for MindeesNative.\n *\n * Router I (Phase 6): codegen-free typed path params ({@link PathParams}),\n * Standard-Schema validated search params, a signals-native router with typed +\n * relative navigation and selector-isolated state, and an injectable history\n * (memory + browser). See ADR-0003.\n *\n * Router II (Phase 7): render integration — {@link createRouterView} (nested,\n * fine-grained, layout-preserving) and typed {@link createLink} — plus SWR data\n * loaders (with `AbortSignal`, {@link Router.invalidate}/{@link Router.preload}),\n * navigation guards ({@link BeforeNavigate} cancel/redirect + idempotent\n * navigation), and web view transitions. See ADR-0004 and ADR-0005.\n *\n * Still a later phase (not exported): the global typed route registry and\n * file-based route scanning + bundler plugin. See `STATUS.md`.\n *\n * @module\n */\n\nimport type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** Render integration: nested view + typed links (Router II). */\nexport {\n createLink,\n createRouterView,\n type LinkComponent,\n type LinkOptions,\n type LinkProps,\n type RouterViewOptions,\n} from './components'\n/** Loaders + data (SWR). */\nexport type {\n LoaderContext,\n LoaderData,\n LoaderDepsFn,\n LoaderFn,\n LoaderStatus,\n} from './data'\n/** Errors. */\nexport { RouterError, type RouterErrorCode } from './errors'\n/** File-based routing: a module map → a router (Expo-style conventions). */\nexport { createFileRouter, type RouteModule, routesFromModules } from './file-routes'\n/** History capability. */\nexport {\n createBrowserHistory,\n createHref,\n createMemoryHistory,\n type HistoryListener,\n type MemoryHistoryOptions,\n parseHref,\n type RouterHistory,\n type RouterLocation,\n} from './history'\n/** Ergonomic hooks + a bound Link that resolve the active router. */\nexport { Link, useParams, usePathname, useRouter, useSearch } from './hooks'\n/** Route patterns + codegen-free typed params. */\nexport {\n buildPath,\n compareSpecificity,\n type HasPathParams,\n matchPattern,\n type PathParams,\n parsePattern,\n} from './pattern'\n/** Router. */\nexport {\n type BeforeNavigate,\n type CreateRouterOptions,\n createRouter,\n type NavigateOptions,\n type NavTarget,\n type RouteComponentProps,\n type RouteMatch,\n type RouteRecord,\n type Router,\n type RouterState,\n resolvePath,\n} from './router'\n/** Search (query) params. */\nexport {\n parseQuery,\n type QueryValue,\n safeValidateSearch,\n stringifyQuery,\n type ValidationResult,\n validateSearch,\n} from './search'\n/** Standard Schema — the validator-agnostic interface (vendored, types only). */\nexport type { StandardSchemaV1 } from './standard-schema'\n\n/** The npm package name. */\nexport const name = '@mindees/router'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/router` — **Quantum**, the typed router for MindeesNative.\n *\n * Router I (Phase 6): codegen-free typed path params ({@link PathParams}),\n * Standard-Schema validated search params, a signals-native router with typed +\n * relative navigation and selector-isolated state, and an injectable history\n * (memory + browser). See ADR-0003.\n *\n * Router II (Phase 7): render integration — {@link createRouterView} (nested,\n * fine-grained, layout-preserving) and typed {@link createLink} — plus SWR data\n * loaders (with `AbortSignal`, {@link Router.invalidate}/{@link Router.preload}),\n * navigation guards ({@link BeforeNavigate} cancel/redirect + idempotent\n * navigation), and web view transitions. See ADR-0004 and ADR-0005.\n *\n * Still a later phase (not exported): the global typed route registry and\n * file-based route scanning + bundler plugin. See `STATUS.md`.\n *\n * @module\n */\n\nimport type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** Render integration: nested view + typed links (Router II). */\nexport {\n createLink,\n createRouterView,\n type LinkComponent,\n type LinkOptions,\n type LinkProps,\n type PrefetchMode,\n type RouterViewOptions,\n} from './components'\n/** Loaders + data (SWR). */\nexport type {\n LoaderContext,\n LoaderData,\n LoaderDepsFn,\n LoaderFn,\n LoaderStatus,\n} from './data'\n/** Errors. */\nexport { RouterError, type RouterErrorCode } from './errors'\n/** File-based routing: a module map → a router (Expo-style conventions). */\nexport { createFileRouter, type RouteModule, routesFromModules } from './file-routes'\n/** History capability. */\nexport {\n createBrowserHistory,\n createHref,\n createMemoryHistory,\n type HistoryListener,\n type MemoryHistoryOptions,\n parseHref,\n type RouterHistory,\n type RouterLocation,\n} from './history'\n/** Ergonomic hooks + a bound Link that resolve the active router. */\nexport { Link, useParams, usePathname, useRouter, useSearch } from './hooks'\n/** Route patterns + codegen-free typed params. */\nexport {\n buildPath,\n compareSpecificity,\n type HasPathParams,\n matchPattern,\n type PathParams,\n parsePattern,\n} from './pattern'\n/** Router. */\nexport {\n type BeforeNavigate,\n type CreateRouterOptions,\n createRouter,\n type NavigateOptions,\n type NavTarget,\n type RouteComponentProps,\n type RouteMatch,\n type RouteRecord,\n type Router,\n type RouterState,\n resolvePath,\n} from './router'\n/** Search (query) params. */\nexport {\n parseQuery,\n type QueryValue,\n safeValidateSearch,\n stringifyQuery,\n type ValidationResult,\n validateSearch,\n} from './search'\n/** Standard Schema — the validator-agnostic interface (vendored, types only). */\nexport type { StandardSchemaV1 } from './standard-schema'\n\n/** The npm package name. */\nexport const name = '@mindees/router'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.4.0'\n\n/**\n * Current maturity. Router I (typed params, Standard-Schema search, history, the\n * signals-native router, selector-isolated state, typed + relative navigation)\n * and Router II (nested rendering, typed links, SWR loaders, navigation guards,\n * view transitions) are implemented and tested. The global typed route registry\n * and file-based route scanning are a later phase — see `STATUS.md`.\n */\nexport const maturity: Maturity = 'experimental'\n\n/** Static identity + maturity metadata for this package. */\nexport const info: PackageInfo = { name, version: VERSION, maturity }\n\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;;;AA8FA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;;;;;;;AASvB,MAAa,WAAqB;;AAGlC,MAAa,OAAoB;CAAE;CAAM,SAAS;CAAS;AAAS"}
|
package/dist/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","names":[],"sources":["../src/router.ts"],"mappings":";;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"router.d.ts","names":[],"sources":["../src/router.ts"],"mappings":";;;;;;;;;;;;;;;UA4DiB,mBAAA;EAUf;EARA,MAAA,EAAQ,MAAA;EAQa;EANrB,MAAA,QAAc,MAAA;EAUC;EARf,MAAA,QAAc,MAAA;;EAEd,IAAA,QAAY,UAAA;EAcA;EAZZ,QAAA,EAAU,WAAA;AAAA;;UAIK,WAAA;EA2BK;;;;;EArBpB,IAAA;EAEA;EAAA,SAAA,GAAY,SAAA,CAAU,mBAAA;EAAA;;;;;;;;;;EAWtB,YAAA,GAAe,gBAAA,UAA0B,MAAA;EAUzC;EARA,MAAA,GAAS,QAAA;EAQO;EANhB,UAAA,GAAa,YAAA;EAMS;EAJtB,SAAA;EAQyB;EANzB,QAAA,YAAoB,WAAA;EAQb;EANP,IAAA,GAAO,QAAA,CAAS,MAAA;AAAA;;UAID,UAAA;EAYN;EAVT,KAAA,EAAO,WAAA;EAUe;EARtB,QAAA;EAFO;EAIP,MAAA,EAAQ,MAAA;EAAR;EAEA,MAAA,EAAQ,MAAA;EAAR;EAEA,SAAA,EAAW,MAAA;EAAX;EAEA,MAAA,GAAS,aAAA,CAAc,gBAAA,CAAiB,KAAA;AAAA;;UAIzB,WAAA;EAJyB;EAMxC,QAAA,EAAU,cAAA;EANmC;AAI/C;;;EAOE,OAAA,WAAkB,UAAA;EAAA;EAElB,KAAA,EAAO,UAAA;EAIC;EAFR,QAAA;EAIc;EAFd,MAAA,EAAQ,MAAA;EAXR;EAaA,MAAA,EAAQ,MAAA;AAAA;;UAQO,eAAA;EAdR;EAgBP,OAAA;EAZA;EAcA,KAAA;EAZA;;;AAAc;EAiBd,cAAA;AAAA;;;;;KAOU,cAAA,IAAkB,EAAA,UAAY,IAAY;;KAGjD,SAAA;EAHO,wDAKV,MAAA,GAAS,MAAA,SAAe,UAAA;EAExB,IAAA;AAAA,KACG,aAAA,CAAc,CAAA;EAAoB,MAAA,EAAQ,UAAA,CAAW,CAAA;AAAA;EAAS,MAAA,GAAS,MAAA;AAAA;;;;;;;;;;;;;;;KAgBhE,SAAA;EAAgC,EAAA,EAAI,CAAA;AAAA,IAAM,SAAA,CAAU,CAAA;;UAe/C,mBAAA;EA/B2D;EAiC1E,MAAA,WAAiB,WAAA;EAjC+D;EAmChF,OAAA,GAAU,aAAA;EAnBS;EAqBnB,cAAA,GAAiB,cAAA;EArB6B;EAuB9C,eAAA;AAAA;;UAIe,MAAA;EA3BK;EA6BpB,KAAA,IAAS,WAAA;EA7BqC;EA+B9C,QAAA,IAAY,cAAA;EA/BkD;EAiC9D,OAAA,aAAoB,UAAA;EAjC2C;EAmC/D,KAAA,IAAS,UAAA;EApByB;EAsBlC,MAAA,IAAU,MAAA;EApBO;EAsBjB,MAAA,IAAU,MAAA;EAlBO;;;;;;;EA0BjB,MAAA,IAAU,QAAA,GAAW,KAAA,EAAO,WAAA,KAAgB,CAAA,EAAG,MAAA,IAAU,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,qBAAsB,CAAA;EA1BvE;EA4BjB,QAAA,mBAA2B,MAAA,WAAiB,SAAA,CAAU,CAAA,GAAI,OAAA,GAAU,eAAA;EA1BrD;EA4Bf,UAAA,CAAW,KAAA,EAAO,UAAA,GAAa,UAAA;EAxBhB;EA0Bf,UAAA;EA1BqB;;;;;;EAiCrB,OAAA,CAAQ,EAAA;EAboB;;;;EAkB5B,SAAA,CAAU,MAAA,WAAiB,WAAA;EAhB2B;;;;;;EAuBtD,MAAA,aAAmB,WAAA;EAED;EAAA,SAAT,OAAA,EAAS,aAAA;EAAa;EAE/B,OAAA;AAAA;;;;;;;;;;;iBA0Ic,WAAA,CAAY,EAAA,UAAY,IAAY;;;;;iBA+CpC,YAAA,CAAa,OAAA,EAAS,mBAAA,GAAsB,MAAM"}
|
package/dist/router.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { buildPath, compareSpecificity, matchPattern } from "./pattern.js";
|
|
1
|
+
import { buildPath, compareSpecificity, matchPattern, parsePattern } from "./pattern.js";
|
|
2
2
|
import { parseQuery, safeValidateSearch, stringifyQuery } from "./search.js";
|
|
3
|
-
import { setActiveRouter } from "./active.js";
|
|
3
|
+
import { clearActiveRouter, setActiveRouter } from "./active.js";
|
|
4
4
|
import { createLoaderManager } from "./data.js";
|
|
5
5
|
import { createHref, createMemoryHistory, parseHref } from "./history.js";
|
|
6
6
|
import { computed, createRoot, effect, signal } from "@mindees/core";
|
|
@@ -46,9 +46,36 @@ function flattenRouteTree(routes, parentPath, parentChain) {
|
|
|
46
46
|
}
|
|
47
47
|
return out;
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
49
|
+
/** Dev-only warning (silent in production). */
|
|
50
|
+
function warnDev(message) {
|
|
51
|
+
const g = globalThis;
|
|
52
|
+
if (g.process?.env?.NODE_ENV === "production") return;
|
|
53
|
+
g.console?.warn?.(`[router] ${message}`);
|
|
54
|
+
}
|
|
55
|
+
/** Shallow record equality — params/search only "change" when an entry actually changes. */
|
|
56
|
+
function shallowEqualRecord(a, b) {
|
|
57
|
+
if (a === b) return true;
|
|
58
|
+
const ak = Object.keys(a);
|
|
59
|
+
if (ak.length !== Object.keys(b).length) return false;
|
|
60
|
+
for (const k of ak) if (a[k] !== b[k]) return false;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Flatten + sort a route tree most-specific first (static > dynamic > catch-all). Routes whose
|
|
65
|
+
* synthesized pattern is invalid (a catch-all parent with children would place a catch-all
|
|
66
|
+
* segment before a literal one) are dropped with a dev warning rather than thrown — one bad
|
|
67
|
+
* route must not poison all router state.
|
|
68
|
+
*/
|
|
50
69
|
function compileRoutes(routes) {
|
|
51
|
-
return flattenRouteTree(routes, "", []).
|
|
70
|
+
return flattenRouteTree(routes, "", []).filter((fr) => {
|
|
71
|
+
try {
|
|
72
|
+
parsePattern(fr.fullPath);
|
|
73
|
+
return true;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
warnDev(`ignoring invalid route pattern ${fr.fullPath}: ${error.message}`);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}).sort((a, b) => compareSpecificity(a.fullPath, b.fullPath));
|
|
52
79
|
}
|
|
53
80
|
/**
|
|
54
81
|
* Match a location against the compiled route table, returning the matched chain
|
|
@@ -141,6 +168,8 @@ function createRouter(options) {
|
|
|
141
168
|
let flatMemo;
|
|
142
169
|
let locationSig;
|
|
143
170
|
let stateMemo;
|
|
171
|
+
let paramsMemo;
|
|
172
|
+
let searchMemo;
|
|
144
173
|
let loaders;
|
|
145
174
|
const dispose = createRoot((disposeRoot) => {
|
|
146
175
|
routesSig = signal(options.routes, { equals: false });
|
|
@@ -160,6 +189,8 @@ function createRouter(options) {
|
|
|
160
189
|
search: leaf ? leaf.search : EMPTY_SEARCH
|
|
161
190
|
};
|
|
162
191
|
});
|
|
192
|
+
paramsMemo = computed(() => stateMemo().params, { equals: shallowEqualRecord });
|
|
193
|
+
searchMemo = computed(() => stateMemo().search, { equals: shallowEqualRecord });
|
|
163
194
|
const dataVersion = signal(0, { equals: false });
|
|
164
195
|
loaders = createLoaderManager({
|
|
165
196
|
location: () => locationSig(),
|
|
@@ -214,8 +245,8 @@ function createRouter(options) {
|
|
|
214
245
|
location: () => locationSig(),
|
|
215
246
|
matches: () => stateMemo().matches,
|
|
216
247
|
match: () => stateMemo().match,
|
|
217
|
-
params: () =>
|
|
218
|
-
search: () =>
|
|
248
|
+
params: () => paramsMemo(),
|
|
249
|
+
search: () => searchMemo(),
|
|
219
250
|
select: (selector, equals = Object.is) => computed(() => selector(stateMemo()), { equals }),
|
|
220
251
|
navigate,
|
|
221
252
|
loaderData: (match) => loaders.read(match),
|
|
@@ -224,7 +255,10 @@ function createRouter(options) {
|
|
|
224
255
|
setRoutes: (routes) => routesSig.set(routes),
|
|
225
256
|
routes: () => routesSig(),
|
|
226
257
|
history,
|
|
227
|
-
dispose
|
|
258
|
+
dispose: () => {
|
|
259
|
+
dispose();
|
|
260
|
+
clearActiveRouter(router);
|
|
261
|
+
}
|
|
228
262
|
};
|
|
229
263
|
setActiveRouter(router);
|
|
230
264
|
return router;
|
package/dist/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","names":[],"sources":["../src/router.ts"],"sourcesContent":["/**\n * The Quantum router — signals-native routing state with typed, validated\n * navigation and re-render isolation.\n *\n * Router state (location, params, search, matched route) is modeled as the\n * fine-grained signal graph from `@mindees/core` (Phase 1 `signal`/`computed`,\n * Phase 2 selector isolation). Consumers read a slice via {@link Router.select}\n * and re-run **only** when that slice changes — no whole-tree re-render on\n * navigation, no global-vs-local hook trap (cf. Expo Router). See ADR-0003.\n *\n * @module\n */\n\nimport {\n type Component,\n computed,\n createRoot,\n effect,\n type Memo,\n type MindeesNode,\n type Signal,\n signal,\n} from '@mindees/core'\nimport { setActiveRouter } from './active'\nimport {\n createLoaderManager,\n type LoaderData,\n type LoaderDepsFn,\n type LoaderFn,\n type LoaderManager,\n} from './data'\nimport {\n createHref,\n createMemoryHistory,\n parseHref,\n type RouterHistory,\n type RouterLocation,\n} from './history'\nimport {\n buildPath,\n compareSpecificity,\n type HasPathParams,\n matchPattern,\n type PathParams,\n} from './pattern'\nimport { parseQuery, type QueryValue, safeValidateSearch, stringifyQuery } from './search'\nimport type { StandardSchemaV1 } from './standard-schema'\n\n// ---------------------------------------------------------------------------\n// Route table types\n// ---------------------------------------------------------------------------\n\n/**\n * Props a route's component receives from {@link createRouterView}. `params` and\n * `search` are **reactive accessors** (read them in a reactive scope so a\n * same-route param change updates in place, no re-mount); `children` is the\n * matched **child route** — render it wherever the nested route should appear\n * (the \"outlet\"). See ADR-0004.\n */\nexport interface RouteComponentProps {\n /** The router instance (for `navigate`/`select`). */\n router: Router\n /** Reactive accessor for the current (merged) path params. */\n params: () => Record<string, string>\n /** Reactive accessor for the current search params. */\n search: () => Record<string, unknown>\n /** Reactive accessor for this route's loader state (`idle` when it has no loader). */\n data: () => LoaderData\n /** The matched child route (the outlet); render it to show nested routes. */\n children: MindeesNode\n}\n\n/** A route: a path pattern, an optional component, optional search schema, and optional children. */\nexport interface RouteRecord {\n /**\n * The path pattern. At the top level this is absolute (`/posts/:postId`); a\n * nested route's path is **relative to its parent** (`settings`), and a child\n * with `''` or `'/'` is the parent's **index** route.\n */\n path: string\n /** The component to render for this route. A component-less route passes its child through. */\n component?: Component<RouteComponentProps>\n /**\n * A Standard Schema validating this route's search params. Its **output must\n * be object-shaped** (search params are a record), so a non-object schema like\n * `z.string()` is rejected at compile time and validated results need no cast.\n *\n * Note: only the **matched leaf** route's `searchSchema` is applied — a schema\n * on a parent/layout route is currently not used (search is global to the URL;\n * the leaf governs it). Per-route end-to-end `InferOutput` typing through\n * `Router.search()` arrives with the typed route registry (a later phase).\n */\n searchSchema?: StandardSchemaV1<unknown, Record<string, unknown>>\n /** Data loader for this route (sync or async); result is exposed via `data()`. */\n loader?: LoaderFn\n /** Declares which inputs key this route's loader cache (e.g. specific search params). */\n loaderDeps?: LoaderDepsFn\n /** Stale-while-revalidate window in ms; within it a successful load is reused. */\n staleTime?: number\n /** Nested child routes (their `path` is relative to this route). */\n children?: readonly RouteRecord[]\n /** Arbitrary route metadata. */\n meta?: Readonly<Record<string, unknown>>\n}\n\n/** The result of matching a location against the route table. */\nexport interface RouteMatch {\n /** The matched route record. */\n route: RouteRecord\n /** The matched pathname. */\n pathname: string\n /** Path params extracted from the pattern. */\n params: Record<string, string>\n /** Search params — validated output when a schema is present, else the raw parse. */\n search: Record<string, unknown>\n /** The raw parsed query, before schema validation. */\n searchRaw: Record<string, string | string[]>\n /** Search-validation issues, present only when validation failed. */\n issues?: ReadonlyArray<StandardSchemaV1.Issue>\n}\n\n/** The router's reactive state — a snapshot read through fine-grained signals. */\nexport interface RouterState {\n /** The current location. */\n location: RouterLocation\n /**\n * The matched route **chain**, root → leaf (one entry per nesting level), or\n * empty when nothing matched. Drives nested rendering ({@link createRouterView}).\n */\n matches: readonly RouteMatch[]\n /** The matched **leaf** route, or `null` if nothing matched. */\n match: RouteMatch | null\n /** Convenience: the current pathname. */\n pathname: string\n /** Convenience: the current path params (`{}` when unmatched). */\n params: Record<string, string>\n /** Convenience: the current search params (`{}` when unmatched). */\n search: Record<string, unknown>\n}\n\n// ---------------------------------------------------------------------------\n// Navigation types — typed targets\n// ---------------------------------------------------------------------------\n\n/** Options that apply to any navigation. */\nexport interface NavigateOptions {\n /** Replace the current history entry instead of pushing a new one. */\n replace?: boolean\n /** Navigate even if the target equals the current location (skips the idempotent no-op). */\n force?: boolean\n /**\n * Wrap the update in `document.startViewTransition` when available (web only).\n * Overrides the router-level `viewTransitions` default. No-op outside a DOM.\n */\n viewTransition?: boolean\n}\n\n/**\n * A navigation guard. Run before each navigation with the target and current\n * hrefs. Return `false` to cancel, a string to redirect, or nothing to proceed.\n */\nexport type BeforeNavigate = (to: string, from: string) => boolean | string | undefined\n\n/** The params/search/hash carried by a structured target, with params required iff the pattern has them. */\ntype NavExtras<P extends string> = {\n /** Search params to serialize into the query string. */\n search?: Record<string, QueryValue>\n /** A hash fragment (with or without a leading `#`). */\n hash?: string\n} & (HasPathParams<P> extends true ? { params: PathParams<P> } : { params?: Record<string, never> })\n\n/**\n * A fully-typed structured navigation target. `to` is a path pattern; `params`\n * is **required** when the pattern has dynamic segments and forbidden otherwise\n * — inferred from `to` with zero codegen.\n *\n * The param requirement is enforced when `to` is a **string literal**. If `to`\n * is a widened `string` (e.g. read from a variable typed `string`), its segments\n * can't be inferred, so `params` is not type-checked — a missing required param\n * then throws {@link RouterError} (`MISSING_PARAM`) at runtime.\n *\n * @example\n * router.navigate({ to: '/posts/:postId', params: { postId: '42' } })\n * router.navigate({ to: '/about' }) // no params allowed\n */\nexport type NavTarget<P extends string> = { to: P } & NavExtras<P>\n\n/** The broad runtime shape `navigate` accepts (the typed surface is {@link NavTarget}). */\ninterface NavTargetInput {\n to: string\n params?: Record<string, string | number>\n search?: Record<string, QueryValue>\n hash?: string\n}\n\n// ---------------------------------------------------------------------------\n// Router\n// ---------------------------------------------------------------------------\n\n/** Options for {@link createRouter}. */\nexport interface CreateRouterOptions {\n /** The route table. Order is irrelevant — routes are matched most-specific first. */\n routes: readonly RouteRecord[]\n /** The history adapter. Defaults to an in-memory history at `/`. */\n history?: RouterHistory\n /** A guard run before each navigation (cancel with `false`, redirect with a string). */\n beforeNavigate?: BeforeNavigate\n /** Wrap navigations in `document.startViewTransition` by default (web only). */\n viewTransitions?: boolean\n}\n\n/** A live router instance. */\nexport interface Router {\n /** The full reactive state snapshot. */\n state(): RouterState\n /** The current location. */\n location(): RouterLocation\n /** The matched route chain (root → leaf), or empty when unmatched. */\n matches(): readonly RouteMatch[]\n /** The current leaf match, or `null`. */\n match(): RouteMatch | null\n /** The current path params (`{}` when unmatched). */\n params(): Record<string, string>\n /** The current search params (`{}` when unmatched). */\n search(): Record<string, unknown>\n /**\n * Subscribe to a derived slice of router state with re-render isolation. The\n * returned accessor (a memo) only changes when `selector(state)` changes under\n * `equals` (default `Object.is`) — the same selector-isolation technique as\n * core's Phase 2 `createProvider` (a computed memo over an `equals:false`\n * source), applied to route state.\n */\n select<S>(selector: (state: RouterState) => S, equals?: (a: S, b: S) => boolean): () => S\n /** Navigate to a typed target or a (possibly relative) href string. */\n navigate<P extends string>(target: string | NavTarget<P>, options?: NavigateOptions): void\n /** Reactively read a match's loader state (`idle` when the route has no loader). */\n loaderData(match: RouteMatch): LoaderData\n /** Re-run the current chain's loaders (marks their cached data stale first). */\n invalidate(): void\n /**\n * Run a target href's loaders WITHOUT navigating (intent prefetch). A\n * still-in-flight preload is aborted if you then navigate to a *different*\n * route; once it settles it warms the cache for that route's next visit\n * (subject to `staleTime`).\n */\n preload(to: string): void\n /**\n * Replace the route table and re-match the current location **in place** — the\n * location is preserved (dynamic reconfiguration without state reset).\n */\n setRoutes(routes: readonly RouteRecord[]): void\n /**\n * The active route table, as provided to {@link createRouter} / {@link Router.setRoutes}\n * (the nested tree, in insertion order). Matching internally flattens the tree\n * and orders leaves by specificity (static > dynamic > catch-all); that\n * precedence is an implementation detail, not the shape returned here.\n */\n routes(): readonly RouteRecord[]\n /** The underlying history adapter. */\n readonly history: RouterHistory\n /** Tear down the router's reactive scope and history subscription. */\n dispose(): void\n}\n\nconst EMPTY_PARAMS: Record<string, string> = Object.freeze({})\nconst EMPTY_SEARCH: Record<string, unknown> = Object.freeze({})\nconst EMPTY_MATCHES: readonly RouteMatch[] = Object.freeze([])\n\n/** A leaf route flattened from the tree: its full path and its root→leaf chain. */\ninterface FlatRoute {\n fullPath: string\n chain: readonly RouteRecord[]\n}\n\n/** Join a parent path and a (relative) child path into a normalized full path. */\nfunction joinPaths(parent: string, child: string): string {\n const base = parent.endsWith('/') ? parent.slice(0, -1) : parent\n const rel = child.startsWith('/') ? child.slice(1) : child\n if (rel.length === 0) return base.length === 0 ? '/' : base\n return `${base}/${rel}`\n}\n\n/**\n * Flatten a (possibly nested) route tree into leaf entries, each carrying its\n * full path and the root→leaf chain of records. A route with children\n * contributes only via its children (add an index child — `path: ''` — to match\n * the parent's own path).\n */\nfunction flattenRouteTree(\n routes: readonly RouteRecord[],\n parentPath: string,\n parentChain: readonly RouteRecord[],\n): FlatRoute[] {\n const out: FlatRoute[] = []\n for (const route of routes) {\n const fullPath = joinPaths(parentPath, route.path)\n const chain = [...parentChain, route]\n if (route.children && route.children.length > 0) {\n out.push(...flattenRouteTree(route.children, fullPath, chain))\n } else {\n out.push({ fullPath, chain })\n }\n }\n return out\n}\n\n/** Flatten + sort a route tree most-specific first (static > dynamic > catch-all). */\nfunction compileRoutes(routes: readonly RouteRecord[]): FlatRoute[] {\n return flattenRouteTree(routes, '', []).sort((a, b) => compareSpecificity(a.fullPath, b.fullPath))\n}\n\n/**\n * Match a location against the compiled route table, returning the matched chain\n * (root → leaf), or an empty array if nothing matched. Search is validated\n * against the **leaf** route's schema and shared across the chain.\n */\nfunction matchLocation(\n flat: readonly FlatRoute[],\n location: RouterLocation,\n): readonly RouteMatch[] {\n for (const fr of flat) {\n const params = matchPattern(fr.fullPath, location.pathname)\n if (params === null) continue\n\n const searchRaw = parseQuery(location.search)\n let search: Record<string, unknown> = searchRaw\n let issues: ReadonlyArray<StandardSchemaV1.Issue> | undefined\n\n const leaf = fr.chain[fr.chain.length - 1]\n if (leaf?.searchSchema) {\n // searchSchema's output is constrained to Record<string, unknown>, so the\n // validated value is already correctly typed — no cast needed.\n try {\n const result = safeValidateSearch(leaf.searchSchema, searchRaw)\n if (result.ok) search = result.value\n else issues = result.issues\n } catch (err) {\n // safeValidateSearch THROWS on an async schema (ASYNC_SCHEMA), and a\n // schema could throw synchronously too. This runs inside matchMemo\n // (a computed read by stateMemo AND the loader effect), so an escaping\n // throw would poison every router-state accessor and wedge navigation.\n // Contain it: degrade to raw search and surface it via match.issues,\n // exactly like the invalid-input path.\n issues = [{ message: err instanceof Error ? err.message : 'search validation failed' }]\n }\n }\n\n return fr.chain.map((route) => {\n const base: RouteMatch = { route, pathname: location.pathname, params, search, searchRaw }\n return issues ? { ...base, issues } : base\n })\n }\n return EMPTY_MATCHES\n}\n\n/**\n * Resolve a (possibly relative) path against a base pathname. Absolute paths\n * (leading `/`) ignore the base; `.`/`..` segments are applied against it,\n * treating the base pathname as a directory.\n *\n * @example\n * resolvePath('/a/b', '/x') // '/a/b'\n * resolvePath('edit', '/posts/1') // '/posts/1/edit'\n * resolvePath('../', '/posts/1') // '/posts'\n */\nexport function resolvePath(to: string, from: string): string {\n const stack = to.startsWith('/') ? [] : from.split('/').filter((s) => s.length > 0)\n for (const seg of to.split('/')) {\n if (seg === '' || seg === '.') continue\n if (seg === '..') stack.pop()\n else stack.push(seg)\n }\n return `/${stack.join('/')}`\n}\n\n/** Resolve an href string (which may be relative and carry query/hash) against a location. */\nfunction resolveHref(to: string, from: RouterLocation): string {\n const hasPath = to.length > 0 && to[0] !== '?' && to[0] !== '#'\n let rest = to\n let hash = ''\n const hashIndex = rest.indexOf('#')\n if (hashIndex !== -1) {\n hash = rest.slice(hashIndex)\n rest = rest.slice(0, hashIndex)\n }\n let search = ''\n const queryIndex = rest.indexOf('?')\n if (queryIndex !== -1) {\n search = rest.slice(queryIndex)\n rest = rest.slice(0, queryIndex)\n }\n const pathname = hasPath ? resolvePath(rest, from.pathname) : from.pathname\n // RFC 3986: a fragment-only reference (no path, no query) keeps the current\n // path AND query, replacing only the fragment — so a `#anchor` navigation must\n // not drop the active search params.\n const finalSearch = !hasPath && queryIndex === -1 ? from.search : search\n return `${pathname}${finalSearch}${hash}`\n}\n\n/** Build an href from a structured navigation target. */\nfunction buildHref(target: NavTargetInput): string {\n const pathname = buildPath(target.to, target.params ?? {})\n const query = target.search ? stringifyQuery(target.search) : ''\n let hash = target.hash ?? ''\n if (hash.length > 0 && !hash.startsWith('#')) hash = `#${hash}`\n return `${pathname}${query ? `?${query}` : ''}${hash}`\n}\n\n/**\n * Create a router over a route table. State is reactive (signals); call\n * {@link Router.dispose} to tear it down.\n */\nexport function createRouter(options: CreateRouterOptions): Router {\n const history = options.history ?? createMemoryHistory()\n\n let routesSig!: Signal<readonly RouteRecord[]>\n let flatMemo!: Memo<FlatRoute[]>\n let locationSig!: Signal<RouterLocation>\n let stateMemo!: Memo<RouterState>\n let loaders!: LoaderManager\n\n const dispose = createRoot((disposeRoot) => {\n routesSig = signal<readonly RouteRecord[]>(options.routes, { equals: false })\n flatMemo = computed(() => compileRoutes(routesSig()))\n locationSig = signal<RouterLocation>(history.location(), { equals: false })\n const matchMemo = computed(() => matchLocation(flatMemo(), locationSig()))\n stateMemo = computed<RouterState>(() => {\n const location = locationSig()\n const matches = matchMemo()\n const leaf = matches.length > 0 ? (matches[matches.length - 1] ?? null) : null\n return {\n location,\n matches,\n match: leaf,\n pathname: location.pathname,\n params: leaf ? leaf.params : EMPTY_PARAMS,\n search: leaf ? leaf.search : EMPTY_SEARCH,\n }\n })\n\n // Reactive data version: bumped on any loader-cache change; loader reads\n // subscribe to it so a component's data binding updates when its load\n // resolves (without re-mounting the component).\n const dataVersion = signal(0, { equals: false })\n loaders = createLoaderManager({\n location: () => locationSig(),\n onChange: () => dataVersion.set(0),\n track: () => {\n dataVersion()\n },\n })\n // Orchestrate loaders on every navigation. Owned by the router root (not a\n // re-running region), and it never reads `dataVersion` — so no loops.\n effect(() => loaders.sync(matchMemo()))\n\n const unsubscribe = history.subscribe((loc) => locationSig.set(loc))\n return () => {\n unsubscribe()\n loaders.dispose()\n disposeRoot()\n }\n })\n\n /** Apply the navigation guard, following redirects (capped). Returns the final href, or null to cancel. */\n const applyGuard = (href: string): string | null => {\n const guard = options.beforeNavigate\n if (!guard) return href\n let current = href\n for (let i = 0; i < 10; i++) {\n const result = guard(current, createHref(locationSig()))\n if (result === false) return null\n if (typeof result === 'string') {\n const next = resolveHref(result, locationSig())\n if (next === current) return current\n current = next\n continue\n }\n return current\n }\n // Redirect cap exceeded (a guard that keeps redirecting): cancel rather than\n // commit a location the guard never approved.\n return null\n }\n\n const navigate = (target: string | NavTargetInput, opts?: NavigateOptions): void => {\n const initial =\n typeof target === 'string' ? resolveHref(target, locationSig()) : buildHref(target)\n const href = applyGuard(initial)\n if (href === null) return\n // Idempotent: navigating to the current location is a no-op unless forced.\n if (opts?.force !== true && href === createHref(locationSig())) return\n\n const commit = (): void => {\n if (opts?.replace) history.replace(href)\n else history.push(href)\n }\n const wantsTransition = opts?.viewTransition ?? options.viewTransitions ?? false\n if (wantsTransition) startViewTransition(commit)\n else commit()\n }\n\n const preload = (to: string): void => {\n const href = resolveHref(to, locationSig())\n loaders.preload(matchLocation(flatMemo(), parseHref(href)))\n }\n\n const router: Router = {\n state: () => stateMemo(),\n location: () => locationSig(),\n matches: () => stateMemo().matches,\n match: () => stateMemo().match,\n params: () => stateMemo().params,\n search: () => stateMemo().search,\n select: <S>(selector: (state: RouterState) => S, equals: (a: S, b: S) => boolean = Object.is) =>\n computed(() => selector(stateMemo()), { equals }),\n navigate: navigate as Router['navigate'],\n loaderData: (match) => loaders.read(match),\n invalidate: () => loaders.invalidate(stateMemo().matches),\n preload,\n setRoutes: (routes) => routesSig.set(routes),\n routes: () => routesSig(),\n history,\n dispose,\n }\n // Register as the active router so hooks (useRouter, …) and the bound Link resolve it.\n setActiveRouter(router)\n return router\n}\n\n/** A document that may support the View Transitions API. */\ninterface ViewTransitionDocument {\n startViewTransition?: (callback: () => void) => unknown\n}\n\n/** The subset of a ViewTransition we touch (its eagerly-created promises). */\ninterface ViewTransitionLike {\n ready?: Promise<unknown>\n updateCallbackDone?: Promise<unknown>\n}\n\n/**\n * Run `commit` inside `document.startViewTransition` when available (web only),\n * else run it directly. The signals re-render is synchronous, so it happens\n * inside the transition. No-op-wrapping outside a DOM (SSR, native, tests).\n *\n * View transitions are a progressive enhancement, so this must NEVER throw out of\n * navigate() or leak an unhandled rejection: (1) a rapid second navigation aborts\n * the first transition and rejects its eagerly-created `ready`/`updateCallbackDone`\n * promises — we mark those handled; (2) some browsers throw synchronously (e.g. a\n * hidden/background document) — we fall back to a plain commit so the navigation\n * still lands (without committing twice).\n */\nfunction startViewTransition(commit: () => void): void {\n const doc =\n typeof document === 'undefined' ? undefined : (document as unknown as ViewTransitionDocument)\n if (!doc?.startViewTransition) {\n commit()\n return\n }\n let committed = false\n const runCommit = (): void => {\n committed = true\n commit()\n }\n try {\n const transition = doc.startViewTransition(runCommit) as ViewTransitionLike | undefined\n void transition?.ready?.catch(() => {})\n void transition?.updateCallbackDone?.catch(() => {})\n } catch {\n if (!committed) commit()\n }\n}\n\nexport { createHref }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"router.js","names":[],"sources":["../src/router.ts"],"sourcesContent":["/**\n * The Quantum router — signals-native routing state with typed, validated\n * navigation and re-render isolation.\n *\n * Router state (location, params, search, matched route) is modeled as the\n * fine-grained signal graph from `@mindees/core` (Phase 1 `signal`/`computed`,\n * Phase 2 selector isolation). Consumers read a slice via {@link Router.select}\n * and re-run **only** when that slice changes — no whole-tree re-render on\n * navigation, no global-vs-local hook trap (cf. Expo Router). See ADR-0003.\n *\n * @module\n */\n\nimport {\n type Component,\n computed,\n createRoot,\n effect,\n type Memo,\n type MindeesNode,\n type Signal,\n signal,\n} from '@mindees/core'\nimport { clearActiveRouter, setActiveRouter } from './active'\nimport {\n createLoaderManager,\n type LoaderData,\n type LoaderDepsFn,\n type LoaderFn,\n type LoaderManager,\n} from './data'\nimport {\n createHref,\n createMemoryHistory,\n parseHref,\n type RouterHistory,\n type RouterLocation,\n} from './history'\nimport {\n buildPath,\n compareSpecificity,\n type HasPathParams,\n matchPattern,\n type PathParams,\n parsePattern,\n} from './pattern'\nimport { parseQuery, type QueryValue, safeValidateSearch, stringifyQuery } from './search'\nimport type { StandardSchemaV1 } from './standard-schema'\n\n// ---------------------------------------------------------------------------\n// Route table types\n// ---------------------------------------------------------------------------\n\n/**\n * Props a route's component receives from {@link createRouterView}. `params` and\n * `search` are **reactive accessors** (read them in a reactive scope so a\n * same-route param change updates in place, no re-mount); `children` is the\n * matched **child route** — render it wherever the nested route should appear\n * (the \"outlet\"). See ADR-0004.\n */\nexport interface RouteComponentProps {\n /** The router instance (for `navigate`/`select`). */\n router: Router\n /** Reactive accessor for the current (merged) path params. */\n params: () => Record<string, string>\n /** Reactive accessor for the current search params. */\n search: () => Record<string, unknown>\n /** Reactive accessor for this route's loader state (`idle` when it has no loader). */\n data: () => LoaderData\n /** The matched child route (the outlet); render it to show nested routes. */\n children: MindeesNode\n}\n\n/** A route: a path pattern, an optional component, optional search schema, and optional children. */\nexport interface RouteRecord {\n /**\n * The path pattern. At the top level this is absolute (`/posts/:postId`); a\n * nested route's path is **relative to its parent** (`settings`), and a child\n * with `''` or `'/'` is the parent's **index** route.\n */\n path: string\n /** The component to render for this route. A component-less route passes its child through. */\n component?: Component<RouteComponentProps>\n /**\n * A Standard Schema validating this route's search params. Its **output must\n * be object-shaped** (search params are a record), so a non-object schema like\n * `z.string()` is rejected at compile time and validated results need no cast.\n *\n * Note: only the **matched leaf** route's `searchSchema` is applied — a schema\n * on a parent/layout route is currently not used (search is global to the URL;\n * the leaf governs it). Per-route end-to-end `InferOutput` typing through\n * `Router.search()` arrives with the typed route registry (a later phase).\n */\n searchSchema?: StandardSchemaV1<unknown, Record<string, unknown>>\n /** Data loader for this route (sync or async); result is exposed via `data()`. */\n loader?: LoaderFn\n /** Declares which inputs key this route's loader cache (e.g. specific search params). */\n loaderDeps?: LoaderDepsFn\n /** Stale-while-revalidate window in ms; within it a successful load is reused. */\n staleTime?: number\n /** Nested child routes (their `path` is relative to this route). */\n children?: readonly RouteRecord[]\n /** Arbitrary route metadata. */\n meta?: Readonly<Record<string, unknown>>\n}\n\n/** The result of matching a location against the route table. */\nexport interface RouteMatch {\n /** The matched route record. */\n route: RouteRecord\n /** The matched pathname. */\n pathname: string\n /** Path params extracted from the pattern. */\n params: Record<string, string>\n /** Search params — validated output when a schema is present, else the raw parse. */\n search: Record<string, unknown>\n /** The raw parsed query, before schema validation. */\n searchRaw: Record<string, string | string[]>\n /** Search-validation issues, present only when validation failed. */\n issues?: ReadonlyArray<StandardSchemaV1.Issue>\n}\n\n/** The router's reactive state — a snapshot read through fine-grained signals. */\nexport interface RouterState {\n /** The current location. */\n location: RouterLocation\n /**\n * The matched route **chain**, root → leaf (one entry per nesting level), or\n * empty when nothing matched. Drives nested rendering ({@link createRouterView}).\n */\n matches: readonly RouteMatch[]\n /** The matched **leaf** route, or `null` if nothing matched. */\n match: RouteMatch | null\n /** Convenience: the current pathname. */\n pathname: string\n /** Convenience: the current path params (`{}` when unmatched). */\n params: Record<string, string>\n /** Convenience: the current search params (`{}` when unmatched). */\n search: Record<string, unknown>\n}\n\n// ---------------------------------------------------------------------------\n// Navigation types — typed targets\n// ---------------------------------------------------------------------------\n\n/** Options that apply to any navigation. */\nexport interface NavigateOptions {\n /** Replace the current history entry instead of pushing a new one. */\n replace?: boolean\n /** Navigate even if the target equals the current location (skips the idempotent no-op). */\n force?: boolean\n /**\n * Wrap the update in `document.startViewTransition` when available (web only).\n * Overrides the router-level `viewTransitions` default. No-op outside a DOM.\n */\n viewTransition?: boolean\n}\n\n/**\n * A navigation guard. Run before each navigation with the target and current\n * hrefs. Return `false` to cancel, a string to redirect, or nothing to proceed.\n */\nexport type BeforeNavigate = (to: string, from: string) => boolean | string | undefined\n\n/** The params/search/hash carried by a structured target, with params required iff the pattern has them. */\ntype NavExtras<P extends string> = {\n /** Search params to serialize into the query string. */\n search?: Record<string, QueryValue>\n /** A hash fragment (with or without a leading `#`). */\n hash?: string\n} & (HasPathParams<P> extends true ? { params: PathParams<P> } : { params?: Record<string, never> })\n\n/**\n * A fully-typed structured navigation target. `to` is a path pattern; `params`\n * is **required** when the pattern has dynamic segments and forbidden otherwise\n * — inferred from `to` with zero codegen.\n *\n * The param requirement is enforced when `to` is a **string literal**. If `to`\n * is a widened `string` (e.g. read from a variable typed `string`), its segments\n * can't be inferred, so `params` is not type-checked — a missing required param\n * then throws {@link RouterError} (`MISSING_PARAM`) at runtime.\n *\n * @example\n * router.navigate({ to: '/posts/:postId', params: { postId: '42' } })\n * router.navigate({ to: '/about' }) // no params allowed\n */\nexport type NavTarget<P extends string> = { to: P } & NavExtras<P>\n\n/** The broad runtime shape `navigate` accepts (the typed surface is {@link NavTarget}). */\ninterface NavTargetInput {\n to: string\n params?: Record<string, string | number>\n search?: Record<string, QueryValue>\n hash?: string\n}\n\n// ---------------------------------------------------------------------------\n// Router\n// ---------------------------------------------------------------------------\n\n/** Options for {@link createRouter}. */\nexport interface CreateRouterOptions {\n /** The route table. Order is irrelevant — routes are matched most-specific first. */\n routes: readonly RouteRecord[]\n /** The history adapter. Defaults to an in-memory history at `/`. */\n history?: RouterHistory\n /** A guard run before each navigation (cancel with `false`, redirect with a string). */\n beforeNavigate?: BeforeNavigate\n /** Wrap navigations in `document.startViewTransition` by default (web only). */\n viewTransitions?: boolean\n}\n\n/** A live router instance. */\nexport interface Router {\n /** The full reactive state snapshot. */\n state(): RouterState\n /** The current location. */\n location(): RouterLocation\n /** The matched route chain (root → leaf), or empty when unmatched. */\n matches(): readonly RouteMatch[]\n /** The current leaf match, or `null`. */\n match(): RouteMatch | null\n /** The current path params (`{}` when unmatched). */\n params(): Record<string, string>\n /** The current search params (`{}` when unmatched). */\n search(): Record<string, unknown>\n /**\n * Subscribe to a derived slice of router state with re-render isolation. The\n * returned accessor (a memo) only changes when `selector(state)` changes under\n * `equals` (default `Object.is`) — the same selector-isolation technique as\n * core's Phase 2 `createProvider` (a computed memo over an `equals:false`\n * source), applied to route state.\n */\n select<S>(selector: (state: RouterState) => S, equals?: (a: S, b: S) => boolean): () => S\n /** Navigate to a typed target or a (possibly relative) href string. */\n navigate<P extends string>(target: string | NavTarget<P>, options?: NavigateOptions): void\n /** Reactively read a match's loader state (`idle` when the route has no loader). */\n loaderData(match: RouteMatch): LoaderData\n /** Re-run the current chain's loaders (marks their cached data stale first). */\n invalidate(): void\n /**\n * Run a target href's loaders WITHOUT navigating (intent prefetch). A\n * still-in-flight preload is aborted if you then navigate to a *different*\n * route; once it settles it warms the cache for that route's next visit\n * (subject to `staleTime`).\n */\n preload(to: string): void\n /**\n * Replace the route table and re-match the current location **in place** — the\n * location is preserved (dynamic reconfiguration without state reset).\n */\n setRoutes(routes: readonly RouteRecord[]): void\n /**\n * The active route table, as provided to {@link createRouter} / {@link Router.setRoutes}\n * (the nested tree, in insertion order). Matching internally flattens the tree\n * and orders leaves by specificity (static > dynamic > catch-all); that\n * precedence is an implementation detail, not the shape returned here.\n */\n routes(): readonly RouteRecord[]\n /** The underlying history adapter. */\n readonly history: RouterHistory\n /** Tear down the router's reactive scope and history subscription. */\n dispose(): void\n}\n\nconst EMPTY_PARAMS: Record<string, string> = Object.freeze({})\nconst EMPTY_SEARCH: Record<string, unknown> = Object.freeze({})\nconst EMPTY_MATCHES: readonly RouteMatch[] = Object.freeze([])\n\n/** A leaf route flattened from the tree: its full path and its root→leaf chain. */\ninterface FlatRoute {\n fullPath: string\n chain: readonly RouteRecord[]\n}\n\n/** Join a parent path and a (relative) child path into a normalized full path. */\nfunction joinPaths(parent: string, child: string): string {\n const base = parent.endsWith('/') ? parent.slice(0, -1) : parent\n const rel = child.startsWith('/') ? child.slice(1) : child\n if (rel.length === 0) return base.length === 0 ? '/' : base\n return `${base}/${rel}`\n}\n\n/**\n * Flatten a (possibly nested) route tree into leaf entries, each carrying its\n * full path and the root→leaf chain of records. A route with children\n * contributes only via its children (add an index child — `path: ''` — to match\n * the parent's own path).\n */\nfunction flattenRouteTree(\n routes: readonly RouteRecord[],\n parentPath: string,\n parentChain: readonly RouteRecord[],\n): FlatRoute[] {\n const out: FlatRoute[] = []\n for (const route of routes) {\n const fullPath = joinPaths(parentPath, route.path)\n const chain = [...parentChain, route]\n if (route.children && route.children.length > 0) {\n out.push(...flattenRouteTree(route.children, fullPath, chain))\n } else {\n out.push({ fullPath, chain })\n }\n }\n return out\n}\n\n/** Dev-only warning (silent in production). */\nfunction warnDev(message: string): void {\n const g = globalThis as {\n process?: { env?: Record<string, string | undefined> }\n console?: { warn?: (message: string) => void }\n }\n if (g.process?.env?.NODE_ENV === 'production') return\n g.console?.warn?.(`[router] ${message}`)\n}\n\n/** Shallow record equality — params/search only \"change\" when an entry actually changes. */\nfunction shallowEqualRecord(a: Record<string, unknown>, b: Record<string, unknown>): boolean {\n if (a === b) return true\n const ak = Object.keys(a)\n if (ak.length !== Object.keys(b).length) return false\n for (const k of ak) if (a[k] !== b[k]) return false\n return true\n}\n\n/**\n * Flatten + sort a route tree most-specific first (static > dynamic > catch-all). Routes whose\n * synthesized pattern is invalid (a catch-all parent with children would place a catch-all\n * segment before a literal one) are dropped with a dev warning rather than thrown — one bad\n * route must not poison all router state.\n */\nfunction compileRoutes(routes: readonly RouteRecord[]): FlatRoute[] {\n return flattenRouteTree(routes, '', [])\n .filter((fr) => {\n try {\n parsePattern(fr.fullPath)\n return true\n } catch (error) {\n warnDev(`ignoring invalid route pattern ${fr.fullPath}: ${(error as Error).message}`)\n return false\n }\n })\n .sort((a, b) => compareSpecificity(a.fullPath, b.fullPath))\n}\n\n/**\n * Match a location against the compiled route table, returning the matched chain\n * (root → leaf), or an empty array if nothing matched. Search is validated\n * against the **leaf** route's schema and shared across the chain.\n */\nfunction matchLocation(\n flat: readonly FlatRoute[],\n location: RouterLocation,\n): readonly RouteMatch[] {\n for (const fr of flat) {\n const params = matchPattern(fr.fullPath, location.pathname)\n if (params === null) continue\n\n const searchRaw = parseQuery(location.search)\n let search: Record<string, unknown> = searchRaw\n let issues: ReadonlyArray<StandardSchemaV1.Issue> | undefined\n\n const leaf = fr.chain[fr.chain.length - 1]\n if (leaf?.searchSchema) {\n // searchSchema's output is constrained to Record<string, unknown>, so the\n // validated value is already correctly typed — no cast needed.\n try {\n const result = safeValidateSearch(leaf.searchSchema, searchRaw)\n if (result.ok) search = result.value\n else issues = result.issues\n } catch (err) {\n // safeValidateSearch THROWS on an async schema (ASYNC_SCHEMA), and a\n // schema could throw synchronously too. This runs inside matchMemo\n // (a computed read by stateMemo AND the loader effect), so an escaping\n // throw would poison every router-state accessor and wedge navigation.\n // Contain it: degrade to raw search and surface it via match.issues,\n // exactly like the invalid-input path.\n issues = [{ message: err instanceof Error ? err.message : 'search validation failed' }]\n }\n }\n\n return fr.chain.map((route) => {\n const base: RouteMatch = { route, pathname: location.pathname, params, search, searchRaw }\n return issues ? { ...base, issues } : base\n })\n }\n return EMPTY_MATCHES\n}\n\n/**\n * Resolve a (possibly relative) path against a base pathname. Absolute paths\n * (leading `/`) ignore the base; `.`/`..` segments are applied against it,\n * treating the base pathname as a directory.\n *\n * @example\n * resolvePath('/a/b', '/x') // '/a/b'\n * resolvePath('edit', '/posts/1') // '/posts/1/edit'\n * resolvePath('../', '/posts/1') // '/posts'\n */\nexport function resolvePath(to: string, from: string): string {\n const stack = to.startsWith('/') ? [] : from.split('/').filter((s) => s.length > 0)\n for (const seg of to.split('/')) {\n if (seg === '' || seg === '.') continue\n if (seg === '..') stack.pop()\n else stack.push(seg)\n }\n return `/${stack.join('/')}`\n}\n\n/** Resolve an href string (which may be relative and carry query/hash) against a location. */\nfunction resolveHref(to: string, from: RouterLocation): string {\n const hasPath = to.length > 0 && to[0] !== '?' && to[0] !== '#'\n let rest = to\n let hash = ''\n const hashIndex = rest.indexOf('#')\n if (hashIndex !== -1) {\n hash = rest.slice(hashIndex)\n rest = rest.slice(0, hashIndex)\n }\n let search = ''\n const queryIndex = rest.indexOf('?')\n if (queryIndex !== -1) {\n search = rest.slice(queryIndex)\n rest = rest.slice(0, queryIndex)\n }\n const pathname = hasPath ? resolvePath(rest, from.pathname) : from.pathname\n // RFC 3986: a fragment-only reference (no path, no query) keeps the current\n // path AND query, replacing only the fragment — so a `#anchor` navigation must\n // not drop the active search params.\n const finalSearch = !hasPath && queryIndex === -1 ? from.search : search\n return `${pathname}${finalSearch}${hash}`\n}\n\n/** Build an href from a structured navigation target. */\nfunction buildHref(target: NavTargetInput): string {\n const pathname = buildPath(target.to, target.params ?? {})\n const query = target.search ? stringifyQuery(target.search) : ''\n let hash = target.hash ?? ''\n if (hash.length > 0 && !hash.startsWith('#')) hash = `#${hash}`\n return `${pathname}${query ? `?${query}` : ''}${hash}`\n}\n\n/**\n * Create a router over a route table. State is reactive (signals); call\n * {@link Router.dispose} to tear it down.\n */\nexport function createRouter(options: CreateRouterOptions): Router {\n const history = options.history ?? createMemoryHistory()\n\n let routesSig!: Signal<readonly RouteRecord[]>\n let flatMemo!: Memo<FlatRoute[]>\n let locationSig!: Signal<RouterLocation>\n let stateMemo!: Memo<RouterState>\n let paramsMemo!: Memo<Record<string, string>>\n let searchMemo!: Memo<Record<string, unknown>>\n let loaders!: LoaderManager\n\n const dispose = createRoot((disposeRoot) => {\n routesSig = signal<readonly RouteRecord[]>(options.routes, { equals: false })\n flatMemo = computed(() => compileRoutes(routesSig()))\n locationSig = signal<RouterLocation>(history.location(), { equals: false })\n const matchMemo = computed(() => matchLocation(flatMemo(), locationSig()))\n stateMemo = computed<RouterState>(() => {\n const location = locationSig()\n const matches = matchMemo()\n const leaf = matches.length > 0 ? (matches[matches.length - 1] ?? null) : null\n return {\n location,\n matches,\n match: leaf,\n pathname: location.pathname,\n params: leaf ? leaf.params : EMPTY_PARAMS,\n search: leaf ? leaf.search : EMPTY_SEARCH,\n }\n })\n // Re-render isolation: params()/search() only emit when an entry actually changes,\n // not on every navigation (stateMemo returns a fresh object each time).\n paramsMemo = computed(() => stateMemo().params, { equals: shallowEqualRecord })\n searchMemo = computed(() => stateMemo().search, { equals: shallowEqualRecord })\n\n // Reactive data version: bumped on any loader-cache change; loader reads\n // subscribe to it so a component's data binding updates when its load\n // resolves (without re-mounting the component).\n const dataVersion = signal(0, { equals: false })\n loaders = createLoaderManager({\n location: () => locationSig(),\n onChange: () => dataVersion.set(0),\n track: () => {\n dataVersion()\n },\n })\n // Orchestrate loaders on every navigation. Owned by the router root (not a\n // re-running region), and it never reads `dataVersion` — so no loops.\n effect(() => loaders.sync(matchMemo()))\n\n const unsubscribe = history.subscribe((loc) => locationSig.set(loc))\n return () => {\n unsubscribe()\n loaders.dispose()\n disposeRoot()\n }\n })\n\n /** Apply the navigation guard, following redirects (capped). Returns the final href, or null to cancel. */\n const applyGuard = (href: string): string | null => {\n const guard = options.beforeNavigate\n if (!guard) return href\n let current = href\n for (let i = 0; i < 10; i++) {\n const result = guard(current, createHref(locationSig()))\n if (result === false) return null\n if (typeof result === 'string') {\n const next = resolveHref(result, locationSig())\n if (next === current) return current\n current = next\n continue\n }\n return current\n }\n // Redirect cap exceeded (a guard that keeps redirecting): cancel rather than\n // commit a location the guard never approved.\n return null\n }\n\n const navigate = (target: string | NavTargetInput, opts?: NavigateOptions): void => {\n const initial =\n typeof target === 'string' ? resolveHref(target, locationSig()) : buildHref(target)\n const href = applyGuard(initial)\n if (href === null) return\n // Idempotent: navigating to the current location is a no-op unless forced.\n if (opts?.force !== true && href === createHref(locationSig())) return\n\n const commit = (): void => {\n if (opts?.replace) history.replace(href)\n else history.push(href)\n }\n const wantsTransition = opts?.viewTransition ?? options.viewTransitions ?? false\n if (wantsTransition) startViewTransition(commit)\n else commit()\n }\n\n const preload = (to: string): void => {\n const href = resolveHref(to, locationSig())\n loaders.preload(matchLocation(flatMemo(), parseHref(href)))\n }\n\n const router: Router = {\n state: () => stateMemo(),\n location: () => locationSig(),\n matches: () => stateMemo().matches,\n match: () => stateMemo().match,\n params: () => paramsMemo(),\n search: () => searchMemo(),\n select: <S>(selector: (state: RouterState) => S, equals: (a: S, b: S) => boolean = Object.is) =>\n computed(() => selector(stateMemo()), { equals }),\n navigate: navigate as Router['navigate'],\n loaderData: (match) => loaders.read(match),\n invalidate: () => loaders.invalidate(stateMemo().matches),\n preload,\n setRoutes: (routes) => routesSig.set(routes),\n routes: () => routesSig(),\n history,\n // Tear down reactivity AND drop the active-router registration, so a disposed\n // router no longer answers useRouter()/useParams()/Link.\n dispose: () => {\n dispose()\n clearActiveRouter(router)\n },\n }\n // Register as the active router so hooks (useRouter, …) and the bound Link resolve it.\n setActiveRouter(router)\n return router\n}\n\n/** A document that may support the View Transitions API. */\ninterface ViewTransitionDocument {\n startViewTransition?: (callback: () => void) => unknown\n}\n\n/** The subset of a ViewTransition we touch (its eagerly-created promises). */\ninterface ViewTransitionLike {\n ready?: Promise<unknown>\n updateCallbackDone?: Promise<unknown>\n}\n\n/**\n * Run `commit` inside `document.startViewTransition` when available (web only),\n * else run it directly. The signals re-render is synchronous, so it happens\n * inside the transition. No-op-wrapping outside a DOM (SSR, native, tests).\n *\n * View transitions are a progressive enhancement, so this must NEVER throw out of\n * navigate() or leak an unhandled rejection: (1) a rapid second navigation aborts\n * the first transition and rejects its eagerly-created `ready`/`updateCallbackDone`\n * promises — we mark those handled; (2) some browsers throw synchronously (e.g. a\n * hidden/background document) — we fall back to a plain commit so the navigation\n * still lands (without committing twice).\n */\nfunction startViewTransition(commit: () => void): void {\n const doc =\n typeof document === 'undefined' ? undefined : (document as unknown as ViewTransitionDocument)\n if (!doc?.startViewTransition) {\n commit()\n return\n }\n let committed = false\n const runCommit = (): void => {\n committed = true\n commit()\n }\n try {\n const transition = doc.startViewTransition(runCommit) as ViewTransitionLike | undefined\n void transition?.ready?.catch(() => {})\n void transition?.updateCallbackDone?.catch(() => {})\n } catch {\n if (!committed) commit()\n }\n}\n\nexport { createHref }\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindees/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Quantum — the typed, signals-native router for MindeesNative: codegen-free typed path params, Standard Schema validated search params, and selector-isolated reactive route state.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"router",
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
"directory": "packages/router"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@mindees/core": "0.
|
|
38
|
+
"@mindees/core": "0.4.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"happy-dom": "20.9.0",
|
|
42
42
|
"valibot": "1.4.1",
|
|
43
43
|
"zod": "4.4.3",
|
|
44
|
-
"@mindees/renderer": "0.
|
|
44
|
+
"@mindees/renderer": "0.4.0"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "tsdown",
|