sinwan-router 0.1.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +232 -8
- package/dist/components.d.ts +44 -0
- package/dist/hooks.d.ts +74 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +918 -0
- package/dist/index.js.map +16 -0
- package/dist/jsx-routes.d.ts +35 -0
- package/dist/link-nav.d.ts +28 -0
- package/dist/matcher.d.ts +75 -0
- package/dist/outlet.d.ts +21 -0
- package/dist/router.d.ts +75 -0
- package/dist/types.d.ts +72 -0
- package/package.json +36 -44
package/README.md
CHANGED
|
@@ -1,15 +1,239 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="left">
|
|
2
|
+
<table border="0" width="100%" align="center">
|
|
3
|
+
<tr>
|
|
4
|
+
<td width="150" align="left">
|
|
5
|
+
<img src="https://avatars.githubusercontent.com/u/252437356?s=400&v=4" alt="Sinwan Logo" width="150" />
|
|
6
|
+
</td>
|
|
7
|
+
<td align="left">
|
|
8
|
+
<h1>Sinwan Router</h1>
|
|
9
|
+
<p>Client-side router for Sinwan apps — declarative matching, params, lazy loading, and prefetching on Sinwan's reactivity system.</p>
|
|
10
|
+
<p>
|
|
11
|
+
<a href="https://github.com/sinwanjs/sinwan-router/stargazers"><img src="https://img.shields.io/github/stars/sinwanjs/sinwan-router.svg?color=ffce3b&label=stars&logo=github" alt="GitHub stars" /></a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/sinwan-router"><img src="https://img.shields.io/npm/dm/sinwan-router?color=42b883&label=downloads&logo=npm" alt="NPM Downloads" /></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/sinwan-router"><img src="https://img.shields.io/npm/l/sinwan-router?color=35495e&label=license" alt="License" /></a>
|
|
14
|
+
</p>
|
|
15
|
+
</td>
|
|
16
|
+
</tr>
|
|
17
|
+
</table>
|
|
18
|
+
</div>
|
|
2
19
|
|
|
3
|
-
|
|
20
|
+
<br clear="both" />
|
|
4
21
|
|
|
5
|
-
|
|
6
|
-
|
|
22
|
+
Sinwan Router matches paths with a radix tree (no regex compilation), keeps route state in signals, and remounts only the outlet depth that actually changed.
|
|
23
|
+
|
|
24
|
+
## Why Sinwan Router?
|
|
25
|
+
|
|
26
|
+
- **Radix-tree matching** — static segments, `:params`, and `*wildcards` in O(depth). Static routes win over params; params win over catch-alls.
|
|
27
|
+
- **Fine-grained reactivity** — `path`, `matched`, `params`, and `searchParams` are signals/computeds. Navigation updates the DOM that reads them.
|
|
28
|
+
- **Nested outlets** — parent layouts stay mounted when only a child param changes. Key identity is depth-local, not the full URL.
|
|
29
|
+
- **Lazy routes + prefetch** — `() => import("./Page")` or `lazy(...)`. `Link` prefetch and `RouterOutlet` share one per-router cache.
|
|
30
|
+
- **Browser-safe links** — `Link` leaves `_blank`, `download`, modifier clicks (`ctrl` / `meta` / `shift` / `alt`), external URLs, and same-page hashes to the browser.
|
|
31
|
+
|
|
32
|
+
Peer dependency: **sinwan >= 1.3.0** (`<Key>` remounts when `cache` is omitted).
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm install sinwan-router sinwan
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
bun add sinwan-router sinwan
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
JSX still comes from Sinwan:
|
|
45
|
+
|
|
46
|
+
```jsonc
|
|
47
|
+
{
|
|
48
|
+
"compilerOptions": {
|
|
49
|
+
"jsx": "react-jsx",
|
|
50
|
+
"jsxImportSource": "sinwan",
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { cc, provide } from "sinwan/component";
|
|
59
|
+
import {
|
|
60
|
+
createRouter,
|
|
61
|
+
RouterKey,
|
|
62
|
+
RouterOutlet,
|
|
63
|
+
Link,
|
|
64
|
+
NavLink,
|
|
65
|
+
} from "sinwan-router";
|
|
66
|
+
|
|
67
|
+
const router = createRouter([
|
|
68
|
+
{ path: "/", component: Home },
|
|
69
|
+
{ path: "/users/:id", component: UserProfile },
|
|
70
|
+
{ path: "/about", component: () => import("./About.tsx") },
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
const App = cc(() => {
|
|
74
|
+
provide(RouterKey, router);
|
|
75
|
+
return (
|
|
76
|
+
<div>
|
|
77
|
+
<nav>
|
|
78
|
+
<Link href="/">Home</Link>
|
|
79
|
+
<NavLink href="/about" activeClass="active">
|
|
80
|
+
About
|
|
81
|
+
</NavLink>
|
|
82
|
+
</nav>
|
|
83
|
+
<RouterOutlet />
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
There is no `RouterProvider` component. Provide the instance with `provide(RouterKey, router)` so hooks and outlet components can `inject` it. Or declare routes in JSX with `<Routes>` / `<Route>` — same fields as `RouteDefinition`.
|
|
90
|
+
|
|
91
|
+
## JSX routes
|
|
92
|
+
|
|
93
|
+
`<Route>` uses the native route fields: `path`, `component`, `children`, and `meta`. Nested `<Route>` elements become `children` on the parent definition.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { Routes, Route, Link } from "sinwan-router";
|
|
97
|
+
|
|
98
|
+
const App = cc(() => (
|
|
99
|
+
<Routes>
|
|
100
|
+
<nav>
|
|
101
|
+
<Link href="/">Home</Link>
|
|
102
|
+
<Link href="/users/1">Profile</Link>
|
|
103
|
+
</nav>
|
|
104
|
+
<Route path="/" component={Home} />
|
|
105
|
+
<Route path="/users/:id" component={UserProfile} />
|
|
106
|
+
<Route path="/app" component={AppLayout} meta={{ title: "App" }}>
|
|
107
|
+
<Route path="/" component={AppIndex} />
|
|
108
|
+
<Route path="settings" component={Settings} />
|
|
109
|
+
</Route>
|
|
110
|
+
</Routes>
|
|
111
|
+
));
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`Routes` collects `<Route>` children into a router, provides `RouterKey` to layout siblings (`Link`, `NavLink`), and renders a `<RouterOutlet />`. Put links **inside** `<Routes>` — they need that context. You can still pass `fallback`, `notFound`, `error`, and `initialPath`.
|
|
115
|
+
|
|
116
|
+
## Routes
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { createRouter, lazy } from "sinwan-router";
|
|
120
|
+
|
|
121
|
+
const router = createRouter([
|
|
122
|
+
{ path: "/", component: Home },
|
|
123
|
+
{ path: "/users/:id", component: UserProfile },
|
|
124
|
+
{ path: "/files/*path", component: Files },
|
|
125
|
+
{
|
|
126
|
+
path: "/app",
|
|
127
|
+
component: AppLayout,
|
|
128
|
+
children: [
|
|
129
|
+
{ path: "/", component: AppIndex },
|
|
130
|
+
{ path: "settings", component: Settings },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
{ path: "/*", component: NotFound },
|
|
134
|
+
]);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- `/:id` — named param, URI-decoded, available from `useParams()`.
|
|
138
|
+
- `/*` or `/*path` — catch-all. Unnamed wildcards land in `params._wildcard`.
|
|
139
|
+
- Nested `children` join onto the parent path. A child `path: "/"` is an index route.
|
|
140
|
+
- Route components are not passed params as props. Read them with hooks.
|
|
141
|
+
|
|
142
|
+
Zero-arg factories (`() => import("./Page")`) are treated as lazy loaders. Functions that take props are treated as components. You can also brand a loader:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
{ path: "/admin", component: lazy(() => import("./Admin.tsx")) }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Navigation
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
router.navigate("/users/42");
|
|
152
|
+
router.navigate("/login", { replace: true, state: { from: router.path } });
|
|
153
|
+
router.prefetch("/about");
|
|
154
|
+
router.dispose();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`prefetch` and `RouterOutlet` share the same instance cache. `dispose()` removes the `popstate` listener and drops that cache.
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
<Link href="/users/1" class="nav">
|
|
161
|
+
Profile
|
|
162
|
+
</Link>
|
|
163
|
+
|
|
164
|
+
<Link href="https://sinwanjs.com" target="_blank" rel="noreferrer">
|
|
165
|
+
Docs
|
|
166
|
+
</Link>
|
|
167
|
+
|
|
168
|
+
<NavLink href="/users" activeClass="active" class="nav">
|
|
169
|
+
Users
|
|
170
|
+
</NavLink>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`NavLink` is active on an exact match or a nested prefix (`/users` matches `/users/42`). Prefetch on hover is on by default; pass `prefetch={false}` to disable.
|
|
174
|
+
|
|
175
|
+
## Outlet
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
<RouterOutlet />
|
|
179
|
+
<RouterOutlet depth={1} fallback={Loading} notFound={NotFound} error={LoadError} />
|
|
7
180
|
```
|
|
8
181
|
|
|
9
|
-
|
|
182
|
+
| Prop | Role |
|
|
183
|
+
| ----------- | -------------------------------------------------------------------- |
|
|
184
|
+
| `depth` | `0` is the root match; `1` is the first nested child, and so on |
|
|
185
|
+
| `fallback` | Shown while a lazy import is in flight |
|
|
186
|
+
| `notFound` | Shown at depth `0` when nothing matches; nested outlets render nothing |
|
|
187
|
+
| `error` | Shown when a lazy import rejects (`{ error?: unknown }` props) |
|
|
10
188
|
|
|
11
|
-
|
|
12
|
-
|
|
189
|
+
Put `<RouterOutlet depth={1} />` inside a parent layout to render the child.
|
|
190
|
+
|
|
191
|
+
## Hooks
|
|
192
|
+
|
|
193
|
+
All hooks require `provide(RouterKey, router)` on an ancestor.
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
import {
|
|
197
|
+
useRouter,
|
|
198
|
+
useParams,
|
|
199
|
+
useSearchParams,
|
|
200
|
+
useRoute,
|
|
201
|
+
useNavigate,
|
|
202
|
+
usePath,
|
|
203
|
+
} from "sinwan-router";
|
|
204
|
+
|
|
205
|
+
const params = useParams(); // { id: "42" }
|
|
206
|
+
const search = useSearchParams(); // URLSearchParams
|
|
207
|
+
const path = usePath(); // "/users/42"
|
|
208
|
+
const route = useRoute(); // MatchedRoute | null
|
|
209
|
+
const navigate = useNavigate();
|
|
210
|
+
navigate("/about", { replace: true });
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Features
|
|
214
|
+
|
|
215
|
+
- Radix-tree matcher: `createRadixTree`, `radixInsert`, `radixSearch`, `buildPath`, `normalizePath`
|
|
216
|
+
- Router: `createRouter`, `navigate`, `prefetch`, `resolveComponent`, `dispose`, `lazy` / `isLazyComponent`
|
|
217
|
+
- Components: `Link`, `NavLink`, `RouterOutlet`, `Routes`, `Route`
|
|
218
|
+
- Hooks: `useRouter`, `useParams`, `useSearchParams`, `useRoute`, `useNavigate`, `usePath`, `RouterKey`
|
|
219
|
+
|
|
220
|
+
## Documentation
|
|
221
|
+
|
|
222
|
+
- [Documentation v1](https://sinwanjs.com)
|
|
223
|
+
- [Sinwan](https://github.com/sinwanjs/sinwan)
|
|
224
|
+
|
|
225
|
+
## Development
|
|
226
|
+
|
|
227
|
+
```sh
|
|
228
|
+
bun test
|
|
229
|
+
bun run typecheck
|
|
230
|
+
bun run build
|
|
13
231
|
```
|
|
14
232
|
|
|
15
|
-
|
|
233
|
+
## Author
|
|
234
|
+
|
|
235
|
+
Mohammed Ben Cheikh
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sinwan Router — Components
|
|
3
|
+
*
|
|
4
|
+
* Link, NavLink, and RouterOutlet that use useRouter() internally.
|
|
5
|
+
* The router instance must be provided via provide(RouterKey, router).
|
|
6
|
+
*/
|
|
7
|
+
import type { SinwanComponent, SinwanNode } from "sinwan/component";
|
|
8
|
+
export interface LinkProps {
|
|
9
|
+
href: string;
|
|
10
|
+
prefetch?: boolean;
|
|
11
|
+
class?: string;
|
|
12
|
+
children?: SinwanNode;
|
|
13
|
+
target?: string;
|
|
14
|
+
download?: string | boolean;
|
|
15
|
+
rel?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Link — client-side navigation anchor.
|
|
19
|
+
* Uses the router from context (provide(RouterKey, router)).
|
|
20
|
+
*/
|
|
21
|
+
export declare const Link: SinwanComponent<LinkProps>;
|
|
22
|
+
export interface NavLinkProps extends LinkProps {
|
|
23
|
+
activeClass?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* NavLink — Link with active state styling.
|
|
27
|
+
* Uses the router from context.
|
|
28
|
+
*/
|
|
29
|
+
export declare const NavLink: SinwanComponent<NavLinkProps>;
|
|
30
|
+
export interface RouterOutletProps {
|
|
31
|
+
fallback?: SinwanComponent;
|
|
32
|
+
notFound?: SinwanComponent;
|
|
33
|
+
error?: SinwanComponent<{
|
|
34
|
+
error?: unknown;
|
|
35
|
+
}>;
|
|
36
|
+
/** Depth of this outlet (0 = top-level, 1 = nested in parent route). */
|
|
37
|
+
depth?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* RouterOutlet — renders the matched route's component at a given depth.
|
|
41
|
+
* For nested routes, place a <RouterOutlet /> inside the parent component.
|
|
42
|
+
* Uses the router from context.
|
|
43
|
+
*/
|
|
44
|
+
export declare const RouterOutlet: SinwanComponent<RouterOutletProps>;
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sinwan Router — Hooks
|
|
3
|
+
*
|
|
4
|
+
* Reactive hooks for accessing router state inside components.
|
|
5
|
+
* Must be called within a component that has a Router in context.
|
|
6
|
+
*/
|
|
7
|
+
import type { InjectionKey } from "sinwan/component";
|
|
8
|
+
import type { Router } from "./router.ts";
|
|
9
|
+
import type { NavigateOptions } from "./types.ts";
|
|
10
|
+
/** Injection key for the router instance. */
|
|
11
|
+
export declare const RouterKey: InjectionKey<Router>;
|
|
12
|
+
/**
|
|
13
|
+
* Access the router instance.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const router = useRouter();
|
|
18
|
+
* router.navigate("/about");
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function useRouter(): Router;
|
|
22
|
+
/**
|
|
23
|
+
* Get the current route parameters.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* const params = useParams();
|
|
28
|
+
* console.log(params.id); // "42" for /users/42
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function useParams(): Record<string, string>;
|
|
32
|
+
/**
|
|
33
|
+
* Get the current search params (query string).
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* const searchParams = useSearchParams();
|
|
38
|
+
* console.log(searchParams.get("q")); // "hello" for /search?q=hello
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function useSearchParams(): URLSearchParams;
|
|
42
|
+
/**
|
|
43
|
+
* Get the current matched route.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* const route = useRoute();
|
|
48
|
+
* if (route) {
|
|
49
|
+
* console.log(route.path, route.params);
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function useRoute(): import("./types.ts").MatchedRoute | null;
|
|
54
|
+
/**
|
|
55
|
+
* Get a navigate function.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```tsx
|
|
59
|
+
* const navigate = useNavigate();
|
|
60
|
+
* navigate("/users/42");
|
|
61
|
+
* navigate("/login", { replace: true });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function useNavigate(): (to: string, options?: NavigateOptions) => void;
|
|
65
|
+
/**
|
|
66
|
+
* Get the current path.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* const path = usePath();
|
|
71
|
+
* console.log(path); // "/users/42"
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function usePath(): string;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sinwan Router — Public API
|
|
3
|
+
*
|
|
4
|
+
* Client-side router for Sinwan apps, built on Sinwan's reactivity system.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { createRouter, RouterKey, RouterOutlet, Link } from "sinwan-router";
|
|
9
|
+
* import { cc, provide } from "sinwan/component";
|
|
10
|
+
*
|
|
11
|
+
* const router = createRouter([
|
|
12
|
+
* { path: "/", component: Home },
|
|
13
|
+
* { path: "/users/:id", component: UserProfile },
|
|
14
|
+
* { path: "/about", component: () => import("./About.tsx") },
|
|
15
|
+
* ]);
|
|
16
|
+
*
|
|
17
|
+
* const App = cc(() => {
|
|
18
|
+
* provide(RouterKey, router);
|
|
19
|
+
* return (
|
|
20
|
+
* <div>
|
|
21
|
+
* <nav>
|
|
22
|
+
* <Link href="/">Home</Link>
|
|
23
|
+
* <Link href="/about">About</Link>
|
|
24
|
+
* </nav>
|
|
25
|
+
* <RouterOutlet />
|
|
26
|
+
* </div>
|
|
27
|
+
* );
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export { createRouter, Router, isLazyComponent, lazy } from "./router.ts";
|
|
32
|
+
export { createRadixTree, radixInsert, radixSearch, buildPath, normalizePath, extractSearchParams, } from "./matcher.ts";
|
|
33
|
+
export type { RadixNode, RouteData, MatchResult } from "./matcher.ts";
|
|
34
|
+
export { Link, NavLink, RouterOutlet } from "./components.tsx";
|
|
35
|
+
export type { LinkProps, NavLinkProps, RouterOutletProps, } from "./components.tsx";
|
|
36
|
+
export { Route, Routes, collectRouteDefinitions, isRouteElement, layoutNodes, } from "./jsx-routes.tsx";
|
|
37
|
+
export type { RouteProps, RoutesProps } from "./jsx-routes.tsx";
|
|
38
|
+
export { useRouter, useParams, useSearchParams, useRoute, useNavigate, usePath, RouterKey, } from "./hooks.ts";
|
|
39
|
+
export type { RouteDefinition, RouteComponent, LazyComponent, MatchedRoute, NavigateOptions, RouterContextValue, LazyPeek, } from "./types.ts";
|