@owlmeans/router 0.1.10 → 0.1.14

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.
@@ -1,30 +1,40 @@
1
1
  ---
2
- description: "How to use @owlmeans/router — abstract router service interface used by web-router and native router implementations. Use when implementing a custom router."
2
+ description: "How to use @owlmeans/router — the UI routing plugin host: RouterService registry, cascade selection, neutral route IR, and pure matcher. Use when registering a routing plugin, using the router service, or reusing the matcher."
3
3
  applyTo: "**/*.ts, **/*.tsx"
4
4
  ---
5
5
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
6
6
 
7
7
  # @owlmeans/router
8
8
 
9
- **Layer:** Core
10
- **Install:** `"@owlmeans/router": "^0.1.10"` in `dependencies`
9
+ **Layer:** Core (L1)
10
+ **Install:** `"@owlmeans/router": "^0.1.14"` in `dependencies`
11
+
12
+ The plugin **host** for OwlMeans UI routing. Defines the contract, holds a registry of routing
13
+ plugins, and selects the active one by cascade. Concrete mechanics are plugins:
14
+ `@owlmeans/web-router` (default OwlMeans browser router) and `@owlmeans/web-router-react-router`
15
+ (opt-in react-router v7).
11
16
 
12
17
  ## Key Exports
13
18
 
14
19
  | Export | Description |
15
20
  |--------|-------------|
16
- | `RouterService` types | Abstract router service interface |
17
- | Constants | `DEFAULT_ALIAS` for the router service |
18
- | Service helpers | Build a router service skeleton |
21
+ | `RouterService` | Facade + registry; `outlet/provider/useParams/useLocation/useNavigate/useSearchParams/compile` delegate to the active plugin. |
22
+ | `RouterPlugin`, `RouterEnv`, `RouteObject` | Plugin contract, selection env (`{hasWindow,ssr,request?}`), neutral route IR (`{index?,path?,children?,Component?}`). |
23
+ | `makeRouterService`, `ensureRouterService(ctx)` | Build / idempotently get the host. |
24
+ | `flattenRoutes`, `rankRouteBranches`, `matchRoutes` | Pure DOM-free matcher (static/`:param`/nested/index). |
25
+ | `defaultRouterEnv()`, `ROUTER_SERVICE`, `DEFAULT_ROUTER_PRIORITY`, `ROUTER_PLUGIN` | Env + constants. |
26
+
27
+ ## Cascade
19
28
 
20
- ## Usage
29
+ Plugins sorted by `priority` desc; `service.plugin(env?)` returns the first whose `match(env)` is
30
+ truthy (undefined ⇒ always). Browser plugin = priority 0, `match: () => true`; a higher-priority
31
+ SSR plugin can win when `env.ssr`. Facade methods stay writable (native monkey-patches them).
21
32
 
22
33
  ```typescript
23
- import { DEFAULT_ALIAS as ROUTER } from '@owlmeans/router'
24
- const router = ctx.service(ROUTER)
25
- router.navigate('/projects')
34
+ import { ROUTER_SERVICE } from '@owlmeans/router'
35
+ ctx.service(ROUTER_SERVICE).useParams() // delegates to the active plugin
26
36
  ```
27
37
 
28
38
  ## Depends On
29
39
 
30
- - `@owlmeans/context`, `@owlmeans/route`
40
+ - `@owlmeans/context`; peer `react` (types only — no react-router, no DOM here)
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "package": "@owlmeans/router",
4
- "version": "0.1.10",
5
- "generatedAt": "2026-06-17T09:37:28.183Z",
4
+ "version": "0.1.14",
5
+ "generatedAt": "2026-08-05T16:56:53.383Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -1,34 +1,59 @@
1
1
  ---
2
2
  name: router
3
- description: How to use @owlmeans/router — abstract router service interface used by web-router and native router implementations. Auto-invoked when importing router service types or implementing a custom router.
3
+ description: How to use @owlmeans/router — the UI routing plugin HOST (RouterService registry + cascade selection + neutral route IR + pure matcher). Auto-invoked when importing router service types, the matcher, or implementing/registering a routing plugin.
4
4
  user-invocable: false
5
5
  ---
6
6
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
7
7
 
8
8
  # @owlmeans/router
9
9
 
10
- **Layer:** Core
11
- **Install:** `"@owlmeans/router": "^0.1.10"` in `dependencies`
10
+ **Layer:** Core (L1)
11
+ **Install:** `"@owlmeans/router": "^0.1.14"` in `dependencies`
12
+
13
+ `@owlmeans/router` is the **plugin host** for OwlMeans UI routing. It does not talk to any
14
+ concrete router; it defines the contract, holds a registry of routing plugins, and selects the
15
+ active one by cascade. Concrete mechanics ship as plugins: `@owlmeans/web-router` (the default
16
+ OwlMeans in-browser router) and `@owlmeans/web-router-react-router` (opt-in react-router v7).
12
17
 
13
18
  ## Key Exports
14
19
 
15
20
  | Export | Description |
16
21
  |--------|-------------|
17
- | `RouterService` types | Abstract router service interface |
18
- | Constants | `DEFAULT_ALIAS` for the router service |
19
- | Service helpers | Build a router service skeleton |
20
-
21
- ## Usage
22
-
23
- Concrete routers (e.g. `@owlmeans/web-router` for React Router 7) implement this interface. Apps don't usually import from `@owlmeans/router` directly — the layer-specific package wires everything up.
22
+ | `RouterService` | Facade + plugin registry. `outlet/provider/useParams/useLocation/useNavigate/useSearchParams/compile` all delegate to the active plugin. |
23
+ | `RouterPlugin` | The interface a routing mechanic implements. |
24
+ | `RouterEnv` | `{ hasWindow, ssr, request? }` — drives cascade selection (SSR pre-design seam). |
25
+ | `RouteObject` | Neutral route IR: `{ index?, path?, children?, Component? }` — shape-compatible with react-router. |
26
+ | `makeRouterService(alias?)` | Build an empty host. |
27
+ | `ensureRouterService(ctx)` | Idempotently get/create the host on a context (plugin packages call this before `registerPlugin`). |
28
+ | `flattenRoutes` / `rankRouteBranches` / `matchRoutes` | Pure, DOM-free matcher (static / `:param` / nested / index). |
29
+ | `defaultRouterEnv()` | Default environment (`window`-based); override per-request for SSR. |
30
+ | Constants | `ROUTER_SERVICE` (`'router-service'`), `DEFAULT_ROUTER_PRIORITY`, `ROUTER_PLUGIN`. |
31
+
32
+ ## Cascade selection
33
+
34
+ Plugins are kept sorted by `priority` (desc, stable). `service.plugin(env?)` returns the first
35
+ plugin whose `match(env, ctx)` is truthy (`match` undefined ⇒ always applies). The browser plugin
36
+ registers at priority 0 with `match: () => true` (universal fallback); a higher-priority SSR plugin
37
+ can later win when `env.ssr` is true. Every facade call re-selects, so plugins can be added at any
38
+ time (e.g. `appendReactRouter(ctx)` outranks the default).
24
39
 
