routexiz 0.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/LICENSE +21 -0
- package/README.md +566 -0
- package/build/components/Link.d.ts +14 -0
- package/build/components/LinkCore.d.ts +23 -0
- package/build/components/NavLink.d.ts +18 -0
- package/build/components/RouteTree.d.ts +4 -0
- package/build/components/index.d.ts +3 -0
- package/build/index.d.ts +2 -0
- package/build/index.esm.js +1 -0
- package/build/index.js +1 -0
- package/build/router/contexts.d.ts +25 -0
- package/build/router/core.d.ts +29 -0
- package/build/router/index.d.ts +6 -0
- package/build/router/resource.d.ts +11 -0
- package/build/router/setting.d.ts +8 -0
- package/build/router/types.d.ts +39 -0
- package/build/router/utils.d.ts +8 -0
- package/package.json +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 delpikye-v
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
# đšī¸ routexiz
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/routexiz) 
|
|
4
|
+
|
|
5
|
+
<a href="https://codesandbox.io/p/sandbox/2lslks" target="_blank">LIVE EXAMPLE</a>
|
|
6
|
+
|
|
7
|
+
A lightweight, powerful, and modern router for React.
|
|
8
|
+
|
|
9
|
+
Built with:
|
|
10
|
+
- ⥠Suspense-first data loading
|
|
11
|
+
- đ§ Guards & middlewares
|
|
12
|
+
- đ Nested routing
|
|
13
|
+
- đ Prefetching & caching
|
|
14
|
+
- đ§Š Fully flexible architecture
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Why routexiz?
|
|
19
|
+
|
|
20
|
+
Traditional routing:
|
|
21
|
+
```ts
|
|
22
|
+
navigate("/users/1")
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
routexiz:
|
|
26
|
+
```ts
|
|
27
|
+
navigate("/users/:id", {
|
|
28
|
+
params: { id: 1 }
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- ⥠Suspense data loading (like React 18 philosophy)
|
|
33
|
+
- đ§ Guards (before navigation)
|
|
34
|
+
- đ§ Middleware (side-effects)
|
|
35
|
+
- đ Nested routing builder API
|
|
36
|
+
- đ Prefetch (hover + viewport)
|
|
37
|
+
- đ§Š Loader caching with TTL
|
|
38
|
+
- â Error boundary per route
|
|
39
|
+
- đ¨ Transition support
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
# Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install routexiz
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
# Quick Start
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { route, RouterProvider } from "routexiz"
|
|
55
|
+
|
|
56
|
+
route("/", () => <div>Home</div>)
|
|
57
|
+
|
|
58
|
+
export default function App() {
|
|
59
|
+
return <RouterProvider />
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
# Routing API
|
|
66
|
+
|
|
67
|
+
<b>route()</b>
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
route(path, component, config?)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
<b>builder API</b>
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
route("/", Layout, root => {
|
|
77
|
+
root.route("/dashboard", Dashboard, dash => {
|
|
78
|
+
dash.guard(fn)
|
|
79
|
+
dash.middleware(fn)
|
|
80
|
+
dash.meta({ title: "Dashboard" })
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
# Navigation
|
|
88
|
+
|
|
89
|
+
## useNavigate
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const navigate = useNavigate()
|
|
93
|
+
|
|
94
|
+
navigate("/users/:id", {
|
|
95
|
+
params: { id: 1 },
|
|
96
|
+
query: { tab: "profile" }
|
|
97
|
+
})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Link
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<Link
|
|
106
|
+
to="/users/:id"
|
|
107
|
+
params={{ id: 1 }}
|
|
108
|
+
query={{ tab: "profile" }}
|
|
109
|
+
>
|
|
110
|
+
Open User
|
|
111
|
+
</Link>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## NavLink
|
|
115
|
+
```ts
|
|
116
|
+
<NavLink
|
|
117
|
+
to="/users/:id"
|
|
118
|
+
params={{ id: 1 }}
|
|
119
|
+
className={({ isActive }) => isActive ? "active" : ""}
|
|
120
|
+
>
|
|
121
|
+
Open User
|
|
122
|
+
</NavLink>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
# Data Loading
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
route("/users/:id", User, {
|
|
131
|
+
loader: async ({ params }) => fetchUser(params.id)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Access loader data:
|
|
135
|
+
const data = useRouteData()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Access data
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const data = useRouteData()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
# Guards
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
dash.guard(({ params }) => {
|
|
150
|
+
if (!isLoggedIn()) return false
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
# Middleware
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
dash.middleware(async ({ params }) => {
|
|
160
|
+
console.log("run side effects")
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
# Prefetch
|
|
167
|
+
|
|
168
|
+
- Hover
|
|
169
|
+
- Viewport (IntersectionObserver)
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import { Link } from "routexiz"
|
|
173
|
+
|
|
174
|
+
export default function UsersList() {
|
|
175
|
+
return (
|
|
176
|
+
<div>
|
|
177
|
+
<h2>Users</h2>
|
|
178
|
+
{/* Prefetch on hover */}
|
|
179
|
+
<Link to="/users/1" params={{ id: 1 }} query={{ tab: "profile" }}>
|
|
180
|
+
User 1 (hover to prefetch)
|
|
181
|
+
</Link>
|
|
182
|
+
|
|
183
|
+
{/* Prefetch when enters viewport */}
|
|
184
|
+
<Link to="/users/2" params={{ id: 2 }} query={{ tab: "profile" }}>
|
|
185
|
+
User 2 (prefetch on viewport)
|
|
186
|
+
</Link>
|
|
187
|
+
</div>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
# Error Handling
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
route("/users/:id", User, {
|
|
198
|
+
errorBoundary: ({ error }) => <div>Error: {String(error)}</div>
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
# Fallback
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
route("/users/:id", User, {
|
|
208
|
+
fallback: <div>Loading...</div>
|
|
209
|
+
})
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
# Lazy
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import React, { Suspense } from "react"
|
|
218
|
+
import { route, RouterProvider } from "routexiz"
|
|
219
|
+
|
|
220
|
+
// Lazy load page
|
|
221
|
+
const Dashboard = React.lazy(() => import("./pages/Dashboard"))
|
|
222
|
+
|
|
223
|
+
route("/", Dashboard, {
|
|
224
|
+
fallback: <div>Loading Dashboard...</div>,
|
|
225
|
+
})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
# Cache (TTL)
|
|
231
|
+
|
|
232
|
+
## Declare route with cache
|
|
233
|
+
```ts
|
|
234
|
+
route("/users/:id", User, {
|
|
235
|
+
loader: async ({ params }) => {
|
|
236
|
+
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${params.id}`)
|
|
237
|
+
if (!res.ok) throw new Error("User not found")
|
|
238
|
+
return res.json()
|
|
239
|
+
},
|
|
240
|
+
ttl: 5000, // cache expires in 5 seconds
|
|
241
|
+
fallback: <div>Loading user...</div>,
|
|
242
|
+
errorBoundary: ({ error }) => <div>Error: {String(error)}</div>
|
|
243
|
+
})
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Access cached data inside component
|
|
247
|
+
```ts
|
|
248
|
+
import { useRouteData, useParams } from "routexiz"
|
|
249
|
+
|
|
250
|
+
function User() {
|
|
251
|
+
const data = useRouteData<any>()
|
|
252
|
+
const params = useParams()
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<div>
|
|
256
|
+
User {params.id}
|
|
257
|
+
<pre>{JSON.stringify(data, null, 2)}</pre>
|
|
258
|
+
</div>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
## Prefetch / warm up cache
|
|
264
|
+
```ts
|
|
265
|
+
import { prefetch } from "routexiz"
|
|
266
|
+
|
|
267
|
+
// Prefetch user 1 in background
|
|
268
|
+
prefetch("/users/1")
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Cleanup expired cache
|
|
272
|
+
```ts
|
|
273
|
+
import { cleanupCache } from "routexiz"
|
|
274
|
+
|
|
275
|
+
// Remove all expired resources
|
|
276
|
+
cleanupCache()
|
|
277
|
+
|
|
278
|
+
// Remove only 10 oldest expired items
|
|
279
|
+
cleanupCache(10)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
> TTL cache ensures route loaders are cached for fast navigation and automatically cleaned after expiry.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
# Global Fallback & Error
|
|
287
|
+
|
|
288
|
+
`routexiz` allows you to configure global fallback UI and global error boundaries for your entire app.
|
|
289
|
+
|
|
290
|
+
## Set Global Fallback
|
|
291
|
+
```ts
|
|
292
|
+
import { setGlobalFallback } from "routexiz"
|
|
293
|
+
|
|
294
|
+
// Enable global fallback with a custom loader
|
|
295
|
+
setGlobalFallback(true, <div>App is loading...</div>)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
- If a route does not provide its own fallback, this global fallback will be used.
|
|
299
|
+
|
|
300
|
+
- Works with Suspense loaders.
|
|
301
|
+
|
|
302
|
+
## Set Global Error
|
|
303
|
+
```ts
|
|
304
|
+
import { setGlobalError } from "routexiz"
|
|
305
|
+
|
|
306
|
+
// Enable global error handling with a custom render
|
|
307
|
+
setGlobalError(true, (error) => (
|
|
308
|
+
<div style={{ color: "red" }}>Oops! {String(error)}</div>
|
|
309
|
+
))
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
- If a route does not provide its own errorBoundary, this global error component will be used.
|
|
313
|
+
|
|
314
|
+
- Receives an error object and must return a valid React element.
|
|
315
|
+
|
|
316
|
+
## Example with RouterProvider
|
|
317
|
+
```ts
|
|
318
|
+
import React, { Suspense } from "react"
|
|
319
|
+
import { RouterProvider, setGlobalFallback, setGlobalError } from "routexiz"
|
|
320
|
+
|
|
321
|
+
setGlobalFallback(true, <div>Loading app...</div>)
|
|
322
|
+
setGlobalError(true, (error) => <div>Error occurred: {String(error)}</div>)
|
|
323
|
+
|
|
324
|
+
export default function App() {
|
|
325
|
+
return (
|
|
326
|
+
<Suspense fallback={null}>
|
|
327
|
+
<RouterProvider />
|
|
328
|
+
</Suspense>
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
- The Suspense wrapper can still override the global fallback if needed.
|
|
334
|
+
|
|
335
|
+
- Per-route fallback and errorBoundary take priority over global settings.
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
# Hooks
|
|
340
|
+
|
|
341
|
+
## useRouteData()
|
|
342
|
+
|
|
343
|
+
Returns the data loaded by the route's loader. Suspense-aware.
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
import { useRouteData } from "routexiz"
|
|
347
|
+
|
|
348
|
+
function User() {
|
|
349
|
+
const data = useRouteData<{ id: string; name: string }>()
|
|
350
|
+
return (
|
|
351
|
+
<div>
|
|
352
|
+
User {data.id}: {data.name}
|
|
353
|
+
</div>
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## useParams()
|
|
360
|
+
|
|
361
|
+
Returns the dynamic route parameters.
|
|
362
|
+
```ts
|
|
363
|
+
import { useParams } from "routexiz"
|
|
364
|
+
|
|
365
|
+
function User() {
|
|
366
|
+
const params = useParams()
|
|
367
|
+
return <div>User ID: {params.id}</div>
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## useRouteContext()
|
|
372
|
+
|
|
373
|
+
Returns the current route path.
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
import { useRouteContext } from "routexiz"
|
|
377
|
+
|
|
378
|
+
function CurrentRoute() {
|
|
379
|
+
const path = useRouteContext()
|
|
380
|
+
return <div>Current route: {path}</div>
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## useNavigation()
|
|
385
|
+
|
|
386
|
+
Returns router state: loading, current path, and pending path.
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
import { useNavigation } from "routexiz"
|
|
390
|
+
|
|
391
|
+
function NavStatus() {
|
|
392
|
+
const { loading, path, pendingPath } = useNavigation()
|
|
393
|
+
return (
|
|
394
|
+
<div>
|
|
395
|
+
{loading ? `Navigating to ${pendingPath}...` : `Current path: ${path}`}
|
|
396
|
+
</div>
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## useTransition()
|
|
402
|
+
|
|
403
|
+
Returns transition info (name and stage). Useful for page animations.
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
import { useTransition } from "routexiz"
|
|
407
|
+
|
|
408
|
+
function PageTransition() {
|
|
409
|
+
const { name, stage } = useTransition()
|
|
410
|
+
return <div>Transition {name}: {stage}</div>
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
# Full Example
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
import React from "react"
|
|
420
|
+
import { Link, NavLink, RouterProvider, route, useRouteData, useParams } from "routexiz"
|
|
421
|
+
|
|
422
|
+
/* =========================
|
|
423
|
+
PAGES
|
|
424
|
+
========================= */
|
|
425
|
+
function Home() {
|
|
426
|
+
return <div>Home Page</div>
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function About() {
|
|
430
|
+
return <div>About Page</div>
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function User() {
|
|
434
|
+
const params = useParams()
|
|
435
|
+
const data = useRouteData<any>()
|
|
436
|
+
|
|
437
|
+
return (
|
|
438
|
+
<div>
|
|
439
|
+
User Page {params.id}
|
|
440
|
+
<pre>{JSON.stringify(data, null, 2)}</pre>
|
|
441
|
+
</div>
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* =========================
|
|
446
|
+
ROUTES
|
|
447
|
+
========================= */
|
|
448
|
+
route("/", Home, root => {
|
|
449
|
+
root.route("/about", About)
|
|
450
|
+
root.route("/user/:id", User, {
|
|
451
|
+
loader: async ({ params }) => {
|
|
452
|
+
await new Promise(r => setTimeout(r, 400))
|
|
453
|
+
return { id: params.id, name: "User " + params.id }
|
|
454
|
+
},
|
|
455
|
+
fallback: <div>Loading user...</div>,
|
|
456
|
+
errorBoundary: ({ error }) => <div>Error: {String(error)}</div>
|
|
457
|
+
})
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
/* =========================
|
|
461
|
+
NAVIGATION
|
|
462
|
+
========================= */
|
|
463
|
+
function Navbar() {
|
|
464
|
+
return (
|
|
465
|
+
<nav style={{ marginBottom: 20 }}>
|
|
466
|
+
{/* Link */}
|
|
467
|
+
<Link
|
|
468
|
+
to="/"
|
|
469
|
+
className="link"
|
|
470
|
+
activeClassName="active-link"
|
|
471
|
+
style={{ marginRight: 10 }}
|
|
472
|
+
>
|
|
473
|
+
Home
|
|
474
|
+
</Link>
|
|
475
|
+
|
|
476
|
+
{/* NavLink */}
|
|
477
|
+
<NavLink
|
|
478
|
+
to="/about"
|
|
479
|
+
className={({ isActive }) => isActive ? "active-link" : "link"}
|
|
480
|
+
style={({ isActive }) => ({
|
|
481
|
+
marginRight: 10,
|
|
482
|
+
color: isActive ? "green" : "blue"
|
|
483
|
+
})}
|
|
484
|
+
>
|
|
485
|
+
About
|
|
486
|
+
</NavLink>
|
|
487
|
+
|
|
488
|
+
<NavLink
|
|
489
|
+
to="/user/1"
|
|
490
|
+
className={({ isActive }) => isActive ? "active-link" : "link"}
|
|
491
|
+
style={({ isActive }) => ({
|
|
492
|
+
marginRight: 10,
|
|
493
|
+
color: isActive ? "green" : "blue"
|
|
494
|
+
})}
|
|
495
|
+
>
|
|
496
|
+
User 1
|
|
497
|
+
</NavLink>
|
|
498
|
+
|
|
499
|
+
<NavLink
|
|
500
|
+
to="/user/2"
|
|
501
|
+
className={({ isActive }) => isActive ? "active-link" : "link"}
|
|
502
|
+
style={({ isActive }) => ({
|
|
503
|
+
color: isActive ? "green" : "blue"
|
|
504
|
+
})}
|
|
505
|
+
>
|
|
506
|
+
User 2
|
|
507
|
+
</NavLink>
|
|
508
|
+
</nav>
|
|
509
|
+
)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/* =========================
|
|
513
|
+
APP
|
|
514
|
+
========================= */
|
|
515
|
+
export default function App() {
|
|
516
|
+
return (
|
|
517
|
+
<div>
|
|
518
|
+
<h1>Link + NavLink Demo</h1>
|
|
519
|
+
<Navbar />
|
|
520
|
+
<RouterProvider />
|
|
521
|
+
</div>
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
# Comparison
|
|
529
|
+
|
|
530
|
+
`routexiz` focuses on **modern React routing** with Suspense-first `data loading`, `nested routes`, `guards`, `middleware`, and `prefetch/caching`.
|
|
531
|
+
Itâs lightweight and flexible, designed for client-side SPAs.
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
| Criteria | routexiz | React Router | TanStack Router | Remix |
|
|
535
|
+
| ------------------------- | -------- | ------------ | --------------- | ----- |
|
|
536
|
+
| Nested routes builder API | â
| â
| â
| â
|
|
|
537
|
+
| Suspense-first loaders | â
| â ī¸ | â
| â
|
|
|
538
|
+
| Guards & middleware | â
| â ī¸ | â
| â ī¸ |
|
|
539
|
+
| Prefetch / caching | â
| â | â
| â ī¸ |
|
|
540
|
+
| Error boundary per route | â
| â ī¸ | â
| â
|
|
|
541
|
+
| Transition support | â
| â | â ī¸ | â ī¸ |
|
|
542
|
+
| Lightweight & minimal | â
| â ī¸ | â ī¸ | â ī¸ |
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
# Architecture
|
|
547
|
+
|
|
548
|
+
```text
|
|
549
|
+
Link / navigate
|
|
550
|
+
â
|
|
551
|
+
matchRouteChain
|
|
552
|
+
â
|
|
553
|
+
guards â middleware
|
|
554
|
+
â
|
|
555
|
+
loader (cached)
|
|
556
|
+
â
|
|
557
|
+
Suspense
|
|
558
|
+
â
|
|
559
|
+
render tree
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
# License
|
|
565
|
+
|
|
566
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
3
|
+
to: string;
|
|
4
|
+
transition?: string;
|
|
5
|
+
replace?: boolean;
|
|
6
|
+
activeClassName?: string;
|
|
7
|
+
onNavigate?: (to: string) => void;
|
|
8
|
+
partialMatch?: boolean;
|
|
9
|
+
params?: Record<string, any>;
|
|
10
|
+
query?: Record<string, any>;
|
|
11
|
+
hash?: string;
|
|
12
|
+
disablePrefetch?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function Link(props: LinkProps): JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type UseLinkCoreOptions = {
|
|
2
|
+
to: string;
|
|
3
|
+
transition?: string;
|
|
4
|
+
replace?: boolean;
|
|
5
|
+
params?: Record<string, any>;
|
|
6
|
+
query?: Record<string, any>;
|
|
7
|
+
hash?: string;
|
|
8
|
+
partialMatch?: boolean;
|
|
9
|
+
disablePrefetch?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function useLinkCore({ to, transition, replace, params, query, hash, partialMatch, disablePrefetch, }: UseLinkCoreOptions): {
|
|
12
|
+
ref: import("react").RefObject<HTMLAnchorElement>;
|
|
13
|
+
fullPath: string;
|
|
14
|
+
isActive: boolean;
|
|
15
|
+
go: () => void;
|
|
16
|
+
};
|
|
17
|
+
export declare const LinkCore: import("react").ForwardRefExoticComponent<import("react").AnchorHTMLAttributes<HTMLAnchorElement> & {
|
|
18
|
+
fullPath: string;
|
|
19
|
+
onNavigate: () => void;
|
|
20
|
+
onPrefetch?: () => void;
|
|
21
|
+
disablePrefetch?: boolean;
|
|
22
|
+
} & import("react").RefAttributes<HTMLAnchorElement>>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface NavLinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'className' | 'style'> {
|
|
3
|
+
to: string;
|
|
4
|
+
transition?: string;
|
|
5
|
+
replace?: boolean;
|
|
6
|
+
params?: Record<string, any>;
|
|
7
|
+
query?: Record<string, any>;
|
|
8
|
+
hash?: string;
|
|
9
|
+
partialMatch?: boolean;
|
|
10
|
+
disablePrefetch?: boolean;
|
|
11
|
+
className?: string | ((opts: {
|
|
12
|
+
isActive: boolean;
|
|
13
|
+
}) => string);
|
|
14
|
+
style?: React.CSSProperties | ((opts: {
|
|
15
|
+
isActive: boolean;
|
|
16
|
+
}) => React.CSSProperties);
|
|
17
|
+
}
|
|
18
|
+
export declare function NavLink(props: NavLinkProps): JSX.Element;
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e,{createContext as n,useContext as r,Suspense as t,useState as o,useEffect as a,forwardRef as i,useRef as s}from"react";import{jsx as c,jsxs as h,Fragment as u}from"react/jsx-runtime";import l from"joinclass";const d=n(null),p=n(null),f=n(null),m=n({loading:!1,path:"",pendingPath:""}),y=n({name:"",stage:"idle"});function g(){const e=r(d);return e?.read()}function w(){return r(p)}function v(){return r(f)}function P(){return r(m)}function b(){return r(y)}let q=!1,k=!1,N=c("div",{children:"Loading..."}),E=({error:e})=>h("div",{children:["Global Error: ",String(e)]});function x(e,n){q=e,n&&(N=n)}function S(e,n){k=e,n&&(E=({error:e})=>n(e))}function C(){return q}function F(){return k}function I(){return N}function K(){return E}class M extends e.Component{constructor(e){super(e),this.reset=()=>{this.setState(e=>({hasError:!1,error:null,resetKey:e.resetKey+1}))},this.state={hasError:!1,error:null,resetKey:0}}static getDerivedStateFromError(e){return{hasError:!0,error:e}}render(){if(this.state.hasError){const e=this.props.errorBoundary??(F()?K():void 0),n=this.props.fallback??h("div",{children:["Error: ",String(this.state.error)]});return e?c(e,{error:this.state.error}):n}return c(e.Fragment,{children:this.props.children},this.state.resetKey)}}function R({resource:e,errorBoundary:n,children:r}){try{return e?.read(),c(u,{children:r})}catch(e){if(e instanceof Promise)throw e;const r=n??(F()?K():void 0);if(r)return c(r,{error:e});throw e}}function B({chain:e,data:n}){return c(u,{children:function r(o=0){const a=e[o];if(!a)return null;const i=a.node.component,s=n[a.node.path];if(!i)return r(o+1);let h=a.node.options?.fallback,u=a.node.parent;for(;!h&&u;)h=u.options?.fallback,u=u.parent;return!h&&C()&&(h=I()),c(M,{fallback:h,errorBoundary:a.node.options?.errorBoundary,children:c(t,{fallback:h,children:c(p.Provider,{value:a.node.path,children:c(d.Provider,{value:s,children:c(f.Provider,{value:a.params,children:c(R,{resource:s,errorBoundary:a.node.options?.errorBoundary,children:c(i,{children:r(o+1)})})})})})})})}()})}const D=new Map;function A(e=1/0){const n=Date.now();let r=0;for(const[t,o]of D.entries())if(o.expiry<=n&&(D.delete(t),r++,r>=e))break}function U(e,n,r,t){const o=Date.now(),a=D.get(e);if(a&&!t&&(!r||a.expiry>o))return a.resource;const i=L(n());return D.set(e,{resource:i,expiry:o+(r??0)}),i}function L(e,n){let r=n?"success":"pending",t=n??null;const o=e.then(e=>{r="success",t=e},e=>{r="error",t=e});return{read(){if("pending"===r)throw o;if("error"===r)throw t;return t},update(e){t=e,r="success"},get:()=>t}}function O(e){return e.startsWith("/")||(e="/"+e),"/"!==e&&e.endsWith("/")&&(e=e.slice(0,-1)),e}function W(e){return O(e).split("/").filter(Boolean)}function j(e){const[n,r]=e.split("#"),[t,o]=n.split("?"),a={};return o&&o.split("&").forEach(e=>{const[n,r]=e.split("=");n&&(a[decodeURIComponent(n)]=decodeURIComponent(r??""))}),{path:O(t),query:a,hash:r??""}}function $(e,n){const{path:r,query:t,hash:o}=j(n),a=W(r),i=e.map(e=>({node:e,segIndex:0,params:{},chain:[]}));for(;i.length;){const{node:e,segIndex:n,params:r,chain:s}=i.pop(),c=W(e.path);let h=0;const u={...r};for(;h<c.length;h++){const e=c[h],r=a[n+h];if(!r)break;if(e.startsWith(":"))u[e.slice(1)]=decodeURIComponent(r);else if(e!==r)break}if(h!==c.length)continue;const l=n+h,d=[...s,{node:e,params:u,query:t,hash:o}];if(l===a.length)return d;for(const n of e.children)i.push({node:n,segIndex:l,params:u,chain:d})}return null}function J(e,n,r,t){if(e=O(e),n)for(const r in n)e=e.replace(`:${r}`,encodeURIComponent(n[r]));if(r&&Object.keys(r).length){e+=`?${new URLSearchParams(r).toString()}`}return t&&(e+=`#${t}`),e}let G=[];function T(e,n,r){const t={path:e,component:n,children:[],options:{}};"function"==typeof r?z(t)(r):r&&(t.options=r),G.push(t)}function z(e){const n={guard:r=>(e.guardFns||(e.guardFns=[]),e.guardFns.push(r),n),middleware:r=>(e.middlewares||(e.middlewares=[]),e.middlewares.push(r),n),meta:r=>(e.meta=r,n),route(n,r,t){const o={path:n,component:r,children:[],options:{},guardFns:[],middlewares:[],parent:e};"function"==typeof t?z(o)(t):t&&(o.options=t),e.children.push(o);const a={guard:e=>(o.guardFns?.push(e),a),middleware:e=>(o.middlewares?.push(e),a),meta:e=>(o.meta=e,a),route(e,n,r){const t={path:e,component:n,children:[],options:{},guardFns:[],middlewares:[],parent:o};return"function"==typeof r?z(t)(r):r&&(t.options=r),o.children.push(t),a}};return a}};return e=>{e(n)}}function H(e,n){const r={};return e.forEach(e=>{const t=e.node.options?.loader;if(!t)return;const o=JSON.stringify({path:e.node.path,params:e.params,query:e.query,hash:e.hash}),a=e.node.options?.ttl,i=U(o,()=>t({params:e.params,path:e.node.path,query:e.query,hash:e.hash}),a,n?.forceReload);r[e.node.path]=i}),r}async function Q(e){const n=$(G,e);if(!n)return{};const r={};n.forEach(e=>{const n=e.node.options?.loader;n&&(r[e.node.path]=U(JSON.stringify({path:e.node.path,params:e.params,query:e.query,hash:e.hash}),()=>n({params:e.params,path:e.node.path,query:e.query,hash:e.hash}),e.node.options?.ttl))});for(const e in r)r[e].read();return r}function V(e){const n=$(G,e);n&&H(n,{forceReload:!1})}let X=null,Y=[],Z=[];function _(e){Y.push(e)}function ee(e){Z.push(e)}async function ne(e,n,r=!1){const t={...n,forceReload:n?.forceReload??!0},o=J(e,t.params,t.query,t.hash),a=await async function(e){return Promise.all(Y.map(n=>n(e))).then(e=>e.every(e=>!1!==e))}(o);if(!a)return console.warn("Navigation blocked by beforeNavigation hook",o);r?window.history.replaceState({},"",o):window.history.pushState({},"",o),X?.(o,t),async function(e){for(const n of Z)await n(e)}(o)}function re(e,n){return ne(e,n,!1)}function te(e,n){return re(e,n)}function oe(e){return P().path===e}function ae(e){return()=>V(e)}function ie(){const e=(e,n)=>ne(J(e,n?.params,n?.query,n?.hash),n);return e.replace=(e,n)=>ne(J(e,n?.params,n?.query,n?.hash),n,!0),e}function se(){const[e,n]=o({chain:[],data:{},loading:!1,path:"",pendingPath:"",transition:{name:"",stage:"idle"}});async function r(e,r){let t=$(G,e)??$(G,"*")??[];for(const n of t)for(const r of n.node.guardFns||[]){if(!await r({params:n.params,path:n.node.path,query:n.query,hash:n.hash}))return console.warn("Navigation blocked by guard",e)}for(const e of t)for(const n of e.node.middlewares||[])await n({params:e.params,path:e.node.path,query:e.query,hash:e.hash}).catch(console.error);n(n=>({...n,loading:!0,pendingPath:e,transition:{name:r?.transition||"fade",stage:"exiting"}}));const o=H(t,r);n(n=>({...n,chain:t,data:{...n.data,...o},loading:!1,path:e,pendingPath:"",transition:{name:r?.transition||"fade",stage:"entering"}})),window.scrollTo(0,0)}return a(()=>{X=r,r(window.location.pathname),window.addEventListener("popstate",()=>r(window.location.pathname))},[]),c(m.Provider,{value:{loading:e.loading,path:e.path,pendingPath:e.pendingPath},children:c(y.Provider,{value:e.transition,children:c(B,{chain:e.chain,data:e.data})})})}function ce({to:e,transition:n,replace:r,params:t,query:o,hash:i,partialMatch:c,disablePrefetch:h}){const u=ie(),l=P(),d=s(null),p=J(e,t,o,i),f=c?l.path?.startsWith(p):l.path===p;return a(()=>{if(h)return;const e=d.current;if(!e)return;const n=new IntersectionObserver(e=>{e.some(e=>e.isIntersecting)&&(V(p),n.disconnect())},{rootMargin:"200px"});return n.observe(e),()=>n.disconnect()},[p,h]),{ref:d,fullPath:p,isActive:f,go:()=>{r?u.replace(e,{transition:n,params:t,query:o,hash:i}):u(e,{transition:n,params:t,query:o,hash:i})}}}re.replace=(e,n)=>ne(e,n,!0);const he=i(({fullPath:e,onNavigate:n,onPrefetch:r,onClick:t,onMouseEnter:o,onKeyDown:a,disablePrefetch:i,...s},h)=>c("a",{ref:h,href:e,...s,onClick:e=>{t&&t(e),e.metaKey||e.ctrlKey||e.shiftKey||(e.preventDefault(),n())},onMouseEnter:e=>{o&&o(e),i||r?.()},onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),n()),a&&a(e)}}));function ue(e){const{to:n,transition:r,replace:t,activeClassName:o="active",onNavigate:a,partialMatch:i,params:s,query:h,hash:u,className:d,disablePrefetch:p,...f}=e,{ref:m,fullPath:y,isActive:g,go:w}=ce({to:n,transition:r,replace:t,params:s,query:h,hash:u,partialMatch:i,disablePrefetch:p});return c(he,{ref:m,fullPath:y,onNavigate:()=>{w(),a?.(y)},onPrefetch:()=>V(y),className:l(d,g&&o),disablePrefetch:p,...f})}function le(e){const{to:n,transition:r,replace:t,params:o,query:a,hash:i,partialMatch:s,className:h,style:u,disablePrefetch:l,...d}=e,{ref:p,fullPath:f,isActive:m,go:y}=ce({to:n,transition:r,replace:t,params:o,query:a,hash:i,partialMatch:s,disablePrefetch:l}),g="function"==typeof h?h({isActive:m}):h,w="function"==typeof u?u({isActive:m}):u;return c(he,{ref:p,fullPath:f,onNavigate:y,onPrefetch:()=>V(f),className:g,style:w,disablePrefetch:l,...d})}he.displayName="LinkCore";export{ue as Link,d as LoaderDataContext,le as NavLink,f as ParamsContext,p as RouteContext,se as RouterProvider,m as RouterStateContext,y as TransitionContext,ee as addAfterNavigationHook,_ as addBeforeNavigationHook,J as buildPath,A as cleanupCache,L as createResource,K as getGlobalError,I as getGlobalFallback,U as getResource,F as getUseGlobalError,C as getUseGlobalFallback,Q as loadRouteData,$ as matchRouteChain,re as navigate,j as parseQueryHash,V as prefetch,te as redirect,T as route,S as setGlobalError,x as setGlobalFallback,ce as useLinkCore,ie as useNavigate,P as useNavigation,v as useParams,ae as usePrefetch,w as useRouteContext,g as useRouteData,oe as useRouteMatch,b as useTransition};
|
package/build/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("react"),t=require("react/jsx-runtime"),r=require("joinclass");const n=e.createContext(null),o=e.createContext(null),a=e.createContext(null),s=e.createContext({loading:!1,path:"",pendingPath:""}),i=e.createContext({name:"",stage:"idle"});function c(){return e.useContext(s)}let u=!1,h=!1,p=t.jsx("div",{children:"Loading..."}),l=({error:e})=>t.jsxs("div",{children:["Global Error: ",String(e)]});function d(){return u}function f(){return h}function x(){return p}function m(){return l}class g extends e.Component{constructor(e){super(e),this.reset=()=>{this.setState(e=>({hasError:!1,error:null,resetKey:e.resetKey+1}))},this.state={hasError:!1,error:null,resetKey:0}}static getDerivedStateFromError(e){return{hasError:!0,error:e}}render(){if(this.state.hasError){const e=this.props.errorBoundary??(f()?m():void 0),r=this.props.fallback??t.jsxs("div",{children:["Error: ",String(this.state.error)]});return e?t.jsx(e,{error:this.state.error}):r}return t.jsx(e.Fragment,{children:this.props.children},this.state.resetKey)}}function y({resource:e,errorBoundary:r,children:n}){try{return e?.read(),t.jsx(t.Fragment,{children:n})}catch(e){if(e instanceof Promise)throw e;const n=r??(f()?m():void 0);if(n)return t.jsx(n,{error:e});throw e}}function v({chain:r,data:s}){return t.jsx(t.Fragment,{children:function i(c=0){const u=r[c];if(!u)return null;const h=u.node.component,p=s[u.node.path];if(!h)return i(c+1);let l=u.node.options?.fallback,f=u.node.parent;for(;!l&&f;)l=f.options?.fallback,f=f.parent;return!l&&d()&&(l=x()),t.jsx(g,{fallback:l,errorBoundary:u.node.options?.errorBoundary,children:t.jsx(e.Suspense,{fallback:l,children:t.jsx(o.Provider,{value:u.node.path,children:t.jsx(n.Provider,{value:p,children:t.jsx(a.Provider,{value:u.params,children:t.jsx(y,{resource:p,errorBoundary:u.node.options?.errorBoundary,children:t.jsx(h,{children:i(c+1)})})})})})})})}()})}const w=new Map;function P(e,t,r,n){const o=Date.now(),a=w.get(e);if(a&&!n&&(!r||a.expiry>o))return a.resource;const s=b(t());return w.set(e,{resource:s,expiry:o+(r??0)}),s}function b(e,t){let r=t?"success":"pending",n=t??null;const o=e.then(e=>{r="success",n=e},e=>{r="error",n=e});return{read(){if("pending"===r)throw o;if("error"===r)throw n;return n},update(e){n=e,r="success"},get:()=>n}}function q(e){return e.startsWith("/")||(e="/"+e),"/"!==e&&e.endsWith("/")&&(e=e.slice(0,-1)),e}function C(e){return q(e).split("/").filter(Boolean)}function j(e){const[t,r]=e.split("#"),[n,o]=t.split("?"),a={};return o&&o.split("&").forEach(e=>{const[t,r]=e.split("=");t&&(a[decodeURIComponent(t)]=decodeURIComponent(r??""))}),{path:q(n),query:a,hash:r??""}}function k(e,t){const{path:r,query:n,hash:o}=j(t),a=C(r),s=e.map(e=>({node:e,segIndex:0,params:{},chain:[]}));for(;s.length;){const{node:e,segIndex:t,params:r,chain:i}=s.pop(),c=C(e.path);let u=0;const h={...r};for(;u<c.length;u++){const e=c[u],r=a[t+u];if(!r)break;if(e.startsWith(":"))h[e.slice(1)]=decodeURIComponent(r);else if(e!==r)break}if(u!==c.length)continue;const p=t+u,l=[...i,{node:e,params:h,query:n,hash:o}];if(p===a.length)return l;for(const t of e.children)s.push({node:t,segIndex:p,params:h,chain:l})}return null}function R(e,t,r,n){if(e=q(e),t)for(const r in t)e=e.replace(`:${r}`,encodeURIComponent(t[r]));if(r&&Object.keys(r).length){e+=`?${new URLSearchParams(r).toString()}`}return n&&(e+=`#${n}`),e}let N=[];function E(e){const t={guard:r=>(e.guardFns||(e.guardFns=[]),e.guardFns.push(r),t),middleware:r=>(e.middlewares||(e.middlewares=[]),e.middlewares.push(r),t),meta:r=>(e.meta=r,t),route(t,r,n){const o={path:t,component:r,children:[],options:{},guardFns:[],middlewares:[],parent:e};"function"==typeof n?E(o)(n):n&&(o.options=n),e.children.push(o);const a={guard:e=>(o.guardFns?.push(e),a),middleware:e=>(o.middlewares?.push(e),a),meta:e=>(o.meta=e,a),route(e,t,r){const n={path:e,component:t,children:[],options:{},guardFns:[],middlewares:[],parent:o};return"function"==typeof r?E(n)(r):r&&(n.options=r),o.children.push(n),a}};return a}};return e=>{e(t)}}function F(e,t){const r={};return e.forEach(e=>{const n=e.node.options?.loader;if(!n)return;const o=JSON.stringify({path:e.node.path,params:e.params,query:e.query,hash:e.hash}),a=e.node.options?.ttl,s=P(o,()=>n({params:e.params,path:e.node.path,query:e.query,hash:e.hash}),a,t?.forceReload);r[e.node.path]=s}),r}function S(e){const t=k(N,e);t&&F(t,{forceReload:!1})}let D=null,M=[],I=[];async function K(e,t,r=!1){const n={...t,forceReload:t?.forceReload??!0},o=R(e,n.params,n.query,n.hash),a=await async function(e){return Promise.all(M.map(t=>t(e))).then(e=>e.every(e=>!1!==e))}(o);if(!a)return console.warn("Navigation blocked by beforeNavigation hook",o);r?window.history.replaceState({},"",o):window.history.pushState({},"",o),D?.(o,n),async function(e){for(const t of I)await t(e)}(o)}function B(e,t){return K(e,t,!1)}function L(){const e=(e,t)=>K(R(e,t?.params,t?.query,t?.hash),t);return e.replace=(e,t)=>K(R(e,t?.params,t?.query,t?.hash),t,!0),e}function G({to:t,transition:r,replace:n,params:o,query:a,hash:s,partialMatch:i,disablePrefetch:u}){const h=L(),p=c(),l=e.useRef(null),d=R(t,o,a,s),f=i?p.path?.startsWith(d):p.path===d;return e.useEffect(()=>{if(u)return;const e=l.current;if(!e)return;const t=new IntersectionObserver(e=>{e.some(e=>e.isIntersecting)&&(S(d),t.disconnect())},{rootMargin:"200px"});return t.observe(e),()=>t.disconnect()},[d,u]),{ref:l,fullPath:d,isActive:f,go:()=>{n?h.replace(t,{transition:r,params:o,query:a,hash:s}):h(t,{transition:r,params:o,query:a,hash:s})}}}B.replace=(e,t)=>K(e,t,!0);const U=e.forwardRef(({fullPath:e,onNavigate:r,onPrefetch:n,onClick:o,onMouseEnter:a,onKeyDown:s,disablePrefetch:i,...c},u)=>t.jsx("a",{ref:u,href:e,...c,onClick:e=>{o&&o(e),e.metaKey||e.ctrlKey||e.shiftKey||(e.preventDefault(),r())},onMouseEnter:e=>{a&&a(e),i||n?.()},onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),r()),s&&s(e)}}));U.displayName="LinkCore",exports.Link=function(e){const{to:n,transition:o,replace:a,activeClassName:s="active",onNavigate:i,partialMatch:c,params:u,query:h,hash:p,className:l,disablePrefetch:d,...f}=e,{ref:x,fullPath:m,isActive:g,go:y}=G({to:n,transition:o,replace:a,params:u,query:h,hash:p,partialMatch:c,disablePrefetch:d});return t.jsx(U,{ref:x,fullPath:m,onNavigate:()=>{y(),i?.(m)},onPrefetch:()=>S(m),className:r(l,g&&s),disablePrefetch:d,...f})},exports.LoaderDataContext=n,exports.NavLink=function(e){const{to:r,transition:n,replace:o,params:a,query:s,hash:i,partialMatch:c,className:u,style:h,disablePrefetch:p,...l}=e,{ref:d,fullPath:f,isActive:x,go:m}=G({to:r,transition:n,replace:o,params:a,query:s,hash:i,partialMatch:c,disablePrefetch:p}),g="function"==typeof u?u({isActive:x}):u,y="function"==typeof h?h({isActive:x}):h;return t.jsx(U,{ref:d,fullPath:f,onNavigate:m,onPrefetch:()=>S(f),className:g,style:y,disablePrefetch:p,...l})},exports.ParamsContext=a,exports.RouteContext=o,exports.RouterProvider=function(){const[r,n]=e.useState({chain:[],data:{},loading:!1,path:"",pendingPath:"",transition:{name:"",stage:"idle"}});async function o(e,t){let r=k(N,e)??k(N,"*")??[];for(const t of r)for(const r of t.node.guardFns||[]){if(!await r({params:t.params,path:t.node.path,query:t.query,hash:t.hash}))return console.warn("Navigation blocked by guard",e)}for(const e of r)for(const t of e.node.middlewares||[])await t({params:e.params,path:e.node.path,query:e.query,hash:e.hash}).catch(console.error);n(r=>({...r,loading:!0,pendingPath:e,transition:{name:t?.transition||"fade",stage:"exiting"}}));const o=F(r,t);n(n=>({...n,chain:r,data:{...n.data,...o},loading:!1,path:e,pendingPath:"",transition:{name:t?.transition||"fade",stage:"entering"}})),window.scrollTo(0,0)}return e.useEffect(()=>{D=o,o(window.location.pathname),window.addEventListener("popstate",()=>o(window.location.pathname))},[]),t.jsx(s.Provider,{value:{loading:r.loading,path:r.path,pendingPath:r.pendingPath},children:t.jsx(i.Provider,{value:r.transition,children:t.jsx(v,{chain:r.chain,data:r.data})})})},exports.RouterStateContext=s,exports.TransitionContext=i,exports.addAfterNavigationHook=function(e){I.push(e)},exports.addBeforeNavigationHook=function(e){M.push(e)},exports.buildPath=R,exports.cleanupCache=function(e=1/0){const t=Date.now();let r=0;for(const[n,o]of w.entries())if(o.expiry<=t&&(w.delete(n),r++,r>=e))break},exports.createResource=b,exports.getGlobalError=m,exports.getGlobalFallback=x,exports.getResource=P,exports.getUseGlobalError=f,exports.getUseGlobalFallback=d,exports.loadRouteData=async function(e){const t=k(N,e);if(!t)return{};const r={};t.forEach(e=>{const t=e.node.options?.loader;t&&(r[e.node.path]=P(JSON.stringify({path:e.node.path,params:e.params,query:e.query,hash:e.hash}),()=>t({params:e.params,path:e.node.path,query:e.query,hash:e.hash}),e.node.options?.ttl))});for(const e in r)r[e].read();return r},exports.matchRouteChain=k,exports.navigate=B,exports.parseQueryHash=j,exports.prefetch=S,exports.redirect=function(e,t){return B(e,t)},exports.route=function(e,t,r){const n={path:e,component:t,children:[],options:{}};"function"==typeof r?E(n)(r):r&&(n.options=r),N.push(n)},exports.setGlobalError=function(e,t){h=e,t&&(l=({error:e})=>t(e))},exports.setGlobalFallback=function(e,t){u=e,t&&(p=t)},exports.useLinkCore=G,exports.useNavigate=L,exports.useNavigation=c,exports.useParams=function(){return e.useContext(a)},exports.usePrefetch=function(e){return()=>S(e)},exports.useRouteContext=function(){return e.useContext(o)},exports.useRouteData=function(){const t=e.useContext(n);return t?.read()},exports.useRouteMatch=function(e){return c().path===e},exports.useTransition=function(){return e.useContext(i)};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type TransitionStage = "idle" | "entering" | "exiting";
|
|
2
|
+
export declare const LoaderDataContext: import("react").Context<any>;
|
|
3
|
+
export declare const RouteContext: import("react").Context<string | null>;
|
|
4
|
+
export declare const ParamsContext: import("react").Context<any>;
|
|
5
|
+
export declare const RouterStateContext: import("react").Context<{
|
|
6
|
+
loading: boolean;
|
|
7
|
+
path: string;
|
|
8
|
+
pendingPath: string;
|
|
9
|
+
}>;
|
|
10
|
+
export declare const TransitionContext: import("react").Context<{
|
|
11
|
+
name: string;
|
|
12
|
+
stage: TransitionStage;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function useRouteData<T = any>(): T;
|
|
15
|
+
export declare function useRouteContext(): string | null;
|
|
16
|
+
export declare function useParams(): any;
|
|
17
|
+
export declare function useNavigation(): {
|
|
18
|
+
loading: boolean;
|
|
19
|
+
path: string;
|
|
20
|
+
pendingPath: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function useTransition(): {
|
|
23
|
+
name: string;
|
|
24
|
+
stage: TransitionStage;
|
|
25
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { BuilderRouteAPI, RouteOptions } from "./types";
|
|
4
|
+
export declare function route<P = any, R = any>(path: string, component?: React.ComponentType<{
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
}>, options?: ((api: BuilderRouteAPI) => void) | RouteOptions<P, R>): void;
|
|
7
|
+
export declare function loadRouteData(path: string): Promise<Record<string, any>>;
|
|
8
|
+
export declare function prefetch(path: string): void;
|
|
9
|
+
export type NavigateOpts = {
|
|
10
|
+
transition?: string;
|
|
11
|
+
forceReload?: boolean;
|
|
12
|
+
params?: Record<string, string | number>;
|
|
13
|
+
query?: Record<string, string | number>;
|
|
14
|
+
hash?: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function addBeforeNavigationHook(fn: (path: string) => boolean | Promise<boolean>): void;
|
|
17
|
+
export declare function addAfterNavigationHook(fn: (path: string) => void | Promise<void>): void;
|
|
18
|
+
export declare function navigate(path: string, opts?: NavigateOpts): Promise<void>;
|
|
19
|
+
export declare namespace navigate {
|
|
20
|
+
var replace: (path: string, opts?: NavigateOpts) => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export declare function redirect(path: string, opts?: NavigateOpts): Promise<void>;
|
|
23
|
+
export declare function useRouteMatch(pathPattern: string): boolean;
|
|
24
|
+
export declare function usePrefetch(path: string): () => void;
|
|
25
|
+
export declare function useNavigate(): {
|
|
26
|
+
(path: string, opts?: NavigateOpts): Promise<void>;
|
|
27
|
+
replace(path: string, opts?: NavigateOpts): Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
export declare function RouterProvider(): JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function cleanupCache(limit?: number): void;
|
|
2
|
+
export declare function getResource<T>(key: string, loader: () => Promise<T>, ttl?: number, forceReload?: boolean): {
|
|
3
|
+
read(): unknown;
|
|
4
|
+
update(newData: unknown): void;
|
|
5
|
+
get(): unknown;
|
|
6
|
+
};
|
|
7
|
+
export declare function createResource<T>(promise: Promise<T>, existing?: T): {
|
|
8
|
+
read(): T | null;
|
|
9
|
+
update(newData: T): void;
|
|
10
|
+
get(): T | null;
|
|
11
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function setGlobalFallback(enable: boolean, fallback?: React.ReactNode): void;
|
|
2
|
+
export declare function setGlobalError(enable: boolean, render?: (error: any) => React.ReactElement | null): void;
|
|
3
|
+
export declare function getUseGlobalFallback(): boolean;
|
|
4
|
+
export declare function getUseGlobalError(): boolean;
|
|
5
|
+
export declare function getGlobalFallback(): import("react").ReactNode;
|
|
6
|
+
export declare function getGlobalError(): import("react").ComponentType<{
|
|
7
|
+
error: any;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
type LoaderContext<P = any> = {
|
|
3
|
+
params: P;
|
|
4
|
+
path: string;
|
|
5
|
+
query?: Record<string, string>;
|
|
6
|
+
hash?: string;
|
|
7
|
+
};
|
|
8
|
+
export type RouteOptions<P = any, R = any> = {
|
|
9
|
+
loader?: (context: LoaderContext<P>) => Promise<R>;
|
|
10
|
+
meta?: Record<string, any>;
|
|
11
|
+
errorBoundary?: React.ComponentType<{
|
|
12
|
+
error: any;
|
|
13
|
+
}>;
|
|
14
|
+
fallback?: React.ReactNode;
|
|
15
|
+
ttl?: number;
|
|
16
|
+
};
|
|
17
|
+
export type GuardFn<P = any> = (context: LoaderContext<P>) => boolean | Promise<boolean>;
|
|
18
|
+
export type MiddlewareFn<P = any> = (context: LoaderContext<P>) => any | Promise<any>;
|
|
19
|
+
export type RouteNode<P = any, R = any> = {
|
|
20
|
+
path: string;
|
|
21
|
+
component?: React.ComponentType<{
|
|
22
|
+
children?: ReactNode;
|
|
23
|
+
}>;
|
|
24
|
+
children: RouteNode[];
|
|
25
|
+
options: RouteOptions<P, R>;
|
|
26
|
+
guardFns?: GuardFn<P>[];
|
|
27
|
+
middlewares?: MiddlewareFn<P>[];
|
|
28
|
+
meta?: Record<string, any>;
|
|
29
|
+
parent?: RouteNode;
|
|
30
|
+
};
|
|
31
|
+
export type BuilderRouteAPI = {
|
|
32
|
+
guard: (fn: GuardFn) => BuilderRouteAPI;
|
|
33
|
+
middleware: (fn: MiddlewareFn) => BuilderRouteAPI;
|
|
34
|
+
route: <P = any, R = any>(path: string, component?: React.ComponentType<{
|
|
35
|
+
children?: ReactNode;
|
|
36
|
+
}>, options?: ((api: BuilderRouteAPI) => void) | RouteOptions<P, R>) => BuilderRouteAPI;
|
|
37
|
+
meta: (obj: Record<string, any>) => BuilderRouteAPI;
|
|
38
|
+
};
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RouteNode } from "./types";
|
|
2
|
+
export declare function parseQueryHash(pathname: string): {
|
|
3
|
+
path: string;
|
|
4
|
+
query: Record<string, string>;
|
|
5
|
+
hash: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function matchRouteChain(routes: RouteNode[], pathname: string): any[] | null;
|
|
8
|
+
export declare function buildPath(path: string, params?: Record<string, any>, query?: Record<string, any>, hash?: string): string;
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "routexiz",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Intent-based routing for React. Navigate by intent instead of URLs.",
|
|
5
|
+
"author": "Delpi.Kye",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"type": "module",
|
|
9
|
+
|
|
10
|
+
"main": "./build/index.js",
|
|
11
|
+
"module": "./build/index.esm.js",
|
|
12
|
+
"types": "./build/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./build/index.d.ts",
|
|
16
|
+
"import": "./build/index.esm.js",
|
|
17
|
+
"require": "./build/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
"files": [
|
|
22
|
+
"build"
|
|
23
|
+
],
|
|
24
|
+
|
|
25
|
+
"scripts": {
|
|
26
|
+
"clean": "rimraf build",
|
|
27
|
+
"build": "rimraf build && rollup -c",
|
|
28
|
+
"watch": "rollup -c -w",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"prepublishOnly": "npm run typecheck && npm run build"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=16"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/delpikye-v/routexiz.git"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/delpikye-v/routexiz#readme",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/delpikye-v/routexiz/issues"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"react",
|
|
45
|
+
"router",
|
|
46
|
+
"intent-router",
|
|
47
|
+
"navigation",
|
|
48
|
+
"intent-navigation",
|
|
49
|
+
"react-navigation",
|
|
50
|
+
"frontend",
|
|
51
|
+
"spa",
|
|
52
|
+
"routing"
|
|
53
|
+
],
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"react": ">=18",
|
|
56
|
+
"react-dom": ">=18"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"joinclass": "^1.1.2"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@rollup/plugin-commonjs": "^25.0.7",
|
|
63
|
+
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
64
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
65
|
+
"@types/react": "^18.0.0",
|
|
66
|
+
"@types/react-dom": "^18.0.0",
|
|
67
|
+
"react": "^18.0.0",
|
|
68
|
+
"react-dom": "^18.0.0",
|
|
69
|
+
"rimraf": "^5.0.5",
|
|
70
|
+
"rollup": "^3.29.4",
|
|
71
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
72
|
+
"rollup-plugin-typescript2": "^0.36.0",
|
|
73
|
+
"tslib": "^2.6.2",
|
|
74
|
+
"typescript": "^5.0.0"
|
|
75
|
+
},
|
|
76
|
+
"publishConfig": {
|
|
77
|
+
"access": "public"
|
|
78
|
+
}
|
|
79
|
+
}
|