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 CHANGED
@@ -1,15 +1,239 @@
1
- # sinwan-router
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
- To install dependencies:
20
+ <br clear="both" />
4
21
 
5
- ```bash
6
- bun install
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
- To run:
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
- ```bash
12
- bun run src/index.ts
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
- This project was created using `bun init` in bun v1.3.13. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
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>;
@@ -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;
@@ -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";