25
40
  ```typescript
26
- import { DEFAULT_ALIAS as ROUTER } from '@owlmeans/router'
27
- const router = ctx.service(ROUTER)
28
- router.navigate('/projects')
41
+ import { ROUTER_SERVICE } from '@owlmeans/router'
42
+ const params = ctx.service(ROUTER_SERVICE).useParams() // delegates to the active plugin
29
43
  ```
30
44
 
45
+ ## Native-safety invariant
46
+
47
+ The facade methods are **plain writable instance properties**, never getters — the native router
48
+ (`@owlmeans/native-router`) monkey-patches `service.outlet = …` directly. When extending the host,
49
+ keep them assignable.
50
+
31
51
  ## Depends On
32
52
 
33
53
  - `@owlmeans/context` — service registration
34
- - `@owlmeans/route` — route shape
54
+ - peer `react`component/hook types only (no react-router, no DOM here)
55
+
56
+ ## Related
57
+
58
+ - [[web-router]] (default OwlMeans browser plugin) · [[router-plugins]] (authoring plugins)
59
+ - `@owlmeans/web-router-react-router` (react-router plugin)
package/build/consts.d.ts CHANGED
@@ -1,3 +1,7 @@
1
1
  export declare const DEFAULT_ALIAS = "router-service";
2
2
  export declare const ROUTER_SERVICE = "router-service";
3
+ /** Default plugin priority; higher-priority plugins win the cascade. */
4
+ export declare const DEFAULT_ROUTER_PRIORITY = 0;
5
+ /** Config-record id for declaring a preferred router plugin via `clientPlugin`. */
6
+ export declare const ROUTER_PLUGIN = "router-plugin";
3
7
  //# sourceMappingURL=consts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,aAAa,mBAAmB,CAAA;AAE7C,eAAO,MAAM,cAAc,mBAAgB,CAAA"}
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,aAAa,mBAAmB,CAAA;AAE7C,eAAO,MAAM,cAAc,mBAAgB,CAAA;AAE3C,wEAAwE;AACxE,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC,mFAAmF;AACnF,eAAO,MAAM,aAAa,kBAAkB,CAAA"}
package/build/consts.js CHANGED
@@ -1,3 +1,7 @@
1
1
  export const DEFAULT_ALIAS = 'router-service';
2
2
  export const ROUTER_SERVICE = DEFAULT_ALIAS;
3
+ /** Default plugin priority; higher-priority plugins win the cascade. */
4
+ export const DEFAULT_ROUTER_PRIORITY = 0;
5
+ /** Config-record id for declaring a preferred router plugin via `clientPlugin`. */
6
+ export const ROUTER_PLUGIN = 'router-plugin';
3
7
  //# sourceMappingURL=consts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,aAAa,GAAG,gBAAgB,CAAA;AAE7C,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAAA"}
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,aAAa,GAAG,gBAAgB,CAAA;AAE7C,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAAA;AAE3C,wEAAwE;AACxE,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAA;AAExC,mFAAmF;AACnF,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAA"}
package/build/env.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { RouterEnv } from './types.js';
2
+ /**
3
+ * Default routing environment. In the browser `hasWindow` is true and `ssr` is
4
+ * false, so the browser plugin (which matches always) is selected. An SSR host
5
+ * will later provide a per-request env (with `ssr: true` and a `request`) so an
6
+ * SSR plugin registered at a higher priority wins.
7
+ *
8
+ * This is the single source of environment truth for cascade selection — do not
9
+ * bake `typeof window` checks into a plugin's `match`.
10
+ */
11
+ export declare const defaultRouterEnv: () => RouterEnv;
12
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE3C;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,QAAO,SAGnC,CAAA"}
package/build/env.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Default routing environment. In the browser `hasWindow` is true and `ssr` is
3
+ * false, so the browser plugin (which matches always) is selected. An SSR host
4
+ * will later provide a per-request env (with `ssr: true` and a `request`) so an
5
+ * SSR plugin registered at a higher priority wins.
6
+ *
7
+ * This is the single source of environment truth for cascade selection — do not
8
+ * bake `typeof window` checks into a plugin's `match`.
9
+ */
10
+ export const defaultRouterEnv = () => {
11
+ const hasWindow = typeof window !== 'undefined';
12
+ return { hasWindow, ssr: !hasWindow };
13
+ };
14
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAc,EAAE;IAC9C,MAAM,SAAS,GAAG,OAAO,MAAM,KAAK,WAAW,CAAA;IAC/C,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAA;AACvC,CAAC,CAAA"}
package/build/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export type * from './types.js';
2
2
  export * from './consts.js';
3
3
  export * from './service.js';
4
+ export * from './matcher.js';
5
+ export * from './env.js';
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,UAAU,CAAA"}
package/build/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from './consts.js';
2
2
  export * from './service.js';
