@rangojs/router 0.0.0-experimental.2
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/CLAUDE.md +7 -0
- package/README.md +19 -0
- package/dist/vite/index.js +1298 -0
- package/package.json +140 -0
- package/skills/caching/SKILL.md +319 -0
- package/skills/document-cache/SKILL.md +152 -0
- package/skills/hooks/SKILL.md +359 -0
- package/skills/intercept/SKILL.md +292 -0
- package/skills/layout/SKILL.md +216 -0
- package/skills/loader/SKILL.md +365 -0
- package/skills/middleware/SKILL.md +442 -0
- package/skills/parallel/SKILL.md +255 -0
- package/skills/route/SKILL.md +141 -0
- package/skills/router-setup/SKILL.md +403 -0
- package/skills/theme/SKILL.md +54 -0
- package/skills/typesafety/SKILL.md +352 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/component-utils.test.ts +76 -0
- package/src/__tests__/route-definition.test.ts +63 -0
- package/src/__tests__/urls.test.tsx +436 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +893 -0
- package/src/browser/navigation-client.ts +162 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +559 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +275 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +178 -0
- package/src/browser/react/use-href.tsx +208 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-navigation.ts +150 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +164 -0
- package/src/browser/rsc-router.tsx +353 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/server-action-bridge.ts +747 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +464 -0
- package/src/cache/__tests__/document-cache.test.ts +522 -0
- package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
- package/src/cache/__tests__/memory-store.test.ts +484 -0
- package/src/cache/cache-scope.ts +565 -0
- package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
- package/src/cache/cf/cf-cache-store.ts +428 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/document-cache.ts +340 -0
- package/src/cache/index.ts +58 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +387 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +621 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +23 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +259 -0
- package/src/handle.ts +120 -0
- package/src/handles/MetaTags.tsx +193 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/href-client.ts +128 -0
- package/src/href-context.ts +33 -0
- package/src/href.ts +177 -0
- package/src/index.rsc.ts +79 -0
- package/src/index.ts +87 -0
- package/src/loader.rsc.ts +204 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +198 -0
- package/src/route-definition.ts +1371 -0
- package/src/route-map-builder.ts +146 -0
- package/src/route-types.ts +198 -0
- package/src/route-utils.ts +89 -0
- package/src/router/__tests__/match-context.test.ts +104 -0
- package/src/router/__tests__/match-pipelines.test.ts +537 -0
- package/src/router/__tests__/match-result.test.ts +566 -0
- package/src/router/__tests__/on-error.test.ts +935 -0
- package/src/router/__tests__/pattern-matching.test.ts +577 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +158 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +138 -0
- package/src/router/match-context.ts +264 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +261 -0
- package/src/router/match-middleware/cache-store.ts +266 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +268 -0
- package/src/router/match-middleware/segment-resolution.ts +174 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +214 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.test.ts +1355 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +272 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +299 -0
- package/src/router/types.ts +96 -0
- package/src/router.ts +3876 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +1060 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +237 -0
- package/src/segment-system.tsx +456 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +417 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +146 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +234 -0
- package/src/theme/ThemeProvider.tsx +291 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/__tests__/theme.test.ts +120 -0
- package/src/theme/constants.ts +55 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/theme-context.ts +70 -0
- package/src/theme/theme-script.ts +152 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types.ts +1561 -0
- package/src/urls.ts +726 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +357 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/index.ts +787 -0
- package/src/vite/package-resolution.ts +125 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +109 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: typesafety
|
|
3
|
+
description: Set up type-safe routes, params, and environment types in rsc-router
|
|
4
|
+
argument-hint: [setup]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Type Safety Setup
|
|
8
|
+
|
|
9
|
+
rsc-router provides end-to-end type safety for routes, parameters, and environment.
|
|
10
|
+
|
|
11
|
+
## Route Type Registration
|
|
12
|
+
|
|
13
|
+
Register route types globally for type-safe `href()` and params:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// router.tsx
|
|
17
|
+
import { createRSCRouter } from "rsc-router/server";
|
|
18
|
+
import { homeRoutes } from "./routes/home";
|
|
19
|
+
import { shopRoutes } from "./routes/shop";
|
|
20
|
+
|
|
21
|
+
const _router = createRSCRouter<AppEnv>({ document: Document })
|
|
22
|
+
.routes(homeRoutes)
|
|
23
|
+
.map(() => import("./handlers/home"))
|
|
24
|
+
.routes("/shop", shopRoutes)
|
|
25
|
+
.map(() => import("./handlers/shop"));
|
|
26
|
+
|
|
27
|
+
// Extract accumulated route types
|
|
28
|
+
type AppRoutes = typeof _router.routeMap;
|
|
29
|
+
|
|
30
|
+
// Register globally via module augmentation
|
|
31
|
+
declare global {
|
|
32
|
+
namespace RSCRouter {
|
|
33
|
+
interface RegisteredRoutes extends AppRoutes {}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default _router;
|
|
38
|
+
export const href = _router.href;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Route Definition with Type-Safe Keys
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// routes/shop.ts
|
|
45
|
+
import { route } from "rsc-router";
|
|
46
|
+
|
|
47
|
+
export const shopRoutes = route({
|
|
48
|
+
"shop.index": "/",
|
|
49
|
+
"shop.products": "/products",
|
|
50
|
+
"shop.product": "/products/:slug",
|
|
51
|
+
"shop.cart": "/cart",
|
|
52
|
+
"shop.checkout": "/checkout/:step?",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Type is inferred:
|
|
56
|
+
// {
|
|
57
|
+
// "shop.index": "/",
|
|
58
|
+
// "shop.products": "/products",
|
|
59
|
+
// "shop.product": "/products/:slug",
|
|
60
|
+
// ...
|
|
61
|
+
// }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Type-Safe href()
|
|
65
|
+
|
|
66
|
+
After registration, `href()` has full autocomplete:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { href } from "./router";
|
|
70
|
+
|
|
71
|
+
// Autocomplete shows all registered routes
|
|
72
|
+
href("shop.index"); // "/shop"
|
|
73
|
+
href("shop.products"); // "/shop/products"
|
|
74
|
+
href("shop.product", { slug: "widget" }); // "/shop/products/widget"
|
|
75
|
+
|
|
76
|
+
// TypeScript errors for:
|
|
77
|
+
href("invalid.route"); // Error: not a valid route
|
|
78
|
+
href("shop.product"); // Error: missing required param 'slug'
|
|
79
|
+
href("shop.product", { wrong: "param" }); // Error: 'wrong' not in params
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Type-Safe Params in Handlers
|
|
83
|
+
|
|
84
|
+
Params are automatically typed based on route patterns:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// handlers/shop.tsx
|
|
88
|
+
import { map } from "rsc-router/server";
|
|
89
|
+
import type { shopRoutes } from "../routes/shop";
|
|
90
|
+
|
|
91
|
+
export default map<typeof shopRoutes>(({ route }) => [
|
|
92
|
+
// ctx.params is typed as { slug: string }
|
|
93
|
+
route("shop.product", (ctx) => {
|
|
94
|
+
const { slug } = ctx.params; // TypeScript knows slug exists
|
|
95
|
+
return <ProductPage slug={slug} />;
|
|
96
|
+
}),
|
|
97
|
+
|
|
98
|
+
// ctx.params is typed as { step?: string }
|
|
99
|
+
route("shop.checkout", (ctx) => {
|
|
100
|
+
const step = ctx.params.step ?? "shipping"; // Optional param
|
|
101
|
+
return <CheckoutPage step={step} />;
|
|
102
|
+
}),
|
|
103
|
+
]);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Environment Type Setup
|
|
107
|
+
|
|
108
|
+
Define your app's environment for type-safe bindings and variables:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// env.ts
|
|
112
|
+
import type { RouterEnv } from "rsc-router/server";
|
|
113
|
+
|
|
114
|
+
// Cloudflare bindings
|
|
115
|
+
interface AppBindings {
|
|
116
|
+
DB: D1Database;
|
|
117
|
+
KV: KVNamespace;
|
|
118
|
+
CACHE: KVNamespace;
|
|
119
|
+
AI: Ai;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Variables set by middleware (ctx.set/ctx.get)
|
|
123
|
+
interface AppVariables {
|
|
124
|
+
user?: { id: string; email: string; role: string };
|
|
125
|
+
requestId?: string;
|
|
126
|
+
permissions?: string[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Combined environment type
|
|
130
|
+
export type AppEnv = RouterEnv<AppBindings, AppVariables>;
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Using Environment Types
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// router.tsx
|
|
137
|
+
import type { AppEnv } from "./env";
|
|
138
|
+
|
|
139
|
+
const router = createRSCRouter<AppEnv>({ document: Document })
|
|
140
|
+
// ...
|
|
141
|
+
|
|
142
|
+
// middleware - typed ctx.set/ctx.get
|
|
143
|
+
const authMiddleware = async (ctx: MiddlewareContext<AppEnv>, next) => {
|
|
144
|
+
ctx.set("user", { id: "123", email: "user@example.com", role: "admin" });
|
|
145
|
+
await next();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// handlers - typed access
|
|
149
|
+
route("dashboard", (ctx) => {
|
|
150
|
+
const user = ctx.get("user"); // { id: string; email: string; role: string } | undefined
|
|
151
|
+
const db = ctx.env.Bindings.DB; // D1Database
|
|
152
|
+
return <Dashboard user={user} />;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// loaders - typed context
|
|
156
|
+
export const UserLoader = createLoader<AppEnv>(async (ctx) => {
|
|
157
|
+
const db = ctx.env.Bindings.DB;
|
|
158
|
+
const userId = ctx.get("user")?.id;
|
|
159
|
+
return db.prepare("SELECT * FROM users WHERE id = ?").bind(userId).first();
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Global Type Registration
|
|
164
|
+
|
|
165
|
+
Register environment types globally for implicit typing:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// router.tsx
|
|
169
|
+
declare global {
|
|
170
|
+
namespace RSCRouter {
|
|
171
|
+
interface RegisteredRoutes extends AppRoutes {}
|
|
172
|
+
interface Env extends AppEnv {}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Now handlers have typed context without explicit imports:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// handlers/dashboard.tsx - no type imports needed
|
|
181
|
+
export default map(({ route }) => [
|
|
182
|
+
route("dashboard", (ctx) => {
|
|
183
|
+
// ctx.get("user") is typed from global Env
|
|
184
|
+
// ctx.params is typed from global RegisteredRoutes
|
|
185
|
+
const user = ctx.get("user");
|
|
186
|
+
return <Dashboard user={user} />;
|
|
187
|
+
}),
|
|
188
|
+
]);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Route Key Conflict Detection
|
|
192
|
+
|
|
193
|
+
TypeScript detects duplicate keys with different URL patterns:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// This causes a type error:
|
|
197
|
+
const router = createRSCRouter<AppEnv>({ document: Document })
|
|
198
|
+
.routes({ index: "/" })
|
|
199
|
+
.map(() => import("./home"))
|
|
200
|
+
.routes({ index: "/other" }) // Error! 'index' already exists with different pattern
|
|
201
|
+
.map(() => import("./other"));
|
|
202
|
+
|
|
203
|
+
// Error message:
|
|
204
|
+
// Property 'map' does not exist on type '{
|
|
205
|
+
// __error: "Route key conflict! Keys [index] already exist with different URL patterns.";
|
|
206
|
+
// hint: "Use unique key names for each route definition.";
|
|
207
|
+
// }'
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Avoiding Conflicts
|
|
211
|
+
|
|
212
|
+
Use namespaced keys:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// Good - unique keys
|
|
216
|
+
export const homeRoutes = route({ "home.index": "/" });
|
|
217
|
+
export const blogRoutes = route({ "blog.index": "/", "blog.post": "/:slug" });
|
|
218
|
+
|
|
219
|
+
// router.tsx
|
|
220
|
+
.routes(homeRoutes)
|
|
221
|
+
.map(() => import("./home"))
|
|
222
|
+
.routes("/blog", blogRoutes) // Keys stay: blog.index, blog.post
|
|
223
|
+
.map(() => import("./blog")) // URLs become: /blog/, /blog/:slug
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Loader Type Safety
|
|
227
|
+
|
|
228
|
+
Loaders have typed return values:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// loaders/product.ts
|
|
232
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
233
|
+
return {
|
|
234
|
+
id: ctx.params.slug,
|
|
235
|
+
name: "Widget",
|
|
236
|
+
price: 99,
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// In handler - type is inferred
|
|
241
|
+
route("shop.product", async (ctx) => {
|
|
242
|
+
const product = await ctx.use(ProductLoader);
|
|
243
|
+
// product: { id: string; name: string; price: number }
|
|
244
|
+
return <ProductPage product={product} />;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// In client component - same type
|
|
248
|
+
function ProductPrice() {
|
|
249
|
+
const { data } = useLoader(ProductLoader);
|
|
250
|
+
// data: { id: string; name: string; price: number }
|
|
251
|
+
return <span>${data.price}</span>;
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Handle Type Safety
|
|
256
|
+
|
|
257
|
+
Handles have typed data:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// handles/breadcrumbs.ts
|
|
261
|
+
import { createHandle } from "rsc-router";
|
|
262
|
+
|
|
263
|
+
export const Breadcrumbs = createHandle<{ label: string; href: string }>();
|
|
264
|
+
|
|
265
|
+
// In handler - typed push
|
|
266
|
+
route("shop.product", (ctx) => {
|
|
267
|
+
const push = ctx.use(Breadcrumbs);
|
|
268
|
+
push({ label: "Products", href: "/shop/products" }); // Typed
|
|
269
|
+
push({ wrong: "data" }); // Error!
|
|
270
|
+
return <ProductPage />;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// In client - typed array
|
|
274
|
+
function BreadcrumbNav() {
|
|
275
|
+
const crumbs = useHandle(Breadcrumbs);
|
|
276
|
+
// crumbs: Array<{ label: string; href: string }>
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Location State Type Safety
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// location-states.ts
|
|
284
|
+
import { createLocationState } from "rsc-router";
|
|
285
|
+
|
|
286
|
+
export const ProductPreview = createLocationState<{
|
|
287
|
+
name: string;
|
|
288
|
+
price: number;
|
|
289
|
+
image: string;
|
|
290
|
+
}>();
|
|
291
|
+
|
|
292
|
+
// Passing state through Link
|
|
293
|
+
<Link
|
|
294
|
+
to={href("shop.product", { slug: "widget" })}
|
|
295
|
+
state={[ProductPreview({ name: "Widget", price: 99, image: "/img.jpg" })]}
|
|
296
|
+
>
|
|
297
|
+
View Product
|
|
298
|
+
</Link>
|
|
299
|
+
|
|
300
|
+
// Reading state in component
|
|
301
|
+
function ProductHeader() {
|
|
302
|
+
const preview = useLocationState(ProductPreview);
|
|
303
|
+
// preview: { name: string; price: number; image: string } | undefined
|
|
304
|
+
|
|
305
|
+
if (preview) {
|
|
306
|
+
return <h1>{preview.name} - ${preview.price}</h1>;
|
|
307
|
+
}
|
|
308
|
+
return <h1>Loading...</h1>;
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Complete Type-Safe Setup
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// 1. env.ts - Environment types
|
|
316
|
+
export type AppEnv = RouterEnv<AppBindings, AppVariables>;
|
|
317
|
+
|
|
318
|
+
// 2. routes/*.ts - Route definitions
|
|
319
|
+
export const shopRoutes = route({
|
|
320
|
+
"shop.index": "/",
|
|
321
|
+
"shop.product": "/products/:slug",
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// 3. router.tsx - Registration
|
|
325
|
+
const _router = createRSCRouter<AppEnv>({ document: Document })
|
|
326
|
+
.routes(shopRoutes)
|
|
327
|
+
.map(() => import("./handlers/shop"));
|
|
328
|
+
|
|
329
|
+
type AppRoutes = typeof _router.routeMap;
|
|
330
|
+
|
|
331
|
+
declare global {
|
|
332
|
+
namespace RSCRouter {
|
|
333
|
+
interface RegisteredRoutes extends AppRoutes {}
|
|
334
|
+
interface Env extends AppEnv {}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export default _router;
|
|
339
|
+
export const href = _router.href;
|
|
340
|
+
|
|
341
|
+
// 4. handlers/*.tsx - Type-safe handlers
|
|
342
|
+
export default map<typeof shopRoutes>(({ route }) => [
|
|
343
|
+
route("shop.product", (ctx) => {
|
|
344
|
+
// ctx.params: { slug: string }
|
|
345
|
+
// ctx.get("user"): User | undefined
|
|
346
|
+
// ctx.env.Bindings.DB: D1Database
|
|
347
|
+
}),
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
// 5. components/*.tsx - Type-safe client code
|
|
351
|
+
<Link to={href("shop.product", { slug: "widget" })}>Widget</Link>
|
|
352
|
+
```
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { isClientComponent, assertClientComponent } from "../component-utils";
|
|
3
|
+
|
|
4
|
+
describe("component-utils", () => {
|
|
5
|
+
describe("isClientComponent", () => {
|
|
6
|
+
it("should return false for regular functions", () => {
|
|
7
|
+
const ServerComponent = () => null;
|
|
8
|
+
expect(isClientComponent(ServerComponent)).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should return false for non-functions", () => {
|
|
12
|
+
expect(isClientComponent(null)).toBe(false);
|
|
13
|
+
expect(isClientComponent(undefined)).toBe(false);
|
|
14
|
+
expect(isClientComponent("string")).toBe(false);
|
|
15
|
+
expect(isClientComponent(123)).toBe(false);
|
|
16
|
+
expect(isClientComponent({})).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should return true for functions with client reference marker", () => {
|
|
20
|
+
const ClientComponent = () => null;
|
|
21
|
+
// Simulate what the bundler does for "use client" components
|
|
22
|
+
(ClientComponent as any).$$typeof = Symbol.for("react.client.reference");
|
|
23
|
+
(ClientComponent as any).$$id = "src/components/MyComponent.tsx#default";
|
|
24
|
+
|
|
25
|
+
expect(isClientComponent(ClientComponent)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return false for functions with wrong $$typeof symbol", () => {
|
|
29
|
+
const Component = () => null;
|
|
30
|
+
(Component as any).$$typeof = Symbol.for("react.element");
|
|
31
|
+
|
|
32
|
+
expect(isClientComponent(Component)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("assertClientComponent", () => {
|
|
37
|
+
it("should throw for non-function values", () => {
|
|
38
|
+
expect(() => assertClientComponent(null, "document")).toThrow(
|
|
39
|
+
'document must be a client component function with "use client" directive'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(() => assertClientComponent({}, "document")).toThrow(
|
|
43
|
+
'document must be a client component function with "use client" directive'
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should throw for server components (no client marker)", () => {
|
|
48
|
+
const ServerComponent = () => null;
|
|
49
|
+
|
|
50
|
+
expect(() => assertClientComponent(ServerComponent, "document")).toThrow(
|
|
51
|
+
'document must be a client component with "use client" directive'
|
|
52
|
+
);
|
|
53
|
+
expect(() => assertClientComponent(ServerComponent, "document")).toThrow(
|
|
54
|
+
"cannot be serialized in the RSC payload"
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should not throw for client components", () => {
|
|
59
|
+
const ClientComponent = () => null;
|
|
60
|
+
(ClientComponent as any).$$typeof = Symbol.for("react.client.reference");
|
|
61
|
+
(ClientComponent as any).$$id = "src/document.tsx#default";
|
|
62
|
+
|
|
63
|
+
expect(() =>
|
|
64
|
+
assertClientComponent(ClientComponent, "document")
|
|
65
|
+
).not.toThrow();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should include component name in error message", () => {
|
|
69
|
+
const ServerComponent = () => null;
|
|
70
|
+
|
|
71
|
+
expect(() => assertClientComponent(ServerComponent, "myLayout")).toThrow(
|
|
72
|
+
"myLayout must be a client component"
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { route } from "../route-definition";
|
|
3
|
+
|
|
4
|
+
describe("route()", () => {
|
|
5
|
+
describe("with nested RouteConfig objects", () => {
|
|
6
|
+
it("should flatten nested RouteConfig with trailing slash config", () => {
|
|
7
|
+
const routes = route({
|
|
8
|
+
index: "/",
|
|
9
|
+
trailingSlash: {
|
|
10
|
+
ignore: { path: "/ts-ignore", trailingSlash: "ignore" },
|
|
11
|
+
always: { path: "/ts-always", trailingSlash: "always" },
|
|
12
|
+
never: { path: "/ts-never", trailingSlash: "never" },
|
|
13
|
+
},
|
|
14
|
+
}) as any;
|
|
15
|
+
|
|
16
|
+
// Check routes are flattened correctly
|
|
17
|
+
expect(routes.index).toBe("/");
|
|
18
|
+
expect(routes["trailingSlash.ignore"]).toBe("/ts-ignore");
|
|
19
|
+
expect(routes["trailingSlash.always"]).toBe("/ts-always");
|
|
20
|
+
expect(routes["trailingSlash.never"]).toBe("/ts-never");
|
|
21
|
+
|
|
22
|
+
// Check trailing slash config is attached
|
|
23
|
+
expect(routes.__trailingSlash).toBeDefined();
|
|
24
|
+
expect(routes.__trailingSlash["trailingSlash.ignore"]).toBe("ignore");
|
|
25
|
+
expect(routes.__trailingSlash["trailingSlash.always"]).toBe("always");
|
|
26
|
+
expect(routes.__trailingSlash["trailingSlash.never"]).toBe("never");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should be enumerable for Object.entries", () => {
|
|
30
|
+
const routes = route({
|
|
31
|
+
index: "/",
|
|
32
|
+
trailingSlash: {
|
|
33
|
+
ignore: { path: "/ts-ignore", trailingSlash: "ignore" },
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const entries = Object.entries(routes);
|
|
38
|
+
expect(entries).toContainEqual(["index", "/"]);
|
|
39
|
+
expect(entries).toContainEqual(["trailingSlash.ignore", "/ts-ignore"]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should apply global default to string routes", () => {
|
|
43
|
+
const routes = route({
|
|
44
|
+
blog: "/blog",
|
|
45
|
+
about: "/about",
|
|
46
|
+
}, { trailingSlash: "never" }) as any;
|
|
47
|
+
|
|
48
|
+
expect(routes.__trailingSlash).toBeDefined();
|
|
49
|
+
expect(routes.__trailingSlash.blog).toBe("never");
|
|
50
|
+
expect(routes.__trailingSlash.about).toBe("never");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should let per-route config override global default", () => {
|
|
54
|
+
const routes = route({
|
|
55
|
+
blog: "/blog",
|
|
56
|
+
api: { path: "/api", trailingSlash: "ignore" },
|
|
57
|
+
}, { trailingSlash: "never" }) as any;
|
|
58
|
+
|
|
59
|
+
expect(routes.__trailingSlash.blog).toBe("never");
|
|
60
|
+
expect(routes.__trailingSlash.api).toBe("ignore");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|