@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.
- package/agent-meta/instructions/router.instructions.md +21 -11
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/router/SKILL.md +39 -14
- package/build/consts.d.ts +4 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +4 -0
- package/build/consts.js.map +1 -1
- package/build/env.d.ts +12 -0
- package/build/env.d.ts.map +1 -0
- package/build/env.js +14 -0
- package/build/env.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/matcher.d.ts +37 -0
- package/build/matcher.d.ts.map +1 -0
- package/build/matcher.js +111 -0
- package/build/matcher.js.map +1 -0
- package/build/service.d.ts +16 -1
- package/build/service.d.ts.map +1 -1
- package/build/service.js +45 -13
- package/build/service.js.map +1 -1
- package/build/types.d.ts +62 -6
- package/build/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/consts.ts +7 -1
- package/src/env.ts +15 -0
- package/src/index.ts +2 -0
- package/src/matcher.ts +149 -0
- package/src/service.ts +58 -16
- package/src/types.ts +70 -7
- package/tsconfig.json +1 -1
- package/build/.gitkeep +0 -0
|
@@ -1,30 +1,40 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "How to use @owlmeans/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.
|
|
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`
|
|
17
|
-
|
|
|
18
|
-
|
|
|
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
|
-
|
|
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 {
|
|
24
|
-
|
|
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
|
|
40
|
+
- `@owlmeans/context`; peer `react` (types only — no react-router, no DOM here)
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@owlmeans/router",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"generatedAt": "2026-
|
|
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 —
|
|
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
|
+
**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`
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
|
27
|
-
const
|
|
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
|
-
-
|
|
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
|
package/build/consts.d.ts.map
CHANGED
|
@@ -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
|
package/build/consts.js.map
CHANGED
|
@@ -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
|
package/build/env.js.map
ADDED
|
@@ -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
package/build/index.d.ts.map
CHANGED
|
@@ -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
package/build/index.js.map
CHANGED
|
@@ -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"}
|
package/build/matcher.js
ADDED
|
@@ -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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"}
|
package/build/service.d.ts
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
-
import type {
|
|
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
|
package/build/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
useLocation: () =>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
package/build/service.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
package/build/types.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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.
|
|
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": "^
|
|
32
|
-
"@types/react": "^19.2.
|
|
33
|
-
"nodemon": "^3.1.
|
|
34
|
-
"typescript": "^6.0.
|
|
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
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
|
|
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
|
|
7
|
-
outlet: () => {
|
|
8
|
-
throw new Error('Router Outlet is not implemented')
|
|
9
|
-
},
|
|
19
|
+
const plugins: RouterPlugin[] = []
|
|
10
20
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/build/.gitkeep
DELETED
|
File without changes
|