3
+ export * from './matcher.js';
4
+ export * from './env.js';
3
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,UAAU,CAAA"}
@@ -0,0 +1,37 @@
1
+ import type { RouteObject, RouteParams } from './types.js';
2
+ type SegmentKind = 'static' | 'param' | 'splat';
3
+ export interface PatternSegment {
4
+ kind: SegmentKind;
5
+ /** literal value for static, param name for param */
6
+ value: string;
7
+ }
8
+ export interface RouteBranch {
9
+ /** root → node chain; drives outlet depth in the renderer */
10
+ chain: RouteObject[];
11
+ segments: PatternSegment[];
12
+ /** true when the terminal node is an index route */
13
+ index: boolean;
14
+ score: number;
15
+ }
16
+ export interface RouteMatch {
17
+ route: RouteObject;
18
+ params: RouteParams;
19
+ pathname: string;
20
+ }
21
+ export declare const splitPath: (path?: string) => string[];
22
+ export declare const segmentsOf: (path?: string) => PatternSegment[];
23
+ /**
24
+ * Flatten a route tree into ranked-matchable branches. Every leaf and every node
25
+ * carrying an index child produces a branch.
26
+ */
27
+ export declare const flattenRoutes: (routes: RouteObject[], parentChain?: RouteObject[], parentSegments?: PatternSegment[]) => RouteBranch[];
28
+ /** Rank branches so the most specific match is tried first. Stable within equal score. */
29
+ export declare const rankRouteBranches: (branches: RouteBranch[]) => RouteBranch[];
30
+ /**
31
+ * Match a pathname against ranked branches. Returns the winning branch as a
32
+ * root→leaf chain of `RouteMatch` (params merged across the whole chain so every
33
+ * depth sees the full leaf param set, matching react-router's `useParams`).
34
+ */
35
+ export declare const matchRoutes: (branches: RouteBranch[], pathname: string) => RouteMatch[] | null;
36
+ export {};
37
+ //# sourceMappingURL=matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcher.d.ts","sourceRoot":"","sources":["../src/matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAsB1D,KAAK,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAA;AAE/C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,WAAW,CAAA;IACjB,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,6DAA6D;IAC7D,KAAK,EAAE,WAAW,EAAE,CAAA;IACpB,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,oDAAoD;IACpD,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAA;IAClB,MAAM,EAAE,WAAW,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,KAAG,MAAM,EACS,CAAA;AAEzD,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,KAAG,cAAc,EAKrD,CAAA;AAEJ;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,QAAQ,WAAW,EAAE,EACrB,cAAa,WAAW,EAAO,EAC/B,iBAAgB,cAAc,EAAO,KACpC,WAAW,EAuBb,CAAA;AAaD,0FAA0F;AAC1F,eAAO,MAAM,iBAAiB,GAAI,UAAU,WAAW,EAAE,KAAG,WAAW,EAKvC,CAAA;AAsBhC;;;;GAIG;AACH,eAAO,MAAM,WAAW,GAAI,UAAU,WAAW,EAAE,EAAE,UAAU,MAAM,KAAG,UAAU,EAAE,GAAG,IAgBtF,CAAA"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Pure, DOM-free route matcher. Supports exactly the route-description subset used
3
+ * across the OwlMeans projects: static segments, `:param` dynamic segments, nested
4
+ * (parent/child) routes and index routes. Splat (`*`) and optional (`:x?`) segments
5
+ * are intentionally not implemented yet — a seam is reserved so they can be added
6
+ * without changing the public shape.
7
+ *
8
+ * Segment weights mirror react-router's ranking intuition (static beats dynamic,
9
+ * more specific / deeper beats shallower).
10
+ */
11
+ const SEP = '/';
12
+ const PARAM = ':';
13
+ const SPLAT = '*';
14
+ const STATIC_WEIGHT = 10;
15
+ const PARAM_WEIGHT = 4;
16
+ const INDEX_WEIGHT = 2;
17
+ const EMPTY_WEIGHT = 1;
18
+ export const splitPath = (path) => (path ?? '').split(SEP).filter(part => part.length > 0);
19
+ export const segmentsOf = (path) => splitPath(path).map(part => {
20
+ if (part === SPLAT)
21
+ return { kind: 'splat', value: SPLAT };
22
+ if (part.startsWith(PARAM))
23
+ return { kind: 'param', value: part.slice(PARAM.length) };
24
+ return { kind: 'static', value: part };
25
+ });
26
+ /**
27
+ * Flatten a route tree into ranked-matchable branches. Every leaf and every node
28
+ * carrying an index child produces a branch.
29
+ */
30
+ export const flattenRoutes = (routes, parentChain = [], parentSegments = []) => {
31
+ const branches = [];
32
+ for (const route of routes) {
33
+ const chain = [...parentChain, route];
34
+ if (route.index === true) {
35
+ // An index route contributes an empty terminal at the parent's path.
36
+ branches.push({ chain, segments: parentSegments, index: true, score: 0 });
37
+ continue;
38
+ }
39
+ const segments = [...parentSegments, ...segmentsOf(route.path)];
40
+ const children = route.children ?? [];
41
+ if (children.length === 0) {
42
+ branches.push({ chain, segments, index: false, score: 0 });
43
+ }
44
+ else {
45
+ branches.push(...flattenRoutes(children, chain, segments));
46
+ }
47
+ }
48
+ return branches;
49
+ };
50
+ const scoreBranch = (branch) => {
51
+ let score = branch.segments.reduce((acc, seg) => {
52
+ if (seg.kind === 'static')
53
+ return acc + STATIC_WEIGHT;
54
+ if (seg.kind === 'param')
55
+ return acc + PARAM_WEIGHT;
56
+ return acc + EMPTY_WEIGHT;
57
+ }, 0);
58
+ if (branch.segments.length === 0)
59
+ score += EMPTY_WEIGHT;
60
+ if (branch.index)
61
+ score += INDEX_WEIGHT;
62
+ return score;
63
+ };
64
+ /** Rank branches so the most specific match is tried first. Stable within equal score. */
65
+ export const rankRouteBranches = (branches) => branches
66
+ .map(branch => ({ ...branch, score: scoreBranch(branch) }))
67
+ .map((branch, index) => ({ branch, index }))
68
+ .sort((a, b) => (b.branch.score - a.branch.score) || (a.index - b.index))
69
+ .map(({ branch }) => branch);
70
+ const matchBranch = (branch, parts) => {
71
+ if (branch.segments.some(seg => seg.kind === 'splat')) {
72
+ throw new Error('router matcher: splat (*) segments are not implemented yet');
73
+ }
74
+ // Exact-length match (no splats/optionals in the supported subset).
75
+ if (branch.segments.length !== parts.length)
76
+ return null;
77
+ const params = {};
78
+ for (let i = 0; i < branch.segments.length; i++) {
79
+ const seg = branch.segments[i];
80
+ const part = parts[i];
81
+ if (seg.kind === 'static') {
82
+ if (seg.value.toLowerCase() !== part.toLowerCase())
83
+ return null;
84
+ }
85
+ else {
86
+ params[seg.value] = decodeURIComponent(part);
87
+ }
88
+ }
89
+ return params;
90
+ };
91
+ /**
92
+ * Match a pathname against ranked branches. Returns the winning branch as a
93
+ * root→leaf chain of `RouteMatch` (params merged across the whole chain so every
94
+ * depth sees the full leaf param set, matching react-router's `useParams`).
95
+ */
96
+ export const matchRoutes = (branches, pathname) => {
97
+ const parts = splitPath(pathname);
98
+ const ranked = rankRouteBranches(branches);
99
+ for (const branch of ranked) {
100
+ const params = matchBranch(branch, parts);
101
+ if (params == null)
102
+ continue;
103
+ return branch.chain.map(route => ({
104
+ route,
105
+ params,
106
+ pathname: SEP + parts.join(SEP)
107
+ }));
108
+ }
109
+ return null;
110
+ };
111
+ //# sourceMappingURL=matcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcher.js","sourceRoot":"","sources":["../src/matcher.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,18 @@
1
- import type { RouterService } from "./types";
1
+ import type { BasicContext } from "@owlmeans/context";
2
+ import type { RouterService } from "./types.js";
3
+ /**
4
+ * Router service = plugin host. It holds a registry of routing plugins and, on
5
+ * every facade call, resolves the active plugin by cascade (priority desc, first
6
+ * whose `match(env)` is truthy) and delegates to it.
7
+ *
8
+ * The facade methods stay ordinary writable properties (never getters) so that
9
+ * non-plugin implementations — e.g. the native router — can keep overriding them
10
+ * directly, exactly as `web-router` did before the plugin model.
11
+ */
2
12
  export declare const makeRouterService: (alias?: string) => RouterService;
