@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.
Files changed (155) hide show
  1. package/CLAUDE.md +7 -0
  2. package/README.md +19 -0
  3. package/dist/vite/index.js +1298 -0
  4. package/package.json +140 -0
  5. package/skills/caching/SKILL.md +319 -0
  6. package/skills/document-cache/SKILL.md +152 -0
  7. package/skills/hooks/SKILL.md +359 -0
  8. package/skills/intercept/SKILL.md +292 -0
  9. package/skills/layout/SKILL.md +216 -0
  10. package/skills/loader/SKILL.md +365 -0
  11. package/skills/middleware/SKILL.md +442 -0
  12. package/skills/parallel/SKILL.md +255 -0
  13. package/skills/route/SKILL.md +141 -0
  14. package/skills/router-setup/SKILL.md +403 -0
  15. package/skills/theme/SKILL.md +54 -0
  16. package/skills/typesafety/SKILL.md +352 -0
  17. package/src/__mocks__/version.ts +6 -0
  18. package/src/__tests__/component-utils.test.ts +76 -0
  19. package/src/__tests__/route-definition.test.ts +63 -0
  20. package/src/__tests__/urls.test.tsx +436 -0
  21. package/src/browser/event-controller.ts +876 -0
  22. package/src/browser/index.ts +18 -0
  23. package/src/browser/link-interceptor.ts +121 -0
  24. package/src/browser/lru-cache.ts +69 -0
  25. package/src/browser/merge-segment-loaders.ts +126 -0
  26. package/src/browser/navigation-bridge.ts +893 -0
  27. package/src/browser/navigation-client.ts +162 -0
  28. package/src/browser/navigation-store.ts +823 -0
  29. package/src/browser/partial-update.ts +559 -0
  30. package/src/browser/react/Link.tsx +248 -0
  31. package/src/browser/react/NavigationProvider.tsx +275 -0
  32. package/src/browser/react/ScrollRestoration.tsx +94 -0
  33. package/src/browser/react/context.ts +53 -0
  34. package/src/browser/react/index.ts +52 -0
  35. package/src/browser/react/location-state-shared.ts +120 -0
  36. package/src/browser/react/location-state.ts +62 -0
  37. package/src/browser/react/use-action.ts +240 -0
  38. package/src/browser/react/use-client-cache.ts +56 -0
  39. package/src/browser/react/use-handle.ts +178 -0
  40. package/src/browser/react/use-href.tsx +208 -0
  41. package/src/browser/react/use-link-status.ts +134 -0
  42. package/src/browser/react/use-navigation.ts +150 -0
  43. package/src/browser/react/use-segments.ts +188 -0
  44. package/src/browser/request-controller.ts +164 -0
  45. package/src/browser/rsc-router.tsx +353 -0
  46. package/src/browser/scroll-restoration.ts +324 -0
  47. package/src/browser/server-action-bridge.ts +747 -0
  48. package/src/browser/shallow.ts +35 -0
  49. package/src/browser/types.ts +464 -0
  50. package/src/cache/__tests__/document-cache.test.ts +522 -0
  51. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  52. package/src/cache/__tests__/memory-store.test.ts +484 -0
  53. package/src/cache/cache-scope.ts +565 -0
  54. package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
  55. package/src/cache/cf/cf-cache-store.ts +428 -0
  56. package/src/cache/cf/index.ts +19 -0
  57. package/src/cache/document-cache.ts +340 -0
  58. package/src/cache/index.ts +58 -0
  59. package/src/cache/memory-segment-store.ts +150 -0
  60. package/src/cache/memory-store.ts +253 -0
  61. package/src/cache/types.ts +387 -0
  62. package/src/client.rsc.tsx +88 -0
  63. package/src/client.tsx +621 -0
  64. package/src/component-utils.ts +76 -0
  65. package/src/components/DefaultDocument.tsx +23 -0
  66. package/src/default-error-boundary.tsx +88 -0
  67. package/src/deps/browser.ts +8 -0
  68. package/src/deps/html-stream-client.ts +2 -0
  69. package/src/deps/html-stream-server.ts +2 -0
  70. package/src/deps/rsc.ts +10 -0
  71. package/src/deps/ssr.ts +2 -0
  72. package/src/errors.ts +259 -0
  73. package/src/handle.ts +120 -0
  74. package/src/handles/MetaTags.tsx +193 -0
  75. package/src/handles/index.ts +6 -0
  76. package/src/handles/meta.ts +247 -0
  77. package/src/href-client.ts +128 -0
  78. package/src/href-context.ts +33 -0
  79. package/src/href.ts +177 -0
  80. package/src/index.rsc.ts +79 -0
  81. package/src/index.ts +87 -0
  82. package/src/loader.rsc.ts +204 -0
  83. package/src/loader.ts +47 -0
  84. package/src/network-error-thrower.tsx +21 -0
  85. package/src/outlet-context.ts +15 -0
  86. package/src/root-error-boundary.tsx +277 -0
  87. package/src/route-content-wrapper.tsx +198 -0
  88. package/src/route-definition.ts +1371 -0
  89. package/src/route-map-builder.ts +146 -0
  90. package/src/route-types.ts +198 -0
  91. package/src/route-utils.ts +89 -0
  92. package/src/router/__tests__/match-context.test.ts +104 -0
  93. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  94. package/src/router/__tests__/match-result.test.ts +566 -0
  95. package/src/router/__tests__/on-error.test.ts +935 -0
  96. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  97. package/src/router/error-handling.ts +287 -0
  98. package/src/router/handler-context.ts +158 -0
  99. package/src/router/loader-resolution.ts +326 -0
  100. package/src/router/manifest.ts +138 -0
  101. package/src/router/match-context.ts +264 -0
  102. package/src/router/match-middleware/background-revalidation.ts +236 -0
  103. package/src/router/match-middleware/cache-lookup.ts +261 -0
  104. package/src/router/match-middleware/cache-store.ts +266 -0
  105. package/src/router/match-middleware/index.ts +81 -0
  106. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  107. package/src/router/match-middleware/segment-resolution.ts +174 -0
  108. package/src/router/match-pipelines.ts +214 -0
  109. package/src/router/match-result.ts +214 -0
  110. package/src/router/metrics.ts +62 -0
  111. package/src/router/middleware.test.ts +1355 -0
  112. package/src/router/middleware.ts +748 -0
  113. package/src/router/pattern-matching.ts +272 -0
  114. package/src/router/revalidation.ts +190 -0
  115. package/src/router/router-context.ts +299 -0
  116. package/src/router/types.ts +96 -0
  117. package/src/router.ts +3876 -0
  118. package/src/rsc/__tests__/helpers.test.ts +175 -0
  119. package/src/rsc/handler.ts +1060 -0
  120. package/src/rsc/helpers.ts +64 -0
  121. package/src/rsc/index.ts +56 -0
  122. package/src/rsc/nonce.ts +18 -0
  123. package/src/rsc/types.ts +237 -0
  124. package/src/segment-system.tsx +456 -0
  125. package/src/server/__tests__/request-context.test.ts +171 -0
  126. package/src/server/context.ts +417 -0
  127. package/src/server/handle-store.ts +230 -0
  128. package/src/server/loader-registry.ts +174 -0
  129. package/src/server/request-context.ts +554 -0
  130. package/src/server/root-layout.tsx +10 -0
  131. package/src/server/tsconfig.json +14 -0
  132. package/src/server.ts +146 -0
  133. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  134. package/src/ssr/index.tsx +234 -0
  135. package/src/theme/ThemeProvider.tsx +291 -0
  136. package/src/theme/ThemeScript.tsx +61 -0
  137. package/src/theme/__tests__/theme.test.ts +120 -0
  138. package/src/theme/constants.ts +55 -0
  139. package/src/theme/index.ts +58 -0
  140. package/src/theme/theme-context.ts +70 -0
  141. package/src/theme/theme-script.ts +152 -0
  142. package/src/theme/types.ts +182 -0
  143. package/src/theme/use-theme.ts +44 -0
  144. package/src/types.ts +1561 -0
  145. package/src/urls.ts +726 -0
  146. package/src/use-loader.tsx +346 -0
  147. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  148. package/src/vite/expose-action-id.ts +344 -0
  149. package/src/vite/expose-handle-id.ts +209 -0
  150. package/src/vite/expose-loader-id.ts +357 -0
  151. package/src/vite/expose-location-state-id.ts +177 -0
  152. package/src/vite/index.ts +787 -0
  153. package/src/vite/package-resolution.ts +125 -0
  154. package/src/vite/version.d.ts +12 -0
  155. 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,6 @@
1
+ /**
2
+ * Mock for rsc-router:version virtual module.
3
+ * Used by vitest since the virtual module is only available at build time.
4
+ * Empty string disables version path in URLs for simpler test assertions.
5
+ */
6
+ export const VERSION = "";
@@ -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
+ });