@owlmeans/web-router 0.1.18-rc.5 → 0.1.18-rc.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -21
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-router/SKILL.md +9 -8
- package/build/browser/chain.d.ts +4 -4
- package/build/browser/chain.js +4 -4
- package/build/browser/plugin.js +2 -2
- package/build/browser/plugin.js.map +1 -1
- package/build/browser/provider.js +2 -2
- package/build/browser/provider.js.map +1 -1
- package/package.json +11 -11
- package/src/browser/chain.tsx +4 -4
- package/src/browser/plugin.ts +2 -2
- package/src/browser/provider.tsx +2 -2
- package/tests/harness/mount.tsx +1 -1
- package/tests/routing.spec.ts +10 -10
- package/build/.gitkeep +0 -0
package/README.md
CHANGED
|
@@ -1,46 +1,149 @@
|
|
|
1
1
|
# @owlmeans/web-router
|
|
2
2
|
|
|
3
|
-
The default OwlMeans in-browser routing plugin
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
- Provides `useParams`, `useLocation`, `useNavigate`, `outlet()`, `provider()` via the service
|
|
12
|
-
- Used internally by `@owlmeans/web-client`'s `makeContext` — not typically used directly
|
|
3
|
+
The default OwlMeans in-browser routing plugin: standard URL routing over the browser History API,
|
|
4
|
+
a pure route matcher from `@owlmeans/router`, and a React provider, outlet and hooks — with no
|
|
5
|
+
third-party router. It registers itself as a `RouterPlugin` on the `@owlmeans/router` host, and
|
|
6
|
+
`@owlmeans/web-client` / `@owlmeans/web-panel` call `appendWebRouter` in their `makeContext`, so a
|
|
7
|
+
web app gets it without importing anything. An app imports it directly only for a custom context
|
|
8
|
+
setup or to reach the plugin's components and hooks outside the `context.router()` facade. To route
|
|
9
|
+
with react-router instead, add `@owlmeans/web-router-react-router` (higher priority, wins the
|
|
10
|
+
cascade); an SSR or React Native host is a plugin of its own.
|
|
13
11
|
|
|
14
12
|
## Installation
|
|
15
13
|
|
|
16
14
|
```bash
|
|
17
|
-
bun add @owlmeans/web-router
|
|
15
|
+
bun add @owlmeans/web-router@^0.1.18-rc.51
|
|
18
16
|
```
|
|
19
17
|
|
|
18
|
+
`react` is a peer dependency.
|
|
19
|
+
|
|
20
|
+
## Concepts
|
|
21
|
+
|
|
22
|
+
- **Router host** — the `RouterService` of `@owlmeans/router`, registered once per context
|
|
23
|
+
(`ensureRouterService`). Its facade (`provider`, `outlet`, `compile`, `useParams`, `useLocation`,
|
|
24
|
+
`useNavigate`, `useSearchParams`) delegates to the active plugin.
|
|
25
|
+
- **Browser plugin** — `makeBrowserRouterPlugin()`: alias `BROWSER_ROUTER`, priority 0, mode
|
|
26
|
+
`'browser'`, `match` always true — the universal browser fallback any higher-priority plugin
|
|
27
|
+
outranks.
|
|
28
|
+
- **Compile** — `compile(routes)` flattens and ranks the neutral `RouteObject[]`
|
|
29
|
+
(`{ index?, path?, children?, Component? }`) into matchable branches; synchronous.
|
|
30
|
+
- **Route chain** — `BrowserRouterProvider` re-matches on every history change and renders the chain
|
|
31
|
+
through `RouteChain`; each `<Outlet/>` renders the next-deeper match. A match without a
|
|
32
|
+
`Component` is a pass-through, as in react-router's implicit outlet.
|
|
33
|
+
- **Route syntax** — static segments, `:param`, nested routes and index routes; a static segment
|
|
34
|
+
outranks a `:param` sibling. No splat or optional segments.
|
|
35
|
+
|
|
20
36
|
## Usage
|
|
21
37
|
|
|
22
|
-
|
|
38
|
+
### Default wiring
|
|
39
|
+
|
|
40
|
+
Nothing to do with `@owlmeans/web-client` or `@owlmeans/web-panel`. A context assembled by hand
|
|
41
|
+
registers the plugin itself:
|
|
23
42
|
|
|
24
43
|
```typescript
|
|
25
44
|
import { appendWebRouter } from '@owlmeans/web-router'
|
|
26
45
|
|
|
27
|
-
appendWebRouter(context)
|
|
46
|
+
appendWebRouter(context) // ensures the router host, then registers the browser plugin
|
|
28
47
|
```
|
|
29
48
|
|
|
30
|
-
|
|
49
|
+
### Read the route in a component
|
|
50
|
+
|
|
51
|
+
Inside an OwlMeans app, reach the hooks through the facade so the active plugin answers:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { useContext } from '@owlmeans/client'
|
|
55
|
+
|
|
56
|
+
export const UserScreen = () => {
|
|
57
|
+
const context = useContext()
|
|
58
|
+
const { id } = context.router().useParams<{ id: string }>()
|
|
59
|
+
const [query] = context.router().useSearchParams()
|
|
60
|
+
const navigate = context.router().useNavigate()
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<button onClick={() => navigate(`/users/${id}/edit`, { state: { from: query.get('tab') } })}>
|
|
64
|
+
edit
|
|
65
|
+
</button>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
```
|
|
31
69
|
|
|
32
|
-
|
|
70
|
+
`navigate(to, { replace?, state? })` takes a path string, a `{ pathname?, search?, hash? }`
|
|
71
|
+
object, or a number for history steps; an absolute `http…` string is a full-page navigation.
|
|
72
|
+
The setter returned by `useSearchParams` (`set(next, { replace?, state? })`) rewrites the search part
|
|
73
|
+
of the current path.
|
|
33
74
|
|
|
34
|
-
|
|
75
|
+
### Use the plugin standalone
|
|
35
76
|
|
|
36
|
-
|
|
77
|
+
The same components and hooks are exported directly — what the package's own browser harness does:
|
|
37
78
|
|
|
38
|
-
|
|
79
|
+
```tsx
|
|
80
|
+
import { createRoot } from 'react-dom/client'
|
|
81
|
+
import type { RouteObject } from '@owlmeans/router'
|
|
82
|
+
import { makeBrowserRouterPlugin, Outlet, useParams } from '@owlmeans/web-router'
|
|
39
83
|
|
|
40
|
-
|
|
84
|
+
const Layout = () => <main><Outlet /></main>
|
|
85
|
+
const Home = () => <p>home</p>
|
|
86
|
+
const User = () => <p>user {useParams().id}</p>
|
|
41
87
|
|
|
42
|
-
|
|
88
|
+
const routes: RouteObject[] = [
|
|
89
|
+
{ path: '', Component: Layout, children: [
|
|
90
|
+
{ index: true, Component: Home },
|
|
91
|
+
{ path: 'users', children: [{ path: ':id', Component: User }] }, // component-less group passes through
|
|
92
|
+
] },
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
const plugin = makeBrowserRouterPlugin()
|
|
96
|
+
const Provider = plugin.provider()
|
|
97
|
+
createRoot(document.getElementById('root')!).render(<Provider router={plugin.compile(routes)} />)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Switch to react-router
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { appendReactRouter } from '@owlmeans/web-router-react-router'
|
|
104
|
+
|
|
105
|
+
appendReactRouter(context) // priority 100 outranks the browser plugin
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## API
|
|
109
|
+
|
|
110
|
+
| Symbol | Kind | Purpose |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `appendWebRouter(ctx)` | function | `ensureRouterService(ctx)` + register the browser plugin; returns the context |
|
|
113
|
+
| `makeWebRouterService()` | function | back-compat: a new router host pre-loaded with the browser plugin |
|
|
114
|
+
| `makeBrowserRouterPlugin()` | function | the `RouterPlugin` itself |
|
|
115
|
+
| `BROWSER_ROUTER` | const | `'owlmeans-browser-router'` — the plugin alias |
|
|
116
|
+
| `BrowserRouterProvider` | component | `{ router: LibraryRouter }` — subscribes to history and renders the match chain |
|
|
117
|
+
| `Outlet` | component | renders the next-deeper match |
|
|
118
|
+
| `RouteChain` | component | `{ depth }` — renders the chain from a depth, skipping component-less matches |
|
|
119
|
+
| `useParams`, `useLocation`, `useNavigate`, `useSearchParams` | hooks | the plugin's hooks; throw outside the provider |
|
|
120
|
+
| `createBrowserHistory()` | function | `BrowserHistory` over `window.history` + `popstate`; an inert stub without `window` |
|
|
121
|
+
| `OwlLibraryRouter` | type | `{ routes, branches }` — what `compile` returns |
|
|
122
|
+
| `RouterState` | type | `{ location, matches, navigate }` — the provider's React context value |
|
|
123
|
+
| `BrowserHistory` | type | `location`, `push`, `replace`, `go`, `listen` |
|
|
124
|
+
|
|
125
|
+
## Common pitfalls
|
|
126
|
+
|
|
127
|
+
- A hook used outside `BrowserRouterProvider` throws
|
|
128
|
+
`owlmeans-browser-router: router hook used outside of the router provider`.
|
|
129
|
+
- Calling the plugin's hooks directly bypasses the cascade; in an app, use `context.router().…` so a
|
|
130
|
+
switched plugin keeps working.
|
|
131
|
+
- Splat (`*`) and optional segments are not supported; declare explicit routes.
|
|
132
|
+
- `makeWebRouterService()` builds a separate host; prefer `appendWebRouter`, which reuses the
|
|
133
|
+
context's host.
|
|
134
|
+
- `noRouter` works on `<App>` only — `<PanelApp>` accepts the prop by type but still mounts the
|
|
135
|
+
router.
|
|
136
|
+
|
|
137
|
+
## Related packages
|
|
138
|
+
|
|
139
|
+
- [`@owlmeans/router`](../router) — the router host, `RouterPlugin` contract and pure matcher
|
|
140
|
+
- [`@owlmeans/web-router-react-router`](../web-router-react-router) — opt-in react-router v7 plugin
|
|
141
|
+
- [`@owlmeans/client`](../client) — builds `RouteObject[]` from the entrypoint tree and exposes `context.router()`
|
|
43
142
|
- [`@owlmeans/web-client`](../web-client) — calls `appendWebRouter` inside `makeContext`
|
|
143
|
+
- [`@owlmeans/web-panel`](../web-panel) — the panel layer, wired the same way
|
|
144
|
+
|
|
145
|
+
The `web-router` skill describes this plugin; the `router-plugins` skill covers the plugin contract,
|
|
146
|
+
cascade selection and authoring a new plugin.
|
|
44
147
|
|
|
45
148
|
<!-- owlmeans:agent-guidance:start -->
|
|
46
149
|
## Agent guidance
|
|
@@ -50,7 +153,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
50
153
|
your project's skill store (`.agents/skills/`):
|
|
51
154
|
|
|
52
155
|
```sh
|
|
53
|
-
npx @owlmeans/agent-skills
|
|
156
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.52
|
|
54
157
|
```
|
|
55
158
|
|
|
56
159
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/web-router",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18-rc.51",
|
|
5
|
+
"generatedAt": "2026-10-08T21:27:54.319Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,19 +8,20 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/web-router
|
|
9
9
|
|
|
10
10
|
**Layer:** Web (React), build level L2
|
|
11
|
-
**Install:** `"@owlmeans/web-router": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/web-router": "^0.1.18-rc.51"` in `dependencies`
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
This package **is the default OwlMeans in-browser router** — a minimal, react-router-free
|
|
14
|
+
implementation of standard URL routing over the browser History API. It registers itself as a
|
|
15
|
+
`RouterPlugin` on the `@owlmeans/router` host. (react-router is the separate opt-in
|
|
16
|
+
`@owlmeans/web-router-react-router` package.)
|
|
17
17
|
|
|
18
18
|
## Key Exports
|
|
19
19
|
|
|
20
20
|
| Export | Description |
|
|
21
21
|
|--------|-------------|
|
|
22
22
|
| `appendWebRouter(ctx)` | Register the OwlMeans browser plugin on a context's router host (idempotent via `ensureRouterService`). Called transparently by `@owlmeans/web-client`. |
|
|
23
|
-
| `makeBrowserRouterPlugin()` | The `RouterPlugin` itself (alias `
|
|
23
|
+
| `makeBrowserRouterPlugin()` | The `RouterPlugin` itself (alias `BROWSER_ROUTER`, priority 0). |
|
|
24
|
+
| `BROWSER_ROUTER` | The plugin's alias string, `'owlmeans-browser-router'` — name the constant rather than the literal when selecting or overriding the plugin. |
|
|
24
25
|
| `makeWebRouterService()` | Back-compat: a host pre-loaded with the browser plugin. |
|
|
25
26
|
| `BrowserRouterProvider`, `Outlet` | React provider/outlet components. |
|
|
26
27
|
| `RouteChain` | Renders the matched chain from a given depth, skipping `Component`-less matches (used by the provider and `Outlet`). |
|
|
@@ -35,8 +36,8 @@ separate opt-in `@owlmeans/web-router-react-router` package.)
|
|
|
35
36
|
chain via `RouteChain`; `<Outlet/>` renders the next-deeper match (depth tracked via context).
|
|
36
37
|
Composes unchanged with the `@owlmeans/client` route renderer (parent components emit `<Outlet/>`).
|
|
37
38
|
- **Component-less matches are pass-through** — exactly like react-router's implicit `<Outlet/>` for
|
|
38
|
-
element-less routes. `@owlmeans/client` emits `RouteObject`s with no `Component` for
|
|
39
|
-
have no handler (grouping
|
|
39
|
+
element-less routes. `@owlmeans/client` emits `RouteObject`s with no `Component` for entrypoints
|
|
40
|
+
that have no handler (grouping entrypoints such as `client-authentication` →
|
|
40
41
|
`client-authentication:authentication`), so rendering only the exact node at a depth would blank
|
|
41
42
|
the whole subtree. `RouteChain` walks down to the first match that has a `Component`, and the
|
|
42
43
|
`OutletContext` depth it publishes is that match's depth, so nested outlets stay aligned.
|
package/build/browser/chain.d.ts
CHANGED
|
@@ -4,11 +4,11 @@ import type { FC } from 'react';
|
|
|
4
4
|
*
|
|
5
5
|
* A matched route that carries no `Component` is a **pass-through**: it renders
|
|
6
6
|
* the next-deeper match, exactly like react-router's implicit `<Outlet/>` for
|
|
7
|
-
* element-less routes. OwlMeans
|
|
8
|
-
* (e.g. `client-authentication` → `client-authentication:authentication`)
|
|
9
|
-
* no handler, so `@owlmeans/client` emits `RouteObject`s without a `Component`
|
|
7
|
+
* element-less routes. OwlMeans entrypoint trees depend on this — grouping
|
|
8
|
+
* entrypoints (e.g. `client-authentication` → `client-authentication:authentication`)
|
|
9
|
+
* have no handler, so `@owlmeans/client` emits `RouteObject`s without a `Component`
|
|
10
10
|
* for them. Rendering only the exact node at `depth` would leave the whole
|
|
11
|
-
* subtree blank whenever a group sits above the screen
|
|
11
|
+
* subtree blank whenever a group sits above the screen entrypoint.
|
|
12
12
|
*/
|
|
13
13
|
export declare const RouteChain: FC<{
|
|
14
14
|
depth: number;
|
package/build/browser/chain.js
CHANGED
|
@@ -5,11 +5,11 @@ import { RouterStateContext, OutletContext } from './context.js';
|
|
|
5
5
|
*
|
|
6
6
|
* A matched route that carries no `Component` is a **pass-through**: it renders
|
|
7
7
|
* the next-deeper match, exactly like react-router's implicit `<Outlet/>` for
|
|
8
|
-
* element-less routes. OwlMeans
|
|
9
|
-
* (e.g. `client-authentication` → `client-authentication:authentication`)
|
|
10
|
-
* no handler, so `@owlmeans/client` emits `RouteObject`s without a `Component`
|
|
8
|
+
* element-less routes. OwlMeans entrypoint trees depend on this — grouping
|
|
9
|
+
* entrypoints (e.g. `client-authentication` → `client-authentication:authentication`)
|
|
10
|
+
* have no handler, so `@owlmeans/client` emits `RouteObject`s without a `Component`
|
|
11
11
|
* for them. Rendering only the exact node at `depth` would leave the whole
|
|
12
|
-
* subtree blank whenever a group sits above the screen
|
|
12
|
+
* subtree blank whenever a group sits above the screen entrypoint.
|
|
13
13
|
*/
|
|
14
14
|
export const RouteChain = ({ depth }) => {
|
|
15
15
|
const state = useContext(RouterStateContext);
|
package/build/browser/plugin.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { routeMatcherHelper } from '@owlmeans/router';
|
|
2
2
|
import { BROWSER_ROUTER } from '../consts.js';
|
|
3
3
|
import { BrowserRouterProvider } from './provider.js';
|
|
4
4
|
import { Outlet } from './outlet.js';
|
|
@@ -15,7 +15,7 @@ export const makeBrowserRouterPlugin = () => ({
|
|
|
15
15
|
match: () => true,
|
|
16
16
|
compile: routes => ({
|
|
17
17
|
routes,
|
|
18
|
-
branches: rankRouteBranches(flattenRoutes(routes))
|
|
18
|
+
branches: routeMatcherHelper.rankRouteBranches(routeMatcherHelper.flattenRoutes(routes))
|
|
19
19
|
}),
|
|
20
20
|
provider: () => BrowserRouterProvider,
|
|
21
21
|
outlet: () => Outlet,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/browser/plugin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/browser/plugin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAGjF;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAiB,EAAE,CAAC,CAAC;IAC1D,KAAK,EAAE,cAAc;IACrB,QAAQ,EAAE,CAAC;IACX,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI;IACjB,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QAClB,MAAM;QACN,QAAQ,EAAE,kBAAkB,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;KAC5C,CAAA;IAC9C,QAAQ,EAAE,GAAG,EAAE,CAAC,qBAAqB;IACrC,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM;IACpB,SAAS;IACT,WAAW;IACX,WAAW;IACX,eAAe;CAChB,CAAC,CAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createElement, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { routeMatcherHelper } from '@owlmeans/router';
|
|
3
3
|
import { createBrowserHistory } from './history.js';
|
|
4
4
|
import { RouterStateContext } from './context.js';
|
|
5
5
|
import { RouteChain } from './chain.js';
|
|
@@ -14,7 +14,7 @@ export const BrowserRouterProvider = ({ router }) => {
|
|
|
14
14
|
const history = historyRef.current;
|
|
15
15
|
const [location, setLocation] = useState(() => history.location);
|
|
16
16
|
useEffect(() => history.listen(setLocation), []);
|
|
17
|
-
const matches = useMemo(() => matchRoutes(lib.branches, location.pathname) ?? [], [lib, location.pathname]);
|
|
17
|
+
const matches = useMemo(() => routeMatcherHelper.matchRoutes(lib.branches, location.pathname) ?? [], [lib, location.pathname]);
|
|
18
18
|
const navigate = useMemo(() => ((to, options) => {
|
|
19
19
|
if (typeof to === 'number') {
|
|
20
20
|
history.go(to);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/browser/provider.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE3E,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/browser/provider.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAGvC;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAkC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE;IACjF,MAAM,GAAG,GAAG,MAA0B,CAAA;IACtC,MAAM,UAAU,GAAG,MAAM,CAAC,oBAAoB,EAAE,CAAC,CAAA;IACjD,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAA;IAElC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAW,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IAE1E,SAAS,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,EAAE,CAAC,CAAA;IAEhD,MAAM,OAAO,GAAG,OAAO,CACrB,GAAG,EAAE,CAAC,kBAAkB,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,EAC3E,CAAC,GAAG,EAAE,QAAQ,CAAC,QAAQ,CAAC,CACzB,CAAA;IAED,MAAM,QAAQ,GAAG,OAAO,CAAmB,GAAG,EAAE,CAAC,CAAC,CAAC,EAA2E,EAAE,OAA4C,EAAE,EAAE;QAC9K,IAAI,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;YAC3B,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;YACd,OAAM;QACR,CAAC;QACD,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YACpD,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,EAAE,CAAA;YACzB,OAAM;QACR,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,EAAE,KAAK,QAAQ;YACjC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,EAAE,CAAA;QAC5D,IAAI,OAAO,EAAE,OAAO,EAAE,CAAC;YACrB,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;QACtC,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,CAAA;QACpC,CAAC;IACH,CAAC,CAAqB,EAAE,CAAC,OAAO,CAAC,CAAC,CAAA;IAElC,OAAO,aAAa,CAClB,kBAAkB,CAAC,QAAQ,EAC3B,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAC1C,aAAa,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CACxC,CAAA;AACH,CAAC,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/web-router",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.51",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -26,20 +26,20 @@
|
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@owlmeans/dep-config": "workspace:*",
|
|
29
|
-
"@owlmeans/test-ui": "^0.1.18-rc.
|
|
30
|
-
"@types/node": "^26.
|
|
31
|
-
"@types/react": "^19.
|
|
32
|
-
"@vitejs/plugin-react": "^6.
|
|
29
|
+
"@owlmeans/test-ui": "^0.1.18-rc.51",
|
|
30
|
+
"@types/node": "^26.6.4",
|
|
31
|
+
"@types/react": "^19.3.0",
|
|
32
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
33
33
|
"nodemon": "^3.1.14",
|
|
34
|
-
"playwright": "^1.
|
|
35
|
-
"react": "^19.
|
|
36
|
-
"react-dom": "^19.
|
|
34
|
+
"playwright": "^1.63.0",
|
|
35
|
+
"react": "^19.3.0",
|
|
36
|
+
"react-dom": "^19.3.0",
|
|
37
37
|
"typescript": "^7.0.2",
|
|
38
|
-
"vite": "^8.
|
|
38
|
+
"vite": "^8.3.2"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
42
|
-
"@owlmeans/router": "^0.1.18-rc.
|
|
41
|
+
"@owlmeans/context": "^0.1.18-rc.34",
|
|
42
|
+
"@owlmeans/router": "^0.1.18-rc.34"
|
|
43
43
|
},
|
|
44
44
|
"publishConfig": {
|
|
45
45
|
"access": "public"
|
package/src/browser/chain.tsx
CHANGED
|
@@ -7,11 +7,11 @@ import { RouterStateContext, OutletContext } from './context.js'
|
|
|
7
7
|
*
|
|
8
8
|
* A matched route that carries no `Component` is a **pass-through**: it renders
|
|
9
9
|
* the next-deeper match, exactly like react-router's implicit `<Outlet/>` for
|
|
10
|
-
* element-less routes. OwlMeans
|
|
11
|
-
* (e.g. `client-authentication` → `client-authentication:authentication`)
|
|
12
|
-
* no handler, so `@owlmeans/client` emits `RouteObject`s without a `Component`
|
|
10
|
+
* element-less routes. OwlMeans entrypoint trees depend on this — grouping
|
|
11
|
+
* entrypoints (e.g. `client-authentication` → `client-authentication:authentication`)
|
|
12
|
+
* have no handler, so `@owlmeans/client` emits `RouteObject`s without a `Component`
|
|
13
13
|
* for them. Rendering only the exact node at `depth` would leave the whole
|
|
14
|
-
* subtree blank whenever a group sits above the screen
|
|
14
|
+
* subtree blank whenever a group sits above the screen entrypoint.
|
|
15
15
|
*/
|
|
16
16
|
export const RouteChain: FC<{ depth: number }> = ({ depth }) => {
|
|
17
17
|
const state = useContext(RouterStateContext)
|
package/src/browser/plugin.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LibraryRouter, RouterPlugin } from '@owlmeans/router'
|
|
2
|
-
import {
|
|
2
|
+
import { routeMatcherHelper } from '@owlmeans/router'
|
|
3
3
|
import { BROWSER_ROUTER } from '../consts.js'
|
|
4
4
|
import { BrowserRouterProvider } from './provider.js'
|
|
5
5
|
import { Outlet } from './outlet.js'
|
|
@@ -18,7 +18,7 @@ export const makeBrowserRouterPlugin = (): RouterPlugin => ({
|
|
|
18
18
|
match: () => true,
|
|
19
19
|
compile: routes => ({
|
|
20
20
|
routes,
|
|
21
|
-
branches: rankRouteBranches(flattenRoutes(routes))
|
|
21
|
+
branches: routeMatcherHelper.rankRouteBranches(routeMatcherHelper.flattenRoutes(routes))
|
|
22
22
|
} satisfies OwlLibraryRouter as LibraryRouter),
|
|
23
23
|
provider: () => BrowserRouterProvider,
|
|
24
24
|
outlet: () => Outlet,
|
package/src/browser/provider.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FC } from 'react'
|
|
2
2
|
import { createElement, useEffect, useMemo, useRef, useState } from 'react'
|
|
3
3
|
import type { LibraryRouter, Location, NavigateFunction } from '@owlmeans/router'
|
|
4
|
-
import {
|
|
4
|
+
import { routeMatcherHelper } from '@owlmeans/router'
|
|
5
5
|
import { createBrowserHistory } from './history.js'
|
|
6
6
|
import { RouterStateContext } from './context.js'
|
|
7
7
|
import { RouteChain } from './chain.js'
|
|
@@ -22,7 +22,7 @@ export const BrowserRouterProvider: FC<{ router: LibraryRouter }> = ({ router })
|
|
|
22
22
|
useEffect(() => history.listen(setLocation), [])
|
|
23
23
|
|
|
24
24
|
const matches = useMemo(
|
|
25
|
-
() => matchRoutes(lib.branches, location.pathname) ?? [],
|
|
25
|
+
() => routeMatcherHelper.matchRoutes(lib.branches, location.pathname) ?? [],
|
|
26
26
|
[lib, location.pathname]
|
|
27
27
|
)
|
|
28
28
|
|
package/tests/harness/mount.tsx
CHANGED
|
@@ -49,7 +49,7 @@ const routes: RouteObject[] = [
|
|
|
49
49
|
]
|
|
50
50
|
},
|
|
51
51
|
// component-less grouping node at the very top of the chain — this is the shape
|
|
52
|
-
// `@owlmeans/client` emits for
|
|
52
|
+
// `@owlmeans/client` emits for entrypoints without a handler (e.g. `client-authentication`)
|
|
53
53
|
{ path: 'group', children: [{ path: 'leaf', Component: Leaf }] }
|
|
54
54
|
]
|
|
55
55
|
|
package/tests/routing.spec.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { afterAll, describe, expect, test } from 'bun:test'
|
|
2
|
-
import { closeBrowser, withPage } from '@owlmeans/test-ui'
|
|
3
2
|
import { HARNESS_URL } from './context.js'
|
|
3
|
+
import { browserHelper } from '@owlmeans/test-ui'
|
|
4
4
|
|
|
5
5
|
const at = (path: string) => new URL(path, HARNESS_URL).toString()
|
|
6
6
|
|
|
7
|
-
afterAll(async () => { await closeBrowser() })
|
|
7
|
+
afterAll(async () => { await browserHelper.closeBrowser() })
|
|
8
8
|
|
|
9
9
|
describe('@owlmeans/web-router — OwlMeans browser routing (chromium e2e)', () => {
|
|
10
10
|
test('renders the index route inside the layout at /', async () => {
|
|
11
|
-
await withPage(async page => {
|
|
11
|
+
await browserHelper.withPage(async page => {
|
|
12
12
|
await page.goto(HARNESS_URL)
|
|
13
13
|
await page.waitForSelector('#home')
|
|
14
14
|
expect(await page.locator('#layout').isVisible()).toBe(true)
|
|
@@ -17,7 +17,7 @@ describe('@owlmeans/web-router — OwlMeans browser routing (chromium e2e)', ()
|
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
test('programmatic navigate resolves nested route, params and search', async () => {
|
|
20
|
-
await withPage(async page => {
|
|
20
|
+
await browserHelper.withPage(async page => {
|
|
21
21
|
await page.goto(HARNESS_URL)
|
|
22
22
|
await page.waitForSelector('#nav-user')
|
|
23
23
|
await page.click('#nav-user')
|
|
@@ -33,7 +33,7 @@ describe('@owlmeans/web-router — OwlMeans browser routing (chromium e2e)', ()
|
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
test('nested index route renders at /users', async () => {
|
|
36
|
-
await withPage(async page => {
|
|
36
|
+
await browserHelper.withPage(async page => {
|
|
37
37
|
await page.goto(HARNESS_URL)
|
|
38
38
|
await page.waitForSelector('#nav-users')
|
|
39
39
|
await page.click('#nav-users')
|
|
@@ -44,7 +44,7 @@ describe('@owlmeans/web-router — OwlMeans browser routing (chromium e2e)', ()
|
|
|
44
44
|
})
|
|
45
45
|
|
|
46
46
|
test('static segment outranks the :id dynamic sibling', async () => {
|
|
47
|
-
await withPage(async page => {
|
|
47
|
+
await browserHelper.withPage(async page => {
|
|
48
48
|
await page.goto(at('users/settings'))
|
|
49
49
|
await page.waitForSelector('#settings')
|
|
50
50
|
expect(await page.locator('#settings').textContent()).toContain('settings-screen')
|
|
@@ -53,7 +53,7 @@ describe('@owlmeans/web-router — OwlMeans browser routing (chromium e2e)', ()
|
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
test('deep-links directly to a :param URL (synchronous initial match)', async () => {
|
|
56
|
-
await withPage(async page => {
|
|
56
|
+
await browserHelper.withPage(async page => {
|
|
57
57
|
await page.goto(at('users/7?token=xyz'))
|
|
58
58
|
await page.waitForSelector('#user')
|
|
59
59
|
expect(await page.locator('#user').textContent()).toBe('user:7:token:xyz')
|
|
@@ -61,7 +61,7 @@ describe('@owlmeans/web-router — OwlMeans browser routing (chromium e2e)', ()
|
|
|
61
61
|
})
|
|
62
62
|
|
|
63
63
|
test('renders through a component-less group at the top of the chain', async () => {
|
|
64
|
-
await withPage(async page => {
|
|
64
|
+
await browserHelper.withPage(async page => {
|
|
65
65
|
await page.goto(at('group/leaf'))
|
|
66
66
|
await page.waitForSelector('#leaf')
|
|
67
67
|
expect(await page.locator('#leaf').textContent()).toContain('leaf-screen')
|
|
@@ -71,7 +71,7 @@ describe('@owlmeans/web-router — OwlMeans browser routing (chromium e2e)', ()
|
|
|
71
71
|
})
|
|
72
72
|
|
|
73
73
|
test('outlet falls through a component-less group in the middle of the chain', async () => {
|
|
74
|
-
await withPage(async page => {
|
|
74
|
+
await browserHelper.withPage(async page => {
|
|
75
75
|
await page.goto(at('users/nested/deep'))
|
|
76
76
|
await page.waitForSelector('#deep')
|
|
77
77
|
expect(await page.locator('#layout').isVisible()).toBe(true)
|
|
@@ -81,7 +81,7 @@ describe('@owlmeans/web-router — OwlMeans browser routing (chromium e2e)', ()
|
|
|
81
81
|
})
|
|
82
82
|
|
|
83
83
|
test('browser back/forward restores the matched route', async () => {
|
|
84
|
-
await withPage(async page => {
|
|
84
|
+
await browserHelper.withPage(async page => {
|
|
85
85
|
await page.goto(HARNESS_URL)
|
|
86
86
|
await page.waitForSelector('#home')
|
|
87
87
|
await page.click('#nav-user')
|
package/build/.gitkeep
DELETED
|
File without changes
|