13
+ /**
14
+ * Idempotently obtain the router service on a context, registering an empty host
15
+ * if none exists yet. Plugin packages call this before `registerPlugin`.
16
+ */
17
+ export declare const ensureRouterService: (ctx: BasicContext<any>) => RouterService;
3
18
  //# sourceMappingURL=service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,eAAO,MAAM,iBAAiB,GAAI,QAAO,MAAsB,KAAG,aAwBjE,CAAA"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGrD,OAAO,KAAK,EACV,aAAa,EACd,MAAM,YAAY,CAAA;AAEnB;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAO,MAAsB,KAAG,aAwCjE,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAAI,KAAK,YAAY,CAAC,GAAG,CAAC,KAAG,aAO5D,CAAA"}
package/build/service.js CHANGED
@@ -1,23 +1,55 @@
1
1
  import { createService } from "@owlmeans/context";
2
2
  import { DEFAULT_ALIAS } from "./consts.js";
3
+ import { defaultRouterEnv } from "./env.js";
4
+ /**
5
+ * Router service = plugin host. It holds a registry of routing plugins and, on
6
+ * every facade call, resolves the active plugin by cascade (priority desc, first
7
+ * whose `match(env)` is truthy) and delegates to it.
8
+ *
9
+ * The facade methods stay ordinary writable properties (never getters) so that
10
+ * non-plugin implementations — e.g. the native router — can keep overriding them
11
+ * directly, exactly as `web-router` did before the plugin model.
12
+ */
3
13
  export const makeRouterService = (alias = DEFAULT_ALIAS) => {
14
+ const plugins = [];
4
15
  const service = createService(alias, {
5
- outlet: () => {
6
- throw new Error('Router Outlet is not implemented');
16
+ registerPlugin: plugin => {
17
+ const existing = plugins.findIndex(candidate => candidate.alias === plugin.alias);
18
+ if (existing >= 0) {
19
+ plugins.splice(existing, 1);
20
+ }
21
+ plugins.push(plugin);
22
+ plugins.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
7
23
  },
8
- provider: () => {
9
- throw new Error('Router Provider is not implemented');
24
+ plugin: env => {
25
+ const environment = env ?? defaultRouterEnv();
26
+ const ctx = service.ctx;
27
+ const plugin = plugins.find(candidate => candidate.match?.(environment, ctx) ?? true);
28
+ if (plugin == null) {
29
+ throw new Error('router: no plugin matches the current environment');
30
+ }
31
+ return plugin;
10
32
  },
11
- useParams: () => {
12
- throw new Error('Router useParams is not implemented');
13
- },
14
- useLocation: () => {
15
- throw new Error('Router useLocation is not implemented');
16
- },
17
- useNavigate: () => {
18
- throw new Error('Router useNavigate is not implemented');
19
- }
33
+ outlet: () => service.plugin().outlet(),
34
+ provider: () => service.plugin().provider(),
35
+ useParams: (() => service.plugin().useParams()),
36
+ useLocation: () => service.plugin().useLocation(),
37
+ useNavigate: () => service.plugin().useNavigate(),
38
+ useSearchParams: ((init) => service.plugin().useSearchParams(init)),
39
+ compile: routes => service.plugin().compile(routes, service.ctx)
20
40
  });
21
41
  return service;
22
42
  };
43
+ /**
44
+ * Idempotently obtain the router service on a context, registering an empty host
45
+ * if none exists yet. Plugin packages call this before `registerPlugin`.
46
+ */
47
+ export const ensureRouterService = (ctx) => {
48
+ if (ctx.hasService(DEFAULT_ALIAS)) {
49
+ return ctx.service(DEFAULT_ALIAS);
50
+ }
51
+ const service = makeRouterService();
52
+ ctx.registerService(service);
53
+ return service;
54
+ };
23
55
  //# sourceMappingURL=service.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAG3C,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,QAAgB,aAAa,EAAiB,EAAE;IAChF,MAAM,OAAO,GAAG,aAAa,CAAgB,KAAK,EAAE;QAClD,MAAM,EAAE,GAAG,EAAE;YACX,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAA;QACrD,CAAC;QAED,QAAQ,EAAE,GAAG,EAAE;YACb,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;QACvD,CAAC;QAED,SAAS,EAAE,GAAG,EAAE;YACd,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAA;QACxD,CAAC;QAED,WAAW,EAAE,GAAG,EAAE;YAChB,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAA;QAC1D,CAAC;QAED,WAAW,EAAE,GAAG,EAAE;YAChB,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAA;QAC1D,CAAC;KACF,CAAC,CAAA;IAEF,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAK3C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,QAAgB,aAAa,EAAiB,EAAE;IAChF,MAAM,OAAO,GAAmB,EAAE,CAAA;IAElC,MAAM,OAAO,GAAkB,aAAa,CAAgB,KAAK,EAAE;QACjE,cAAc,EAAE,MAAM,CAAC,EAAE;YACvB,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC,SAAS,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC,CAAA;YACjF,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;gBAClB,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;YAC7B,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAA;QAC/D,CAAC;QAED,MAAM,EAAE,GAAG,CAAC,EAAE;YACZ,MAAM,WAAW,GAAG,GAAG,IAAI,gBAAgB,EAAE,CAAA;YAC7C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAoC,CAAA;YACxD,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,CAAA;YACrF,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;gBACnB,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAA;YACtE,CAAC;YACD,OAAO,MAAM,CAAA;QACf,CAAC;QAED,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE;QAEvC,QAAQ,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAE3C,SAAS,EAAE,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,SAAS,EAAE,CAAkB;QAEhE,WAAW,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE;QAEjD,WAAW,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE;QAEjD,eAAe,EAAE,CAAC,CAAC,IAA+C,EAAE,EAAE,CACpE,OAAO,CAAC,MAAM,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,CAAwB;QAEhE,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,GAAoC,CAAC;KAClG,CAAC,CAAA;IAEF,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,GAAsB,EAAiB,EAAE;IAC3E,IAAI,GAAG,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAClC,OAAO,GAAG,CAAC,OAAO,CAAgB,aAAa,CAAC,CAAA;IAClD,CAAC;IACD,MAAM,OAAO,GAAG,iBAAiB,EAAE,CAAA;IACnC,GAAG,CAAC,eAAe,CAAC,OAAO,CAAC,CAAA;IAC5B,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
package/build/types.d.ts CHANGED
@@ -1,14 +1,61 @@
1
- import type { InitializedService } from "@owlmeans/context";
2
- export type { ComponentType } from 'react';
1
+ import type { InitializedService, BasicContext } from "@owlmeans/context";
2
+ import type { ComponentType } from 'react';
3
+ export type { ComponentType };
3
4
  export type LibraryRouter = unknown;
4
- export type RouteObject = unknown;
5
- export type RouterProvider = React.ComponentType<any>;
5
+ /**
6
+ * Neutral route intermediate representation. Shape-compatible with react-router's
7
+ * `RouteObject` so that `createBrowserRouter` accepts it directly AND the OwlMeans
8
+ * matcher can consume the very same objects.
9
+ */
10
+ export interface RouteObject {
11
+ index?: boolean;
12
+ path?: string;
13
+ children?: RouteObject[];
14
+ Component?: ComponentType;
15
+ }
16
+ export type RouterProvider = ComponentType<any>;
17
+ /**
18
+ * Environment descriptor used by the cascade to select a routing plugin.
19
+ * Pre-designed for SSR: an SSR plugin selects on `ssr`/`request`; the browser
20
+ * plugin is the universal fallback in the browser.
21
+ */
22
+ export interface RouterEnv {
23
+ hasWindow: boolean;
24
+ ssr: boolean;
25
+ request?: unknown;
26
+ }
27
+ /**
28
+ * A pluggable routing mechanic. Multiple plugins may be registered on a single
29
+ * router service; the active one is chosen by cascade (priority + `match`).
30
+ */
31
+ export interface RouterPlugin {
32
+ alias: string;
33
+ /** Higher wins among matching plugins. Defaults to 0. */
34
+ priority?: number;
35
+ /** Free-form tag, e.g. 'browser' | 'ssr' | 'memory'. */
36
+ mode?: string;
37
+ /** Selector; `undefined` means "always applicable". */
38
+ match?: (env: RouterEnv, ctx?: BasicContext<any>) => boolean;
39
+ /** Compile the neutral route IR into a plugin-specific library router. */
40
+ compile: (routes: RouteObject[], ctx?: BasicContext<any>) => LibraryRouter | Promise<LibraryRouter>;
41
+ provider: () => RouterProvider;
42
+ outlet: () => ComponentType;
43
+ useParams: UseParamsHook;
44
+ useLocation: UseLocationHook;
45
+ useNavigate: UseNavigateHook;
46
+ useSearchParams: UseSearchParamsHook;
47
+ }
6
48
  export interface RouterService extends InitializedService {
7
- outlet: () => React.ComponentType;
49
+ registerPlugin: (plugin: RouterPlugin) => void;
50
+ /** Select the active plugin for the given (or default) environment. */
51
+ plugin: (env?: RouterEnv) => RouterPlugin;
52
+ outlet: () => ComponentType;
8
53
  provider: () => RouterProvider;
9
54
  useParams: UseParamsHook;
10
55
  useLocation: UseLocationHook;
11
56
  useNavigate: UseNavigateHook;
57
+ useSearchParams: UseSearchParamsHook;
58
+ compile: (routes: RouteObject[]) => LibraryRouter | Promise<LibraryRouter>;
12
59
  }
13
60
  export type RouteParams = Record<string, string | undefined>;
14
61
  export interface UseParamsHook {
@@ -26,7 +73,7 @@ export interface Location<State = any> extends Path {
26
73
  export interface UseLocationHook {
27
74
  (): Location;
28
75
  }
29
- interface NavigateOptions {
76
+ export interface NavigateOptions {
30
77
  replace?: boolean;
31
78
  state?: any;
32
79
  preventScrollReset?: boolean;
@@ -41,4 +88,13 @@ export interface NavigateFunction {
41
88
  export interface UseNavigateHook {
42
89
  (): NavigateFunction;
43
90
  }
91
+ export interface SetSearchParams {
92
+ (next: URLSearchParams | Record<string, string> | ((prev: URLSearchParams) => URLSearchParams), options?: {
93
+ replace?: boolean;
94
+ state?: any;
95
+ }): void;
96
+ }
97
+ export interface UseSearchParamsHook {
98
+ (init?: URLSearchParams | Record<string, string>): [URLSearchParams, SetSearchParams];
99
+ }
44
100
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC3D,YAAY,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAE1C,MAAM,MAAM,aAAa,GAAG,OAAO,CAAA;AAEnC,MAAM,MAAM,WAAW,GAAG,OAAO,CAAA;AAEjC,MAAM,MAAM,cAAc,GAAG,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;AAErD,MAAM,WAAW,aAAc,SAAQ,kBAAkB;IACvD,MAAM,EAAE,MAAM,KAAK,CAAC,aAAa,CAAA;IACjC,QAAQ,EAAE,MAAM,cAAc,CAAA;IAC9B,SAAS,EAAE,aAAa,CAAA;IACxB,WAAW,EAAE,eAAe,CAAA;IAC5B,WAAW,EAAE,eAAe,CAAA;CAC7B;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;AAE5D,MAAM,WAAW,aAAa;IAC5B,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,KAAK,CAAC,CAAA;CAC3C;AAED,MAAM,WAAW,IAAI;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,QAAQ,CAAC,KAAK,GAAG,GAAG,CAAE,SAAQ,IAAI;IACjD,KAAK,EAAE,KAAK,CAAA;IAEZ,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,QAAQ,CAAA;CACb;AAED,UAAU,eAAe;IACvB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,GAAG,CAAA;IACX,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7E,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,gBAAgB,CAAA;CACrB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/router",
3
- "version": "0.1.10",
3
+ "version": "0.1.14",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -21,17 +21,17 @@
21
21
  }
22
22
  },
23
23
  "dependencies": {
24
- "@owlmeans/context": "^0.1.10"
24
+ "@owlmeans/context": "^0.1.14"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "react": "*"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@owlmeans/dep-config": "workspace:*",
31
- "@types/node": "^24.10.1",
32
- "@types/react": "^19.2.7",
33
- "nodemon": "^3.1.11",
34
- "typescript": "^6.0.2"
31
+ "@types/node": "^26.1.0",
32
+ "@types/react": "^19.2.17",
33
+ "nodemon": "^3.1.14",
34
+ "typescript": "^6.0.3"
35
35
  },
36
36
  "publishConfig": {
37
37
  "access": "public"
package/src/consts.ts CHANGED
@@ -1,4 +1,10 @@
1
1
 
2
2
  export const DEFAULT_ALIAS = 'router-service'
3
3
 
4
- export const ROUTER_SERVICE = DEFAULT_ALIAS
4
+ export const ROUTER_SERVICE = DEFAULT_ALIAS
5
+
6
+ /** Default plugin priority; higher-priority plugins win the cascade. */
7
+ export const DEFAULT_ROUTER_PRIORITY = 0
8
+
9
+ /** Config-record id for declaring a preferred router plugin via `clientPlugin`. */
10
+ export const ROUTER_PLUGIN = 'router-plugin'
package/src/env.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { RouterEnv } from './types.js'
2
+
3
+ /**
4
+ * Default routing environment. In the browser `hasWindow` is true and `ssr` is
5
+ * false, so the browser plugin (which matches always) is selected. An SSR host
6
+ * will later provide a per-request env (with `ssr: true` and a `request`) so an
7
+ * SSR plugin registered at a higher priority wins.
8
+ *
9
+ * This is the single source of environment truth for cascade selection — do not
10
+ * bake `typeof window` checks into a plugin's `match`.
11
+ */
12
+ export const defaultRouterEnv = (): RouterEnv => {
13
+ const hasWindow = typeof window !== 'undefined'
14
+ return { hasWindow, ssr: !hasWindow }
15
+ }
package/src/index.ts CHANGED
@@ -2,3 +2,5 @@
2
2
  export type * from './types.js'
3
3
  export * from './consts.js'
4
4
  export * from './service.js'
5
+ export * from './matcher.js'
6
+ export * from './env.js'
package/src/matcher.ts ADDED
@@ -0,0 +1,149 @@
1
+ import type { RouteObject, RouteParams } from './types.js'
2
+
3
+ /**
4
+ * Pure, DOM-free route matcher. Supports exactly the route-description subset used
5
+ * across the OwlMeans projects: static segments, `:param` dynamic segments, nested
6
+ * (parent/child) routes and index routes. Splat (`*`) and optional (`:x?`) segments
7
+ * are intentionally not implemented yet — a seam is reserved so they can be added
8
+ * without changing the public shape.
9
+ *
10
+ * Segment weights mirror react-router's ranking intuition (static beats dynamic,
11
+ * more specific / deeper beats shallower).
12
+ */
13
+
14
+ const SEP = '/'
15
+ const PARAM = ':'
16
+ const SPLAT = '*'
17
+
18
+ const STATIC_WEIGHT = 10
19
+ const PARAM_WEIGHT = 4
20
+ const INDEX_WEIGHT = 2
21
+ const EMPTY_WEIGHT = 1
22
+
23
+ type SegmentKind = 'static' | 'param' | 'splat'
24
+
25
+ export interface PatternSegment {
26
+ kind: SegmentKind
27
+ /** literal value for static, param name for param */
28
+ value: string
29
+ }
30
+
31
+ export interface RouteBranch {
32
+ /** root → node chain; drives outlet depth in the renderer */
33
+ chain: RouteObject[]
34
+ segments: PatternSegment[]
35
+ /** true when the terminal node is an index route */
36
+ index: boolean
37
+ score: number
38
+ }
39
+
40
+ export interface RouteMatch {
41
+ route: RouteObject
42
+ params: RouteParams
43
+ pathname: string
44
+ }
45
+
46
+ export const splitPath = (path?: string): string[] =>
47
+ (path ?? '').split(SEP).filter(part => part.length > 0)
48
+
49
+ export const segmentsOf = (path?: string): PatternSegment[] =>
50
+ splitPath(path).map(part => {
51
+ if (part === SPLAT) return { kind: 'splat', value: SPLAT }
52
+ if (part.startsWith(PARAM)) return { kind: 'param', value: part.slice(PARAM.length) }
53
+ return { kind: 'static', value: part }
54
+ })
55
+
56
+ /**
57
+ * Flatten a route tree into ranked-matchable branches. Every leaf and every node
58
+ * carrying an index child produces a branch.
59
+ */
60
+ export const flattenRoutes = (
61
+ routes: RouteObject[],
62
+ parentChain: RouteObject[] = [],
63
+ parentSegments: PatternSegment[] = []
64
+ ): RouteBranch[] => {
65
+ const branches: RouteBranch[] = []
66
+
67
+ for (const route of routes) {
68
+ const chain = [...parentChain, route]
69
+
70
+ if (route.index === true) {
71
+ // An index route contributes an empty terminal at the parent's path.
72
+ branches.push({ chain, segments: parentSegments, index: true, score: 0 })
73
+ continue
74
+ }
75
+
76
+ const segments = [...parentSegments, ...segmentsOf(route.path)]
77
+ const children = route.children ?? []
78
+
79
+ if (children.length === 0) {
80
+ branches.push({ chain, segments, index: false, score: 0 })
81
+ } else {
82
+ branches.push(...flattenRoutes(children, chain, segments))
83
+ }
84
+ }
85
+
86
+ return branches
87
+ }
88
+
89
+ const scoreBranch = (branch: RouteBranch): number => {
90
+ let score = branch.segments.reduce((acc, seg) => {
91
+ if (seg.kind === 'static') return acc + STATIC_WEIGHT
92
+ if (seg.kind === 'param') return acc + PARAM_WEIGHT
93
+ return acc + EMPTY_WEIGHT
94
+ }, 0)
95
+ if (branch.segments.length === 0) score += EMPTY_WEIGHT
96
+ if (branch.index) score += INDEX_WEIGHT
97
+ return score
98
+ }
99
+
100
+ /** Rank branches so the most specific match is tried first. Stable within equal score. */
101
+ export const rankRouteBranches = (branches: RouteBranch[]): RouteBranch[] =>
102
+ branches
103
+ .map(branch => ({ ...branch, score: scoreBranch(branch) }))
104
+ .map((branch, index) => ({ branch, index }))
105
+ .sort((a, b) => (b.branch.score - a.branch.score) || (a.index - b.index))
106
+ .map(({ branch }) => branch)
107
+
108
+ const matchBranch = (branch: RouteBranch, parts: string[]): RouteParams | null => {
109
+ if (branch.segments.some(seg => seg.kind === 'splat')) {
110
+ throw new Error('router matcher: splat (*) segments are not implemented yet')
111
+ }
112
+ // Exact-length match (no splats/optionals in the supported subset).
113
+ if (branch.segments.length !== parts.length) return null
114
+
115
+ const params: RouteParams = {}
116
+ for (let i = 0; i < branch.segments.length; i++) {
117
+ const seg = branch.segments[i]
118
+ const part = parts[i]
119
+ if (seg.kind === 'static') {
120
+ if (seg.value.toLowerCase() !== part.toLowerCase()) return null
121
+ } else {
122
+ params[seg.value] = decodeURIComponent(part)
123
+ }
124
+ }
125
+ return params
126
+ }
127
+
128
+ /**
129
+ * Match a pathname against ranked branches. Returns the winning branch as a
130
+ * root→leaf chain of `RouteMatch` (params merged across the whole chain so every
131
+ * depth sees the full leaf param set, matching react-router's `useParams`).
132
+ */
133
+ export const matchRoutes = (branches: RouteBranch[], pathname: string): RouteMatch[] | null => {
134
+ const parts = splitPath(pathname)
135
+ const ranked = rankRouteBranches(branches)
136
+
137
+ for (const branch of ranked) {
138
+ const params = matchBranch(branch, parts)
139
+ if (params == null) continue
140
+
141
+ return branch.chain.map(route => ({
142
+ route,
143
+ params,
144
+ pathname: SEP + parts.join(SEP)
145
+ }))
146
+ }
147
+
148
+ return null
149
+ }
package/src/service.ts CHANGED
@@ -1,29 +1,71 @@
1
1
  import { createService } from "@owlmeans/context"
2
+ import type { BasicContext } from "@owlmeans/context"
2
3
  import { DEFAULT_ALIAS } from "./consts.js"
3
- import type { RouterService } from "./types"
4
+ import { defaultRouterEnv } from "./env.js"
5
+ import type {
6
+ RouterService, RouterPlugin, UseParamsHook, UseSearchParamsHook
7
+ } from "./types.js"
4
8
 
9
+ /**
10
+ * Router service = plugin host. It holds a registry of routing plugins and, on
11
+ * every facade call, resolves the active plugin by cascade (priority desc, first
12
+ * whose `match(env)` is truthy) and delegates to it.
13
+ *
14
+ * The facade methods stay ordinary writable properties (never getters) so that
15
+ * non-plugin implementations — e.g. the native router — can keep overriding them
16
+ * directly, exactly as `web-router` did before the plugin model.
17
+ */
5
18
  export const makeRouterService = (alias: string = DEFAULT_ALIAS): RouterService => {
6
- const service = createService<RouterService>(alias, {
7
- outlet: () => {
8
- throw new Error('Router Outlet is not implemented')
9
- },
19
+ const plugins: RouterPlugin[] = []
10
20
 
11
- provider: () => {
12
- throw new Error('Router Provider is not implemented')
21
+ const service: RouterService = createService<RouterService>(alias, {
22
+ registerPlugin: plugin => {
23
+ const existing = plugins.findIndex(candidate => candidate.alias === plugin.alias)
24
+ if (existing >= 0) {
25
+ plugins.splice(existing, 1)
26
+ }
27
+ plugins.push(plugin)
28
+ plugins.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
13
29
  },
14
30
 
15
- useParams: () => {
16
- throw new Error('Router useParams is not implemented')
31
+ plugin: env => {
32
+ const environment = env ?? defaultRouterEnv()
33
+ const ctx = service.ctx as BasicContext<any> | undefined
34
+ const plugin = plugins.find(candidate => candidate.match?.(environment, ctx) ?? true)
35
+ if (plugin == null) {
36
+ throw new Error('router: no plugin matches the current environment')
37
+ }
38
+ return plugin
17
39
  },
18
40
 
19
- useLocation: () => {
20
- throw new Error('Router useLocation is not implemented')
21
- },
41
+ outlet: () => service.plugin().outlet(),
42
+
43
+ provider: () => service.plugin().provider(),
44
+
45
+ useParams: (() => service.plugin().useParams()) as UseParamsHook,
46
+
47
+ useLocation: () => service.plugin().useLocation(),
22
48
 
23
- useNavigate: () => {
24
- throw new Error('Router useNavigate is not implemented')
25
- }
49
+ useNavigate: () => service.plugin().useNavigate(),
50
+
51
+ useSearchParams: ((init?: URLSearchParams | Record<string, string>) =>
52
+ service.plugin().useSearchParams(init)) as UseSearchParamsHook,
53
+
54
+ compile: routes => service.plugin().compile(routes, service.ctx as BasicContext<any> | undefined)
26
55
  })
27
56
 
28
57
  return service
29
- }
58
+ }
59
+
60
+ /**
61
+ * Idempotently obtain the router service on a context, registering an empty host
62
+ * if none exists yet. Plugin packages call this before `registerPlugin`.
63
+ */
64
+ export const ensureRouterService = (ctx: BasicContext<any>): RouterService => {
65
+ if (ctx.hasService(DEFAULT_ALIAS)) {
66
+ return ctx.service<RouterService>(DEFAULT_ALIAS)
67
+ }
68
+ const service = makeRouterService()
69
+ ctx.registerService(service)
70
+ return service
71
+ }
package/src/types.ts CHANGED
@@ -1,18 +1,70 @@
1
- import type { InitializedService } from "@owlmeans/context"
2
- export type { ComponentType } from 'react'
1
+ import type { InitializedService, BasicContext } from "@owlmeans/context"
2
+ import type { ComponentType } from 'react'
3
+ export type { ComponentType }
3
4
 
4
5
  export type LibraryRouter = unknown
5
6
 
6
- export type RouteObject = unknown
7
+ /**
8
+ * Neutral route intermediate representation. Shape-compatible with react-router's
9
+ * `RouteObject` so that `createBrowserRouter` accepts it directly AND the OwlMeans
10
+ * matcher can consume the very same objects.
11
+ */
12
+ export interface RouteObject {
13
+ index?: boolean
14
+ path?: string
15
+ children?: RouteObject[]
16
+ Component?: ComponentType
17
+ }
18
+
19
+ export type RouterProvider = ComponentType<any>
20
+
21
+ /**
22
+ * Environment descriptor used by the cascade to select a routing plugin.
23
+ * Pre-designed for SSR: an SSR plugin selects on `ssr`/`request`; the browser
24
+ * plugin is the universal fallback in the browser.
25
+ */
26
+ export interface RouterEnv {
27
+ hasWindow: boolean
28
+ ssr: boolean
29
+ request?: unknown
30
+ }
7
31
 
8
- export type RouterProvider = React.ComponentType<any>
32
+ /**
33
+ * A pluggable routing mechanic. Multiple plugins may be registered on a single
34
+ * router service; the active one is chosen by cascade (priority + `match`).
35
+ */
36
+ export interface RouterPlugin {
37
+ alias: string
38
+ /** Higher wins among matching plugins. Defaults to 0. */
39
+ priority?: number
40
+ /** Free-form tag, e.g. 'browser' | 'ssr' | 'memory'. */
41
+ mode?: string
42
+ /** Selector; `undefined` means "always applicable". */
43
+ match?: (env: RouterEnv, ctx?: BasicContext<any>) => boolean
44
+ /** Compile the neutral route IR into a plugin-specific library router. */
45
+ compile: (routes: RouteObject[], ctx?: BasicContext<any>) => LibraryRouter | Promise<LibraryRouter>
46
+ provider: () => RouterProvider
47
+ outlet: () => ComponentType
48
+ useParams: UseParamsHook
49
+ useLocation: UseLocationHook
50
+ useNavigate: UseNavigateHook
51
+ useSearchParams: UseSearchParamsHook
52
+ }
9
53
 
10
54
  export interface RouterService extends InitializedService {
11
- outlet: () => React.ComponentType
55
+ registerPlugin: (plugin: RouterPlugin) => void
56
+ /** Select the active plugin for the given (or default) environment. */
57
+ plugin: (env?: RouterEnv) => RouterPlugin
58
+ // Facade — every method delegates to the active plugin.
59
+ // NOTE: these remain plain writable instance properties so that alternative
60
+ // implementations (e.g. the native router) can monkey-patch them directly.
61
+ outlet: () => ComponentType
12
62
  provider: () => RouterProvider
13
63
  useParams: UseParamsHook
14
64
  useLocation: UseLocationHook
15
65
  useNavigate: UseNavigateHook
66
+ useSearchParams: UseSearchParamsHook
67
+ compile: (routes: RouteObject[]) => LibraryRouter | Promise<LibraryRouter>
16
68
  }
17
69
 
18
70
  export type RouteParams = Record<string, string | undefined>
@@ -37,7 +89,7 @@ export interface UseLocationHook {
37
89
  (): Location
38
90
  }
39
91
 
40
- interface NavigateOptions {
92
+ export interface NavigateOptions {
41
93
  replace?: boolean
42
94
  state?: any
43
95
  preventScrollReset?: boolean
@@ -53,4 +105,15 @@ export interface NavigateFunction {
53
105
 
54
106
  export interface UseNavigateHook {
55
107
  (): NavigateFunction
56
- }
108
+ }
109
+
110
+ export interface SetSearchParams {
111
+ (
112
+ next: URLSearchParams | Record<string, string> | ((prev: URLSearchParams) => URLSearchParams),
113
+ options?: { replace?: boolean; state?: any }
114
+ ): void
115
+ }
116
+
117
+ export interface UseSearchParamsHook {
118
+ (init?: URLSearchParams | Record<string, string>): [URLSearchParams, SetSearchParams]
119
+ }
package/tsconfig.json CHANGED
@@ -7,5 +7,5 @@
7
7
  "rootDir": "./src/",
8
8
  "outDir": "./build/"
9
9
  },
10
- "exclude": ["./dist/**/*", "./build/**/*", "./*.ts"]
10
+ "exclude": ["./dist/**/*", "./build/**/*", "./tests/**/*", "./*.ts"]
11
11
  }
package/build/.gitkeep DELETED
File without changes