@funstack/router 0.0.6 → 0.0.7-alpha.0
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/dist/bin/skill-installer.d.mts +1 -0
- package/dist/bin/skill-installer.mjs +13 -0
- package/dist/bin/skill-installer.mjs.map +1 -0
- package/dist/docs/ApiComponentsPage.tsx +85 -0
- package/dist/docs/ApiHooksPage.tsx +323 -0
- package/dist/docs/ApiTypesPage.tsx +310 -0
- package/dist/docs/ApiUtilitiesPage.tsx +298 -0
- package/dist/docs/GettingStartedPage.tsx +186 -0
- package/dist/docs/LearnNavigationApiPage.tsx +255 -0
- package/dist/docs/LearnNestedRoutesPage.tsx +601 -0
- package/dist/docs/LearnRscPage.tsx +293 -0
- package/dist/docs/LearnSsrPage.tsx +180 -0
- package/dist/docs/LearnTransitionsPage.tsx +146 -0
- package/dist/docs/LearnTypeSafetyPage.tsx +522 -0
- package/dist/docs/index.md +21 -0
- package/dist/index.d.mts +1 -1
- package/dist/{route-Bc8BUlhv.d.mts → route-ClVnhrQD.d.mts} +1 -1
- package/dist/{route-Bc8BUlhv.d.mts.map → route-ClVnhrQD.d.mts.map} +1 -1
- package/dist/server.d.mts +1 -1
- package/package.json +11 -2
- package/skills/funstack-router-knowledge/SKILL.md +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
import { install } from "@funstack/skill-installer";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/bin/skill-installer.ts
|
|
7
|
+
const skillDir = resolve(dirname(fileURLToPath(import.meta.url)), "../../skills/funstack-router-knowledge");
|
|
8
|
+
console.log("Installing skill from:", skillDir);
|
|
9
|
+
await install(skillDir);
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { };
|
|
13
|
+
//# sourceMappingURL=skill-installer.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-installer.mjs","names":[],"sources":["../../src/bin/skill-installer.ts"],"sourcesContent":["#! /usr/bin/env node\n\nimport { install } from \"@funstack/skill-installer\";\nimport { fileURLToPath } from \"node:url\";\nimport { resolve, dirname } from \"node:path\";\n\n// Resolve the skill directory relative to this script's location.\n// This script is at dist/bin/skill-installer.mjs, so go up two levels\n// to reach the package root, then into skills/.\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst skillDir = resolve(__dirname, \"../../skills/funstack-router-knowledge\");\n\nconsole.log(\"Installing skill from:\", skillDir);\n\nawait install(skillDir);\n"],"mappings":";;;;;;AAUA,MAAM,WAAW,QADC,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EACrB,yCAAyC;AAE7E,QAAQ,IAAI,0BAA0B,SAAS;AAE/C,MAAM,QAAQ,SAAS"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { CodeBlock } from "../components/CodeBlock.js";
|
|
2
|
+
|
|
3
|
+
export function ApiComponentsPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="page docs-page api-page">
|
|
6
|
+
<h1>Components</h1>
|
|
7
|
+
<p className="page-intro">
|
|
8
|
+
Core components for building routing in your React application.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<article className="api-item">
|
|
12
|
+
<h3>
|
|
13
|
+
<code>{"<Router>"}</code>
|
|
14
|
+
</h3>
|
|
15
|
+
<p>The main router component that provides routing context.</p>
|
|
16
|
+
<CodeBlock language="tsx">{`import { Router } from "@funstack/router";
|
|
17
|
+
|
|
18
|
+
<Router
|
|
19
|
+
routes={routes}
|
|
20
|
+
onNavigate={(event, info) => {
|
|
21
|
+
console.log("Navigating to:", event.destination.url);
|
|
22
|
+
console.log("Matched routes:", info.matches);
|
|
23
|
+
console.log("Will intercept:", info.intercepting);
|
|
24
|
+
}}
|
|
25
|
+
/>`}</CodeBlock>
|
|
26
|
+
<h4>Props</h4>
|
|
27
|
+
<table className="props-table">
|
|
28
|
+
<thead>
|
|
29
|
+
<tr>
|
|
30
|
+
<th>Prop</th>
|
|
31
|
+
<th>Type</th>
|
|
32
|
+
<th>Description</th>
|
|
33
|
+
</tr>
|
|
34
|
+
</thead>
|
|
35
|
+
<tbody>
|
|
36
|
+
<tr>
|
|
37
|
+
<td>
|
|
38
|
+
<code>routes</code>
|
|
39
|
+
</td>
|
|
40
|
+
<td>
|
|
41
|
+
<code>RouteDefinition[]</code>
|
|
42
|
+
</td>
|
|
43
|
+
<td>Array of route definitions</td>
|
|
44
|
+
</tr>
|
|
45
|
+
<tr>
|
|
46
|
+
<td>
|
|
47
|
+
<code>onNavigate</code>
|
|
48
|
+
</td>
|
|
49
|
+
<td>
|
|
50
|
+
<code>OnNavigateCallback</code>
|
|
51
|
+
</td>
|
|
52
|
+
<td>
|
|
53
|
+
Callback fired before navigation is intercepted. Receives the
|
|
54
|
+
NavigateEvent and an info object with matched routes and whether
|
|
55
|
+
the router will intercept the navigation.
|
|
56
|
+
</td>
|
|
57
|
+
</tr>
|
|
58
|
+
</tbody>
|
|
59
|
+
</table>
|
|
60
|
+
</article>
|
|
61
|
+
|
|
62
|
+
<article className="api-item">
|
|
63
|
+
<h3>
|
|
64
|
+
<code>{"<Outlet>"}</code>
|
|
65
|
+
</h3>
|
|
66
|
+
<p>
|
|
67
|
+
Renders the child route's component. Used in parent routes for nested
|
|
68
|
+
layouts.
|
|
69
|
+
</p>
|
|
70
|
+
<CodeBlock language="tsx">{`import { Outlet } from "@funstack/router";
|
|
71
|
+
|
|
72
|
+
function Layout() {
|
|
73
|
+
return (
|
|
74
|
+
<div>
|
|
75
|
+
<header>My App</header>
|
|
76
|
+
<main>
|
|
77
|
+
<Outlet />
|
|
78
|
+
</main>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}`}</CodeBlock>
|
|
82
|
+
</article>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { CodeBlock } from "../components/CodeBlock.js";
|
|
2
|
+
|
|
3
|
+
export function ApiHooksPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="page docs-page api-page">
|
|
6
|
+
<h1>Hooks</h1>
|
|
7
|
+
<p className="page-intro">
|
|
8
|
+
React hooks for accessing router state and navigation.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<article className="api-item">
|
|
12
|
+
<h3>
|
|
13
|
+
<code>useNavigate()</code>
|
|
14
|
+
</h3>
|
|
15
|
+
<p>Returns a function to programmatically navigate.</p>
|
|
16
|
+
<CodeBlock language="tsx">{`import { useNavigate } from "@funstack/router";
|
|
17
|
+
|
|
18
|
+
function MyComponent() {
|
|
19
|
+
const navigate = useNavigate();
|
|
20
|
+
|
|
21
|
+
// Navigate to a path
|
|
22
|
+
navigate("/about");
|
|
23
|
+
|
|
24
|
+
// Navigate with options
|
|
25
|
+
navigate("/users/123", {
|
|
26
|
+
replace: true, // Replace current history entry
|
|
27
|
+
state: { from: "home" }, // Persistent state (survives back/forward)
|
|
28
|
+
info: { referrer: "home" }, // Ephemeral info (only for this navigation)
|
|
29
|
+
});
|
|
30
|
+
}`}</CodeBlock>
|
|
31
|
+
</article>
|
|
32
|
+
|
|
33
|
+
<article className="api-item">
|
|
34
|
+
<h3>
|
|
35
|
+
<code>useLocation()</code>
|
|
36
|
+
</h3>
|
|
37
|
+
<p>Returns the current location object.</p>
|
|
38
|
+
<CodeBlock language="tsx">{`import { useLocation } from "@funstack/router";
|
|
39
|
+
|
|
40
|
+
function MyComponent() {
|
|
41
|
+
const location = useLocation();
|
|
42
|
+
|
|
43
|
+
console.log(location.pathname); // "/users/123"
|
|
44
|
+
console.log(location.search); // "?tab=profile"
|
|
45
|
+
console.log(location.hash); // "#section"
|
|
46
|
+
}`}</CodeBlock>
|
|
47
|
+
</article>
|
|
48
|
+
|
|
49
|
+
<article className="api-item">
|
|
50
|
+
<h3>
|
|
51
|
+
<code>useSearchParams()</code>
|
|
52
|
+
</h3>
|
|
53
|
+
<p>
|
|
54
|
+
Returns a tuple of the current search params and a setter function.
|
|
55
|
+
</p>
|
|
56
|
+
<CodeBlock language="tsx">{`import { useSearchParams } from "@funstack/router";
|
|
57
|
+
|
|
58
|
+
function SearchPage() {
|
|
59
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
60
|
+
|
|
61
|
+
const query = searchParams.get("q");
|
|
62
|
+
|
|
63
|
+
const handleSearch = (newQuery: string) => {
|
|
64
|
+
setSearchParams({ q: newQuery });
|
|
65
|
+
};
|
|
66
|
+
}`}</CodeBlock>
|
|
67
|
+
</article>
|
|
68
|
+
|
|
69
|
+
<article className="api-item">
|
|
70
|
+
<h3>
|
|
71
|
+
<code>useBlocker(options)</code>
|
|
72
|
+
</h3>
|
|
73
|
+
<p>
|
|
74
|
+
Prevents navigation away from the current route. Useful for scenarios
|
|
75
|
+
like unsaved form data, ongoing file uploads, or any state that would
|
|
76
|
+
be lost on navigation.
|
|
77
|
+
</p>
|
|
78
|
+
<CodeBlock language="tsx">{`import { useBlocker } from "@funstack/router";
|
|
79
|
+
import { useState, useCallback } from "react";
|
|
80
|
+
|
|
81
|
+
function EditForm() {
|
|
82
|
+
const [isDirty, setIsDirty] = useState(false);
|
|
83
|
+
|
|
84
|
+
useBlocker({
|
|
85
|
+
shouldBlock: useCallback(() => {
|
|
86
|
+
if (isDirty) {
|
|
87
|
+
return !confirm("You have unsaved changes. Leave anyway?");
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}, [isDirty]),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const handleSave = () => {
|
|
94
|
+
// Save logic...
|
|
95
|
+
setIsDirty(false);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<form>
|
|
100
|
+
<input onChange={() => setIsDirty(true)} />
|
|
101
|
+
<button type="button" onClick={handleSave}>
|
|
102
|
+
Save
|
|
103
|
+
</button>
|
|
104
|
+
</form>
|
|
105
|
+
);
|
|
106
|
+
}`}</CodeBlock>
|
|
107
|
+
<h4>Options</h4>
|
|
108
|
+
<ul>
|
|
109
|
+
<li>
|
|
110
|
+
<code>shouldBlock</code>: A function that returns <code>true</code>{" "}
|
|
111
|
+
to block navigation, or <code>false</code> to allow it. You can call{" "}
|
|
112
|
+
<code>confirm()</code> inside this function to show a confirmation
|
|
113
|
+
dialog. Wrap with <code>useCallback</code> when the function depends
|
|
114
|
+
on state.
|
|
115
|
+
</li>
|
|
116
|
+
</ul>
|
|
117
|
+
<h4>Notes</h4>
|
|
118
|
+
<ul>
|
|
119
|
+
<li>
|
|
120
|
+
Multiple blockers can coexist in the component tree. If any blocker
|
|
121
|
+
returns <code>true</code>, navigation is blocked.
|
|
122
|
+
</li>
|
|
123
|
+
<li>
|
|
124
|
+
This hook only handles SPA navigations (links, programmatic
|
|
125
|
+
navigation). For hard navigations (tab close, refresh), handle{" "}
|
|
126
|
+
<code>beforeunload</code> separately.
|
|
127
|
+
</li>
|
|
128
|
+
</ul>
|
|
129
|
+
</article>
|
|
130
|
+
|
|
131
|
+
<article className="api-item">
|
|
132
|
+
<h3>
|
|
133
|
+
<code>useIsPending()</code>
|
|
134
|
+
</h3>
|
|
135
|
+
<p>
|
|
136
|
+
Returns whether a navigation transition is currently pending. When
|
|
137
|
+
navigating to a route that suspends (e.g., using{" "}
|
|
138
|
+
<code>React.lazy</code>), <code>isPending</code> becomes{" "}
|
|
139
|
+
<code>true</code> while React keeps the previous UI visible, and
|
|
140
|
+
returns to <code>false</code> once the new route is ready.
|
|
141
|
+
</p>
|
|
142
|
+
<CodeBlock language="tsx">{`import { useIsPending } from "@funstack/router";
|
|
143
|
+
|
|
144
|
+
function MyComponent() {
|
|
145
|
+
const isPending = useIsPending();
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div style={{ opacity: isPending ? 0.7 : 1 }}>
|
|
149
|
+
{isPending && <span>Navigating...</span>}
|
|
150
|
+
{/* page content */}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}`}</CodeBlock>
|
|
154
|
+
<h4>Return Value</h4>
|
|
155
|
+
<ul>
|
|
156
|
+
<li>
|
|
157
|
+
<code>boolean</code> — <code>true</code> when a navigation
|
|
158
|
+
transition is in progress (the destination route is suspending),{" "}
|
|
159
|
+
<code>false</code> otherwise.
|
|
160
|
+
</li>
|
|
161
|
+
</ul>
|
|
162
|
+
<h4>Notes</h4>
|
|
163
|
+
<ul>
|
|
164
|
+
<li>
|
|
165
|
+
This hook is powered by React's <code>useTransition</code>. The
|
|
166
|
+
router wraps navigation state updates in{" "}
|
|
167
|
+
<code>startTransition</code>, so React defers rendering suspended
|
|
168
|
+
routes and keeps the current UI visible.
|
|
169
|
+
</li>
|
|
170
|
+
<li>
|
|
171
|
+
The same <code>isPending</code> value is also available as a prop on
|
|
172
|
+
route components.
|
|
173
|
+
</li>
|
|
174
|
+
</ul>
|
|
175
|
+
</article>
|
|
176
|
+
|
|
177
|
+
<h2>Type-Safe Hooks</h2>
|
|
178
|
+
<p>
|
|
179
|
+
These hooks provide type-safe access to route data when using routes
|
|
180
|
+
defined with an <code>id</code>. They extract type information from{" "}
|
|
181
|
+
<code>TypefulOpaqueRouteDefinition</code> and validate at runtime that
|
|
182
|
+
the specified route exists in the current route hierarchy.
|
|
183
|
+
</p>
|
|
184
|
+
<p>
|
|
185
|
+
In nested routes, these hooks can access data from any ancestor route in
|
|
186
|
+
the hierarchy. For example, a child route component can use{" "}
|
|
187
|
+
<code>useRouteParams(parentRoute)</code> to access the parent route's
|
|
188
|
+
parameters.
|
|
189
|
+
</p>
|
|
190
|
+
|
|
191
|
+
<article className="api-item">
|
|
192
|
+
<h3>
|
|
193
|
+
<code>useRouteParams(route)</code>
|
|
194
|
+
</h3>
|
|
195
|
+
<p>
|
|
196
|
+
Returns typed route parameters for the given route definition. The
|
|
197
|
+
parameter types are automatically inferred from the route's path
|
|
198
|
+
pattern.
|
|
199
|
+
</p>
|
|
200
|
+
<CodeBlock language="tsx">{`import { route, useRouteParams } from "@funstack/router";
|
|
201
|
+
|
|
202
|
+
// Define route with id for type-safe access
|
|
203
|
+
const userRoute = route({
|
|
204
|
+
id: "user",
|
|
205
|
+
path: "/users/:userId",
|
|
206
|
+
component: UserPage,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
function UserPage() {
|
|
210
|
+
// params is typed as { userId: string }
|
|
211
|
+
const params = useRouteParams(userRoute);
|
|
212
|
+
|
|
213
|
+
return <div>User ID: {params.userId}</div>;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// In nested routes, access parent route params:
|
|
217
|
+
const orgRoute = route({
|
|
218
|
+
id: "org",
|
|
219
|
+
path: "/org/:orgId",
|
|
220
|
+
component: OrgLayout,
|
|
221
|
+
children: [teamRoute],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
function TeamPage() {
|
|
225
|
+
// Access parent route's params
|
|
226
|
+
const { orgId } = useRouteParams(orgRoute);
|
|
227
|
+
return <div>Org: {orgId}</div>;
|
|
228
|
+
}`}</CodeBlock>
|
|
229
|
+
<h4>Errors</h4>
|
|
230
|
+
<ul>
|
|
231
|
+
<li>Throws if called outside a route component (no RouteContext).</li>
|
|
232
|
+
<li>
|
|
233
|
+
Throws if the specified route's <code>id</code> is not found in the
|
|
234
|
+
current route hierarchy (neither the current route nor any
|
|
235
|
+
ancestor).
|
|
236
|
+
</li>
|
|
237
|
+
</ul>
|
|
238
|
+
</article>
|
|
239
|
+
|
|
240
|
+
<article className="api-item">
|
|
241
|
+
<h3>
|
|
242
|
+
<code>useRouteState(route)</code>
|
|
243
|
+
</h3>
|
|
244
|
+
<p>
|
|
245
|
+
Returns typed navigation state for the given route definition. Use
|
|
246
|
+
this with routes defined via <code>routeState<T>()</code> to get
|
|
247
|
+
properly typed state.
|
|
248
|
+
</p>
|
|
249
|
+
<CodeBlock language="tsx">{`import { routeState, useRouteState } from "@funstack/router";
|
|
250
|
+
|
|
251
|
+
type ScrollState = { scrollPos: number };
|
|
252
|
+
|
|
253
|
+
const scrollRoute = routeState<ScrollState>()({
|
|
254
|
+
id: "scroll",
|
|
255
|
+
path: "/page",
|
|
256
|
+
component: ScrollPage,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
function ScrollPage() {
|
|
260
|
+
// state is typed as ScrollState | undefined
|
|
261
|
+
const state = useRouteState(scrollRoute);
|
|
262
|
+
|
|
263
|
+
return <div>Scroll position: {state?.scrollPos ?? 0}</div>;
|
|
264
|
+
}`}</CodeBlock>
|
|
265
|
+
<h4>Return Value</h4>
|
|
266
|
+
<p>
|
|
267
|
+
Returns <code>State | undefined</code>. State is{" "}
|
|
268
|
+
<code>undefined</code> on initial visit and when navigating to a new
|
|
269
|
+
entry without state.
|
|
270
|
+
</p>
|
|
271
|
+
<h4>Errors</h4>
|
|
272
|
+
<ul>
|
|
273
|
+
<li>Throws if called outside a route component (no RouteContext).</li>
|
|
274
|
+
<li>
|
|
275
|
+
Throws if the specified route's <code>id</code> is not found in the
|
|
276
|
+
current route hierarchy (neither the current route nor any
|
|
277
|
+
ancestor).
|
|
278
|
+
</li>
|
|
279
|
+
</ul>
|
|
280
|
+
</article>
|
|
281
|
+
|
|
282
|
+
<article className="api-item">
|
|
283
|
+
<h3>
|
|
284
|
+
<code>useRouteData(route)</code>
|
|
285
|
+
</h3>
|
|
286
|
+
<p>
|
|
287
|
+
Returns typed loader data for the given route definition. The data
|
|
288
|
+
type is automatically inferred from the route's <code>loader</code>{" "}
|
|
289
|
+
function.
|
|
290
|
+
</p>
|
|
291
|
+
<CodeBlock language="tsx">{`import { route, useRouteData } from "@funstack/router";
|
|
292
|
+
|
|
293
|
+
const userRoute = route({
|
|
294
|
+
id: "user",
|
|
295
|
+
path: "/users/:userId",
|
|
296
|
+
loader: async ({ params }) => {
|
|
297
|
+
const res = await fetch(\`/api/users/\${params.userId}\`);
|
|
298
|
+
return res.json() as Promise<{ name: string; age: number }>;
|
|
299
|
+
},
|
|
300
|
+
component: UserPage,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
function UserPage() {
|
|
304
|
+
// data is typed as Promise<{ name: string; age: number }>
|
|
305
|
+
const data = useRouteData(userRoute);
|
|
306
|
+
|
|
307
|
+
// Use with React.use() or Suspense
|
|
308
|
+
const user = use(data);
|
|
309
|
+
return <div>User: {user.name}</div>;
|
|
310
|
+
}`}</CodeBlock>
|
|
311
|
+
<h4>Errors</h4>
|
|
312
|
+
<ul>
|
|
313
|
+
<li>Throws if called outside a route component (no RouteContext).</li>
|
|
314
|
+
<li>
|
|
315
|
+
Throws if the specified route's <code>id</code> is not found in the
|
|
316
|
+
current route hierarchy (neither the current route nor any
|
|
317
|
+
ancestor).
|
|
318
|
+
</li>
|
|
319
|
+
</ul>
|
|
320
|
+
</article>
|
|
321
|
+
</div>
|
|
322
|
+
);
|
|
323
|
+
}
|