@geometra/router 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # @geometra/router
2
+
3
+ Routing primitives for Geometra.
4
+
5
+ Current scope (foundation): route matching and nested branch composition utilities.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @geometra/router
11
+ ```
12
+
13
+ ## Key Exports
14
+
15
+ - `matchPath` -- matches path patterns with static, dynamic, optional, and splat segments
16
+ - `buildPath` -- reverse routing helper with typed params
17
+ - `parseQuery` / `stringifyQuery` -- query parse/stringify helpers with deterministic key ordering
18
+ - `createMemoryHistory` -- history adapter for non-browser runtimes and tests
19
+ - `createBrowserHistory` -- browser adapter using pushState/replaceState/popstate
20
+ - `createRouter` -- router lifecycle (`start`, `navigate`, `subscribe`, `dispose`)
21
+ - route `loader` support -- params/query/requestContext-aware data loading for matched routes
22
+ - `state.loaderData` -- per-route loader results keyed by route `id`
23
+ - route `action` support -- write/mutation handlers with submission payloads
24
+ - `router.submitAction(routeId, submission)` + `state.actionData` -- mutation workflow primitives
25
+ - `submitAction(..., { optimistic })` -- optimistic mutation hooks with rollback on failure
26
+ - automatic loader revalidation after actions + explicit `router.revalidate()` support
27
+ - `redirect()` / `response()` / `json()` -- loader/action helpers for redirects and structured results
28
+ - loader/action contexts include `signal` for AbortController-driven cancellation
29
+ - router state exposes `pending` / `submitting` / `loading` flags for transition-aware UI
30
+ - router state exposes structured `error` payloads for loader/action/navigation failures
31
+ - `router.isActive(to)` / `router.isPending(to)` -- route state helpers for active and transition states
32
+ - `router.addBlocker(fn)` -- guards transitions for unsaved-state and confirmation flows
33
+ - `restoration` policy + per-navigation options -- scroll/focus restoration control on transitions
34
+ - `link` -- declarative link element with click + keyboard activation semantics
35
+ - `scorePathPattern` -- computes route specificity score for deterministic ranking
36
+ - `comparePatternSpecificity` -- compares two patterns by ranking
37
+ - `matchRouteTree` -- matches nested route trees with layout routes
38
+ - `renderMatchedOutlet` -- composes matched branch render output from leaf to root
39
+
40
+ ## Usage
41
+
42
+ ```ts
43
+ import { matchPath } from '@geometra/router'
44
+
45
+ const match = matchPath('/users/:id?', '/users/42')
46
+ // { params: { id: '42' } }
47
+ ```
@@ -0,0 +1,28 @@
1
+ export type RouterLocation = {
2
+ pathname: string;
3
+ search: string;
4
+ hash: string;
5
+ };
6
+ export type HistoryUpdate = {
7
+ location: RouterLocation;
8
+ action: 'push' | 'replace' | 'pop';
9
+ };
10
+ export type Unsubscribe = () => void;
11
+ export interface HistoryAdapter {
12
+ readonly location: RouterLocation;
13
+ push(to: string): void;
14
+ replace(to: string): void;
15
+ go(delta: number): void;
16
+ listen(listener: (update: HistoryUpdate) => void): Unsubscribe;
17
+ }
18
+ export type BrowserHistoryOptions = {
19
+ window?: Pick<Window, 'location' | 'history' | 'addEventListener' | 'removeEventListener'>;
20
+ };
21
+ type MemoryHistoryOptions = {
22
+ initialEntries?: string[];
23
+ initialIndex?: number;
24
+ };
25
+ export declare function createMemoryHistory(options?: MemoryHistoryOptions): HistoryAdapter;
26
+ export declare function createBrowserHistory(options?: BrowserHistoryOptions): HistoryAdapter;
27
+ export {};
28
+ //# sourceMappingURL=history.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,cAAc,CAAA;IACxB,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAA;AAEpC,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAA;IACjC,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,GAAG,WAAW,CAAA;CAC/D;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,kBAAkB,GAAG,qBAAqB,CAAC,CAAA;CAC3F,CAAA;AAmBD,KAAK,oBAAoB,GAAG;IAC1B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,oBAAyB,GAAG,cAAc,CA6CtF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B,GAAG,cAAc,CAsDxF"}
@@ -0,0 +1,111 @@
1
+ function parseToLocation(to) {
2
+ const url = new URL(to, 'https://geometra.local');
3
+ return {
4
+ pathname: url.pathname || '/',
5
+ search: url.search,
6
+ hash: url.hash,
7
+ };
8
+ }
9
+ function locationFromWindow(windowLike) {
10
+ return {
11
+ pathname: windowLike.location.pathname || '/',
12
+ search: windowLike.location.search || '',
13
+ hash: windowLike.location.hash || '',
14
+ };
15
+ }
16
+ export function createMemoryHistory(options = {}) {
17
+ const entries = (options.initialEntries ?? ['/']).map(parseToLocation);
18
+ let index = Math.min(Math.max(options.initialIndex ?? entries.length - 1, 0), Math.max(entries.length - 1, 0));
19
+ if (entries.length === 0) {
20
+ entries.push(parseToLocation('/'));
21
+ index = 0;
22
+ }
23
+ const listeners = new Set();
24
+ const notify = (action) => {
25
+ const location = entries[index];
26
+ if (!location)
27
+ return;
28
+ const update = { location, action };
29
+ for (const listener of listeners)
30
+ listener(update);
31
+ };
32
+ return {
33
+ get location() {
34
+ return entries[index] ?? parseToLocation('/');
35
+ },
36
+ push(to) {
37
+ const next = parseToLocation(to);
38
+ entries.splice(index + 1);
39
+ entries.push(next);
40
+ index = entries.length - 1;
41
+ notify('push');
42
+ },
43
+ replace(to) {
44
+ entries[index] = parseToLocation(to);
45
+ notify('replace');
46
+ },
47
+ go(delta) {
48
+ const nextIndex = Math.max(0, Math.min(index + delta, entries.length - 1));
49
+ if (nextIndex === index)
50
+ return;
51
+ index = nextIndex;
52
+ notify('pop');
53
+ },
54
+ listen(listener) {
55
+ listeners.add(listener);
56
+ return () => listeners.delete(listener);
57
+ },
58
+ };
59
+ }
60
+ export function createBrowserHistory(options = {}) {
61
+ const windowLike = options.window ??
62
+ (typeof window !== 'undefined'
63
+ ? window
64
+ : null);
65
+ if (!windowLike) {
66
+ throw new Error('createBrowserHistory requires a browser window');
67
+ }
68
+ const listeners = new Set();
69
+ const notify = (action) => {
70
+ const update = {
71
+ location: locationFromWindow(windowLike),
72
+ action,
73
+ };
74
+ for (const listener of listeners)
75
+ listener(update);
76
+ };
77
+ const onPopState = () => notify('pop');
78
+ return {
79
+ get location() {
80
+ return locationFromWindow(windowLike);
81
+ },
82
+ push(to) {
83
+ const next = parseToLocation(to);
84
+ const href = `${next.pathname}${next.search}${next.hash}`;
85
+ windowLike.history.pushState(null, '', href);
86
+ notify('push');
87
+ },
88
+ replace(to) {
89
+ const next = parseToLocation(to);
90
+ const href = `${next.pathname}${next.search}${next.hash}`;
91
+ windowLike.history.replaceState(null, '', href);
92
+ notify('replace');
93
+ },
94
+ go(delta) {
95
+ windowLike.history.go(delta);
96
+ },
97
+ listen(listener) {
98
+ if (listeners.size === 0) {
99
+ windowLike.addEventListener('popstate', onPopState);
100
+ }
101
+ listeners.add(listener);
102
+ return () => {
103
+ listeners.delete(listener);
104
+ if (listeners.size === 0) {
105
+ windowLike.removeEventListener('popstate', onPopState);
106
+ }
107
+ };
108
+ },
109
+ };
110
+ }
111
+ //# sourceMappingURL=history.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.js","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ export { matchPath } from './matcher.js';
2
+ export type { RouteMatch } from './matcher.js';
3
+ export { buildPath } from './path.js';
4
+ export type { PathParams } from './path.js';
5
+ export { parseQuery, stringifyQuery } from './query.js';
6
+ export type { QueryInput, QueryValue, ParsedQuery } from './query.js';
7
+ export { createMemoryHistory } from './history.js';
8
+ export { createBrowserHistory } from './history.js';
9
+ export type { RouterLocation, HistoryUpdate, HistoryAdapter, BrowserHistoryOptions } from './history.js';
10
+ export { link } from './link.js';
11
+ export type { LinkProps } from './link.js';
12
+ export { redirect, response, json } from './responses.js';
13
+ export type { RedirectResult, ResponseResult } from './responses.js';
14
+ export { scorePathPattern, comparePatternSpecificity } from './ranking.js';
15
+ export { createRouter } from './router.js';
16
+ export type { Router, RouterState, RouterSubscriber, RouterNavigationState, CreateRouterOptions, NavigationOptions, NavigationBlocker, RouterRestorationPolicy, NavigationRestorationContext, OptimisticMutation, SubmitActionOptions, RouterErrorPayload, RouterErrorPhase, } from './router.js';
17
+ export { matchRouteTree, renderMatchedOutlet } from './tree.js';
18
+ export type { RouteNode, RouteBranchMatch } from './tree.js';
19
+ export type { RouteLoaderContext, RouteActionContext, RouteActionSubmission } from './tree.js';
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACrC,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AACvD,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACnD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACxG,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,YAAY,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAA;AACzD,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpE,OAAO,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAA;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,YAAY,EACV,MAAM,EACN,WAAW,EACX,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,uBAAuB,EACvB,4BAA4B,EAC5B,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAC/D,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAC5D,YAAY,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { matchPath } from './matcher.js';
2
+ export { buildPath } from './path.js';
3
+ export { parseQuery, stringifyQuery } from './query.js';
4
+ export { createMemoryHistory } from './history.js';
5
+ export { createBrowserHistory } from './history.js';
6
+ export { link } from './link.js';
7
+ export { redirect, response, json } from './responses.js';
8
+ export { scorePathPattern, comparePatternSpecificity } from './ranking.js';
9
+ export { createRouter } from './router.js';
10
+ export { matchRouteTree, renderMatchedOutlet } from './tree.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAExC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAErC,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAEnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAA;AAEzD,OAAO,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAA;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAgB1C,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA"}
package/dist/link.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { type BoxElement, type HitEvent, type KeyboardHitEvent, type SemanticProps, type StyleProps, type UIElement } from '@geometra/core';
2
+ import type { FlexProps } from 'textura';
3
+ import type { Router } from './router.js';
4
+ export type LinkProps = FlexProps & StyleProps & {
5
+ to: string;
6
+ router: Pick<Router, 'navigate'>;
7
+ replace?: boolean;
8
+ key?: string;
9
+ semantic?: SemanticProps;
10
+ onClick?: (event: HitEvent) => void;
11
+ onKeyDown?: (event: KeyboardHitEvent) => void;
12
+ };
13
+ export declare function link(props: LinkProps, children?: UIElement[]): BoxElement;
14
+ //# sourceMappingURL=link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,KAAK,gBAAgB,EAAE,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAChJ,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,MAAM,SAAS,GAAG,SAAS,GAC/B,UAAU,GAAG;IACX,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IAChC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,aAAa,CAAA;IACxB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAA;IACnC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAA;CAC9C,CAAA;AAMH,wBAAgB,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,GAAE,SAAS,EAAO,GAAG,UAAU,CAoC7E"}
package/dist/link.js ADDED
@@ -0,0 +1,28 @@
1
+ import { box } from '@geometra/core';
2
+ function isActivationKey(key) {
3
+ return key === 'Enter' || key === ' ' || key === 'Spacebar';
4
+ }
5
+ export function link(props, children = []) {
6
+ const { to, router, replace, key, semantic, onClick, onKeyDown, cursor, ...styleAndLayout } = props;
7
+ return box({
8
+ ...styleAndLayout,
9
+ cursor: cursor ?? 'pointer',
10
+ key,
11
+ semantic: {
12
+ ...semantic,
13
+ tag: semantic?.tag ?? 'a',
14
+ role: semantic?.role ?? 'link',
15
+ },
16
+ onClick: (event) => {
17
+ onClick?.(event);
18
+ router.navigate(to, { replace });
19
+ },
20
+ onKeyDown: (event) => {
21
+ onKeyDown?.(event);
22
+ if (isActivationKey(event.key)) {
23
+ router.navigate(to, { replace });
24
+ }
25
+ },
26
+ }, children);
27
+ }
28
+ //# sourceMappingURL=link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.js","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAA8G,MAAM,gBAAgB,CAAA;AAehJ,SAAS,eAAe,CAAC,GAAW;IAClC,OAAO,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,UAAU,CAAA;AAC7D,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,KAAgB,EAAE,WAAwB,EAAE;IAC/D,MAAM,EACJ,EAAE,EACF,MAAM,EACN,OAAO,EACP,GAAG,EACH,QAAQ,EACR,OAAO,EACP,SAAS,EACT,MAAM,EACN,GAAG,cAAc,EAClB,GAAG,KAAK,CAAA;IAET,OAAO,GAAG,CACR;QACE,GAAG,cAAc;QACjB,MAAM,EAAE,MAAM,IAAI,SAAS;QAC3B,GAAG;QACH,QAAQ,EAAE;YACR,GAAG,QAAQ;YACX,GAAG,EAAE,QAAQ,EAAE,GAAG,IAAI,GAAG;YACzB,IAAI,EAAE,QAAQ,EAAE,IAAI,IAAI,MAAM;SAC/B;QACD,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YACjB,OAAO,EAAE,CAAC,KAAK,CAAC,CAAA;YAChB,MAAM,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAA;QAClC,CAAC;QACD,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE;YACnB,SAAS,EAAE,CAAC,KAAK,CAAC,CAAA;YAClB,IAAI,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC/B,MAAM,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAA;YAClC,CAAC;QACH,CAAC;KACF,EACD,QAAQ,CACT,CAAA;AACH,CAAC"}
@@ -0,0 +1,5 @@
1
+ export type RouteMatch = {
2
+ params: Record<string, string>;
3
+ };
4
+ export declare function matchPath(pattern: string, pathname: string): RouteMatch | null;
5
+ //# sourceMappingURL=matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcher.d.ts","sourceRoot":"","sources":["../src/matcher.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC/B,CAAA;AA6GD,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAS9E"}
@@ -0,0 +1,96 @@
1
+ function normalizePath(path, stripSearchHash) {
2
+ if (path === '')
3
+ return '/';
4
+ const pathnameOnly = stripSearchHash ? (path.split(/[?#]/, 1)[0] ?? '') : path;
5
+ const withLeading = pathnameOnly.startsWith('/') ? pathnameOnly : `/${pathnameOnly}`;
6
+ if (withLeading.length > 1 && withLeading.endsWith('/')) {
7
+ return withLeading.replace(/\/+$/, '');
8
+ }
9
+ return withLeading;
10
+ }
11
+ function trimSlashes(value) {
12
+ return value.replace(/^\/+|\/+$/g, '');
13
+ }
14
+ function parseSegments(pattern) {
15
+ const normalized = normalizePath(pattern, false);
16
+ const trimmed = trimSlashes(normalized);
17
+ if (trimmed === '')
18
+ return [];
19
+ const parts = trimmed.split('/');
20
+ return parts.map((part) => {
21
+ if (part.startsWith('*')) {
22
+ return { kind: 'splat', name: part.slice(1) || '*' };
23
+ }
24
+ const optional = part.endsWith('?');
25
+ const raw = optional ? part.slice(0, -1) : part;
26
+ if (raw.startsWith(':')) {
27
+ return { kind: 'param', name: raw.slice(1), optional };
28
+ }
29
+ return { kind: 'static', value: raw, optional };
30
+ });
31
+ }
32
+ function decodeSegment(value) {
33
+ try {
34
+ return decodeURIComponent(value);
35
+ }
36
+ catch {
37
+ return value;
38
+ }
39
+ }
40
+ function matchRecursive(segments, pathnameSegments, segmentIndex, pathIndex, params) {
41
+ if (segmentIndex >= segments.length) {
42
+ return pathIndex === pathnameSegments.length ? params : null;
43
+ }
44
+ const segment = segments[segmentIndex];
45
+ if (!segment)
46
+ return null;
47
+ switch (segment.kind) {
48
+ case 'splat': {
49
+ const rest = pathnameSegments.slice(pathIndex).map(decodeSegment).join('/');
50
+ return { ...params, [segment.name]: rest };
51
+ }
52
+ case 'static': {
53
+ const current = pathnameSegments[pathIndex];
54
+ if (current == null) {
55
+ if (segment.optional) {
56
+ return matchRecursive(segments, pathnameSegments, segmentIndex + 1, pathIndex, params);
57
+ }
58
+ return null;
59
+ }
60
+ if (current === segment.value) {
61
+ return matchRecursive(segments, pathnameSegments, segmentIndex + 1, pathIndex + 1, params);
62
+ }
63
+ if (segment.optional) {
64
+ return matchRecursive(segments, pathnameSegments, segmentIndex + 1, pathIndex, params);
65
+ }
66
+ return null;
67
+ }
68
+ case 'param': {
69
+ const current = pathnameSegments[pathIndex];
70
+ if (current == null) {
71
+ if (segment.optional) {
72
+ return matchRecursive(segments, pathnameSegments, segmentIndex + 1, pathIndex, params);
73
+ }
74
+ return null;
75
+ }
76
+ const withValue = matchRecursive(segments, pathnameSegments, segmentIndex + 1, pathIndex + 1, { ...params, [segment.name]: decodeSegment(current) });
77
+ if (withValue)
78
+ return withValue;
79
+ if (segment.optional) {
80
+ return matchRecursive(segments, pathnameSegments, segmentIndex + 1, pathIndex, params);
81
+ }
82
+ return null;
83
+ }
84
+ }
85
+ }
86
+ export function matchPath(pattern, pathname) {
87
+ const segments = parseSegments(pattern);
88
+ const normalizedPath = normalizePath(pathname, true);
89
+ const trimmedPath = trimSlashes(normalizedPath);
90
+ const pathSegments = trimmedPath === '' ? [] : trimmedPath.split('/');
91
+ const params = matchRecursive(segments, pathSegments, 0, 0, {});
92
+ if (!params)
93
+ return null;
94
+ return { params };
95
+ }
96
+ //# sourceMappingURL=matcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcher.js","sourceRoot":"","sources":["../src/matcher.ts"],"names":[],"mappings":"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"}
package/dist/path.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ type SegmentParamKeys<Path extends string> = Path extends `${infer Head}/${infer Tail}` ? SegmentParamKeys<Head> | SegmentParamKeys<Tail> : Path extends `:${infer Param}?` ? Param : Path extends `:${infer Param}` ? Param : Path extends `*${infer Param}` ? (Param extends '' ? '*' : Param) : never;
2
+ type SegmentRequiredParamKeys<Path extends string> = Path extends `${infer Head}/${infer Tail}` ? SegmentRequiredParamKeys<Head> | SegmentRequiredParamKeys<Tail> : Path extends `:${infer Param}` ? (Param extends `${string}?` ? never : Param) : Path extends `*${infer Param}` ? (Param extends '' ? '*' : Param) : never;
3
+ type SegmentOptionalParamKeys<Path extends string> = Exclude<SegmentParamKeys<Path>, SegmentRequiredParamKeys<Path>>;
4
+ /**
5
+ * Path parameters inferred from a pattern string (static segments, `:param`, optional `:param?`,
6
+ * and splat `*name` or anonymous `*`). Use with {@link buildPath}.
7
+ */
8
+ export type PathParams<Path extends string> = {
9
+ [K in SegmentRequiredParamKeys<Path>]: string | number;
10
+ } & {
11
+ [K in SegmentOptionalParamKeys<Path>]?: string | number;
12
+ };
13
+ /**
14
+ * Build a pathname from a route pattern and {@link PathParams}. Static segments are copied as-is;
15
+ * dynamic `:id` and optional `:id?` are filled from `params`; splat `*rest` (or a lone `*`, key `'*'`)
16
+ * inserts the remainder with internal slashes preserved. For optional segments, `null`, `undefined`,
17
+ * or empty string omits the segment (same as leaving the key unset). Required dynamic and splat
18
+ * params throw when missing or empty. Leading and trailing slashes on `pattern` are trimmed before building.
19
+ */
20
+ export declare function buildPath<Path extends string>(pattern: Path, params: PathParams<Path>): string;
21
+ export {};
22
+ //# sourceMappingURL=path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,KAAK,gBAAgB,CAAC,IAAI,SAAS,MAAM,IACvC,IAAI,SAAS,GAAG,MAAM,IAAI,IAAI,MAAM,IAAI,EAAE,GACtC,gBAAgB,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,IAAI,CAAC,GAC/C,IAAI,SAAS,IAAI,MAAM,KAAK,GAAG,GAC7B,KAAK,GACL,IAAI,SAAS,IAAI,MAAM,KAAK,EAAE,GAC5B,KAAK,GACL,IAAI,SAAS,IAAI,MAAM,KAAK,EAAE,GAC5B,CAAC,KAAK,SAAS,EAAE,GAAG,GAAG,GAAG,KAAK,CAAC,GAChC,KAAK,CAAA;AAEjB,KAAK,wBAAwB,CAAC,IAAI,SAAS,MAAM,IAC/C,IAAI,SAAS,GAAG,MAAM,IAAI,IAAI,MAAM,IAAI,EAAE,GACtC,wBAAwB,CAAC,IAAI,CAAC,GAAG,wBAAwB,CAAC,IAAI,CAAC,GAC/D,IAAI,SAAS,IAAI,MAAM,KAAK,EAAE,GAC5B,CAAC,KAAK,SAAS,GAAG,MAAM,GAAG,GAAG,KAAK,GAAG,KAAK,CAAC,GAC5C,IAAI,SAAS,IAAI,MAAM,KAAK,EAAE,GAC5B,CAAC,KAAK,SAAS,EAAE,GAAG,GAAG,GAAG,KAAK,CAAC,GAChC,KAAK,CAAA;AAEf,KAAK,wBAAwB,CAAC,IAAI,SAAS,MAAM,IAAI,OAAO,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,wBAAwB,CAAC,IAAI,CAAC,CAAC,CAAA;AAEpH;;;GAGG;AACH,MAAM,MAAM,UAAU,CAAC,IAAI,SAAS,MAAM,IAAI;KAC3C,CAAC,IAAI,wBAAwB,CAAC,IAAI,CAAC,GAAG,MAAM,GAAG,MAAM;CACvD,GAAG;KACD,CAAC,IAAI,wBAAwB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;CACxD,CAAA;AAUD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,SAAS,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC,GAAG,MAAM,CAkC9F"}
package/dist/path.js ADDED
@@ -0,0 +1,46 @@
1
+ function trimSlashes(value) {
2
+ return value.replace(/^\/+|\/+$/g, '');
3
+ }
4
+ function stringifyParam(value) {
5
+ return encodeURIComponent(String(value));
6
+ }
7
+ /**
8
+ * Build a pathname from a route pattern and {@link PathParams}. Static segments are copied as-is;
9
+ * dynamic `:id` and optional `:id?` are filled from `params`; splat `*rest` (or a lone `*`, key `'*'`)
10
+ * inserts the remainder with internal slashes preserved. For optional segments, `null`, `undefined`,
11
+ * or empty string omits the segment (same as leaving the key unset). Required dynamic and splat
12
+ * params throw when missing or empty. Leading and trailing slashes on `pattern` are trimmed before building.
13
+ */
14
+ export function buildPath(pattern, params) {
15
+ const trimmed = trimSlashes(pattern);
16
+ if (trimmed === '')
17
+ return '/';
18
+ const parts = trimmed.split('/');
19
+ const out = [];
20
+ for (const part of parts) {
21
+ if (part.startsWith('*')) {
22
+ const key = part.slice(1) || '*';
23
+ const value = params[key];
24
+ if (value == null || value === '') {
25
+ throw new Error(`Missing required splat param: ${key}`);
26
+ }
27
+ out.push(trimSlashes(String(value)));
28
+ continue;
29
+ }
30
+ if (part.startsWith(':')) {
31
+ const optional = part.endsWith('?');
32
+ const key = optional ? part.slice(1, -1) : part.slice(1);
33
+ const value = params[key];
34
+ if (value == null || value === '') {
35
+ if (optional)
36
+ continue;
37
+ throw new Error(`Missing required path param: ${key}`);
38
+ }
39
+ out.push(stringifyParam(value));
40
+ continue;
41
+ }
42
+ out.push(part.endsWith('?') ? part.slice(0, -1) : part);
43
+ }
44
+ return `/${out.join('/')}`;
45
+ }
46
+ //# sourceMappingURL=path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.js","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAgCA,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO,KAAK,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAA;AACxC,CAAC;AAED,SAAS,cAAc,CAAC,KAAsB;IAC5C,OAAO,kBAAkB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAsB,OAAa,EAAE,MAAwB;IACpF,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;IACpC,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,GAAG,CAAA;IAE9B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAChC,MAAM,GAAG,GAAa,EAAE,CAAA;IAExB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,CAAA;YAChC,MAAM,KAAK,GAAG,MAAM,CAAC,GAA6B,CAAC,CAAA;YACnD,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;gBAClC,MAAM,IAAI,KAAK,CAAC,iCAAiC,GAAG,EAAE,CAAC,CAAA;YACzD,CAAC;YACD,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YACpC,SAAQ;QACV,CAAC;QAED,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;YACnC,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YACxD,MAAM,KAAK,GAAG,MAAM,CAAC,GAA6B,CAAC,CAAA;YACnD,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;gBAClC,IAAI,QAAQ;oBAAE,SAAQ;gBACtB,MAAM,IAAI,KAAK,CAAC,gCAAgC,GAAG,EAAE,CAAC,CAAA;YACxD,CAAC;YACD,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;YAC/B,SAAQ;QACV,CAAC;QAED,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IACzD,CAAC;IAED,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;AAC5B,CAAC"}
@@ -0,0 +1,23 @@
1
+ export type QueryValue = string | number | boolean | null | undefined;
2
+ export type QueryInput = Record<string, QueryValue | QueryValue[]>;
3
+ /**
4
+ * Result of {@link parseQuery}: each key maps to one value or, when repeated in the input,
5
+ * an array of values (order preserved). Instances use a null prototype so query keys such as
6
+ * `__proto__` are stored as ordinary string keys and cannot mutate `Object.prototype`.
7
+ */
8
+ export type ParsedQuery = Record<string, string | string[]>;
9
+ /**
10
+ * Parse a URL query string (optional leading `?`). Keys and values are percent-decoded;
11
+ * `+` in values is treated as space. Duplicate keys collapse to `string[]` in encounter order.
12
+ * If decoding a segment throws, that segment is left as the raw string (see malformed `%` escapes).
13
+ * The returned object has a null prototype (see {@link ParsedQuery}).
14
+ */
15
+ export declare function parseQuery(search: string): ParsedQuery;
16
+ /**
17
+ * Serialize a shallow query object to `?a=1&b=2`. Keys are sorted lexicographically for stable output.
18
+ * Skips `null` and `undefined`; array values become repeated keys. Booleans become `"true"` / `"false"`.
19
+ * Skips non-finite numbers (`NaN`, `±Infinity`) so accidental numeric garbage does not produce query pairs.
20
+ * Returns `""` when there are no pairs to emit.
21
+ */
22
+ export declare function stringifyQuery(query: QueryInput): string;
23
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAA;AACrE,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,EAAE,CAAC,CAAA;AAClE;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAA;AAc3D;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAqBtD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAkBxD"}
package/dist/query.js ADDED
@@ -0,0 +1,68 @@
1
+ function decode(value) {
2
+ try {
3
+ return decodeURIComponent(value.replace(/\+/g, ' '));
4
+ }
5
+ catch {
6
+ return value;
7
+ }
8
+ }
9
+ function encode(value) {
10
+ return encodeURIComponent(value);
11
+ }
12
+ /**
13
+ * Parse a URL query string (optional leading `?`). Keys and values are percent-decoded;
14
+ * `+` in values is treated as space. Duplicate keys collapse to `string[]` in encounter order.
15
+ * If decoding a segment throws, that segment is left as the raw string (see malformed `%` escapes).
16
+ * The returned object has a null prototype (see {@link ParsedQuery}).
17
+ */
18
+ export function parseQuery(search) {
19
+ const input = search.startsWith('?') ? search.slice(1) : search;
20
+ if (input === '')
21
+ return Object.create(null);
22
+ const result = Object.create(null);
23
+ for (const pair of input.split('&')) {
24
+ if (pair === '')
25
+ continue;
26
+ const [rawKey, ...rest] = pair.split('=');
27
+ const key = decode(rawKey ?? '');
28
+ const value = decode(rest.join('='));
29
+ const current = result[key];
30
+ if (current == null) {
31
+ result[key] = value;
32
+ }
33
+ else if (Array.isArray(current)) {
34
+ current.push(value);
35
+ }
36
+ else {
37
+ result[key] = [current, value];
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+ /**
43
+ * Serialize a shallow query object to `?a=1&b=2`. Keys are sorted lexicographically for stable output.
44
+ * Skips `null` and `undefined`; array values become repeated keys. Booleans become `"true"` / `"false"`.
45
+ * Skips non-finite numbers (`NaN`, `±Infinity`) so accidental numeric garbage does not produce query pairs.
46
+ * Returns `""` when there are no pairs to emit.
47
+ */
48
+ export function stringifyQuery(query) {
49
+ const keys = Object.keys(query).sort((a, b) => a.localeCompare(b));
50
+ const pairs = [];
51
+ for (const key of keys) {
52
+ const raw = query[key];
53
+ if (raw == null)
54
+ continue;
55
+ const values = Array.isArray(raw) ? raw : [raw];
56
+ for (const value of values) {
57
+ if (value == null)
58
+ continue;
59
+ if (typeof value === 'number' && !Number.isFinite(value))
60
+ continue;
61
+ pairs.push(`${encode(key)}=${encode(String(value))}`);
62
+ }
63
+ }
64
+ if (pairs.length === 0)
65
+ return '';
66
+ return `?${pairs.join('&')}`;
67
+ }
68
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export declare function scorePathPattern(pattern: string): number;
2
+ export declare function comparePatternSpecificity(a: string, b: string): number;
3
+ //# sourceMappingURL=ranking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ranking.d.ts","sourceRoot":"","sources":["../src/ranking.ts"],"names":[],"mappings":"AAuCA,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGxD;AAED,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAUtE"}