@rangojs/router 0.0.0-experimental.107 → 0.0.0-experimental.109

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 (83) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/rango.js +16 -16
  3. package/dist/vite/index.js +146 -150
  4. package/package.json +6 -6
  5. package/skills/hooks/SKILL.md +2 -0
  6. package/skills/links/SKILL.md +13 -1
  7. package/skills/loader/SKILL.md +1 -1
  8. package/skills/middleware/SKILL.md +3 -3
  9. package/skills/mime-routes/SKILL.md +27 -0
  10. package/skills/prerender/SKILL.md +13 -13
  11. package/skills/rango/SKILL.md +9 -0
  12. package/skills/response-routes/SKILL.md +58 -9
  13. package/skills/router-setup/SKILL.md +3 -3
  14. package/skills/typesafety/SKILL.md +273 -31
  15. package/src/__augment-tests__/augment.ts +81 -0
  16. package/src/__augment-tests__/augmented.check.ts +117 -0
  17. package/src/browser/index.ts +3 -3
  18. package/src/browser/react/location-state-shared.ts +3 -3
  19. package/src/browser/react/use-handle.ts +17 -9
  20. package/src/browser/rsc-router.tsx +14 -14
  21. package/src/browser/segment-structure-assert.ts +2 -2
  22. package/src/build/generate-manifest.ts +3 -3
  23. package/src/build/route-types/codegen.ts +4 -4
  24. package/src/build/route-types/include-resolution.ts +1 -1
  25. package/src/build/route-types/per-module-writer.ts +3 -3
  26. package/src/build/route-types/router-processing.ts +4 -4
  27. package/src/build/route-types/scan-filter.ts +1 -1
  28. package/src/client.tsx +4 -7
  29. package/src/errors.ts +1 -1
  30. package/src/handle.ts +2 -2
  31. package/src/href-client.ts +136 -19
  32. package/src/index.rsc.ts +4 -4
  33. package/src/index.ts +2 -2
  34. package/src/loader.rsc.ts +1 -1
  35. package/src/loader.ts +1 -1
  36. package/src/prerender.ts +4 -4
  37. package/src/route-definition/dsl-helpers.ts +2 -2
  38. package/src/route-definition/helpers-types.ts +2 -2
  39. package/src/router/error-handling.ts +1 -1
  40. package/src/router/lazy-includes.ts +2 -2
  41. package/src/router/metrics.ts +1 -1
  42. package/src/router/middleware-types.ts +1 -1
  43. package/src/router/prerender-match.ts +1 -1
  44. package/src/router/router-interfaces.ts +34 -28
  45. package/src/router/router-options.ts +1 -1
  46. package/src/router/router-registry.ts +2 -5
  47. package/src/router/segment-resolution/fresh.ts +2 -2
  48. package/src/router/segment-resolution/revalidation.ts +2 -2
  49. package/src/router.ts +13 -16
  50. package/src/rsc/handler-context.ts +2 -2
  51. package/src/rsc/index.ts +1 -1
  52. package/src/rsc/types.ts +2 -2
  53. package/src/search-params.ts +4 -4
  54. package/src/serialize.ts +243 -0
  55. package/src/server/context.ts +16 -16
  56. package/src/static-handler.ts +1 -1
  57. package/src/types/global-namespace.ts +39 -26
  58. package/src/types/handler-context.ts +3 -3
  59. package/src/urls/path-helper-types.ts +2 -2
  60. package/src/urls/pattern-types.ts +34 -0
  61. package/src/urls/type-extraction.ts +6 -1
  62. package/src/use-loader.tsx +6 -4
  63. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  64. package/src/vite/discovery/discover-routers.ts +3 -3
  65. package/src/vite/discovery/discovery-errors.ts +1 -1
  66. package/src/vite/discovery/prerender-collection.ts +19 -25
  67. package/src/vite/discovery/route-types-writer.ts +3 -3
  68. package/src/vite/discovery/state.ts +4 -4
  69. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
  70. package/src/vite/plugins/expose-action-id.ts +2 -2
  71. package/src/vite/plugins/expose-id-utils.ts +12 -8
  72. package/src/vite/plugins/expose-ids/export-analysis.ts +33 -9
  73. package/src/vite/plugins/expose-internal-ids.ts +1 -1
  74. package/src/vite/plugins/performance-tracks.ts +12 -16
  75. package/src/vite/plugins/use-cache-transform.ts +1 -1
  76. package/src/vite/plugins/version-plugin.ts +2 -2
  77. package/src/vite/plugins/virtual-entries.ts +2 -2
  78. package/src/vite/rango.ts +11 -11
  79. package/src/vite/router-discovery.ts +26 -29
  80. package/src/vite/utils/ast-handler-extract.ts +15 -15
  81. package/src/vite/utils/bundle-analysis.ts +4 -2
  82. package/src/vite/utils/forward-user-plugins.ts +46 -17
  83. package/src/vite/utils/shared-utils.ts +26 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.107",
3
+ "version": "0.0.0-experimental.109",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -135,7 +135,7 @@
135
135
  "scripts": {
136
136
  "build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
137
137
  "prepublishOnly": "pnpm build",
138
- "typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit",
138
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
139
139
  "test": "playwright test",
140
140
  "test:ui": "playwright test --ui",
141
141
  "test:unit": "vitest run",
@@ -143,7 +143,7 @@
143
143
  },
144
144
  "dependencies": {
145
145
  "@types/debug": "^4.1.12",
146
- "@vitejs/plugin-rsc": "^0.5.23",
146
+ "@vitejs/plugin-rsc": "^0.5.26",
147
147
  "debug": "^4.4.1",
148
148
  "magic-string": "^0.30.17",
149
149
  "picomatch": "^4.0.3",
@@ -163,11 +163,11 @@
163
163
  "vitest": "^4.0.0"
164
164
  },
165
165
  "peerDependencies": {
166
- "@cloudflare/vite-plugin": "^1.25.0",
167
- "@vitejs/plugin-rsc": "^0.5.23",
166
+ "@cloudflare/vite-plugin": "^1.38.0",
167
+ "@vitejs/plugin-rsc": "^0.5.26",
168
168
  "react": ">=19.2.6 <20",
169
169
  "react-dom": ">=19.2.6 <20",
170
- "vite": "^7.3.0"
170
+ "vite": "^8.0.0"
171
171
  },
172
172
  "peerDependenciesMeta": {
173
173
  "@cloudflare/vite-plugin": {
@@ -776,6 +776,8 @@ function MountInfo() {
776
776
 
777
777
  Mount-aware local reverse for client components. Import the generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse(".name", params?)`. Auto-fills params from `useParams()`; explicit params override.
778
778
 
779
+ > Per-module `*.gen.ts` files are **CLI opt-in and not Vite-watched** — run `rango generate <urls-file>` (or wire it into `predev`) and re-run it whenever the module's routes change. See `/links` for the full generated-file setup and exposure-boundary rules.
780
+
779
781
  ```tsx
780
782
  "use client";
781
783
  import { Link, useReverse } from "@rangojs/router/client";
@@ -213,7 +213,17 @@ function GlobalNav() {
213
213
  }
214
214
  ```
215
215
 
216
- `href()` provides compile-time validation via `ValidPaths` type. Paths are validated against registered route patterns using `PatternToPath`.
216
+ `href()` provides compile-time validation via the `Rango.Path` type. Paths are validated against registered route patterns using `PatternToPath`.
217
+
218
+ When wrapping `href()`, type the wrapper's path parameter as `Rango.Path` so it
219
+ keeps the same generated-route validation. `Rango.Path` is ambient — no import,
220
+ just like `Rango.Env` / `Rango.Vars`:
221
+
222
+ ```typescript
223
+ import { href } from "@rangojs/router/client";
224
+
225
+ export const appHref = (path: Rango.Path): string => href(path);
226
+ ```
217
227
 
218
228
  `href()` is a raw path helper — it is **not** basename-aware. It returns the path as-is (or with the include mount prefix via `useHref()`). For basename-aware navigation, use `Link`, `useRouter().push()`, or `reverse()`, which auto-prefix root-relative paths with the router's basename.
219
229
 
@@ -263,6 +273,8 @@ function MountInfo() {
263
273
 
264
274
  Hook that returns a typed local reverse function for a `routes` map imported from a generated `.gen.ts` next to a `urls()` module. The route map is the **exposure boundary** — `useReverse` only knows about names in that map, never the full app manifest.
265
275
 
276
+ > Import the per-module `routes` (e.g. `urls/blog.gen.ts`), **not** `router.named-routes.gen.ts`. The named-routes file is the whole app manifest and is server-only data — importing it into a client component pulls every route name into the client bundle.
277
+
266
278
  ```tsx
267
279
  "use client";
268
280
  import { Link, useReverse } from "@rangojs/router/client";
@@ -185,7 +185,7 @@ export const ProductLoader = createLoader(async (ctx) => {
185
185
  // Request headers
186
186
  const auth = ctx.request.headers.get("Authorization");
187
187
 
188
- // Variables set by middleware (from RSCRouter.Vars augmentation)
188
+ // Variables set by middleware (from Rango.Vars augmentation)
189
189
  const user = ctx.get("user");
190
190
 
191
191
  // Type-checked URLs for payloads. `.name` resolves within the current
@@ -196,7 +196,7 @@ export const myMiddleware: Middleware = async (ctx, next) => {
196
196
  ctx.env.DB; // D1Database
197
197
  ctx.env.KV; // KVNamespace
198
198
 
199
- // Set variables for downstream handlers (typed via RSCRouter.Vars)
199
+ // Set variables for downstream handlers (typed via Rango.Vars)
200
200
  ctx.set("user", { id: "123", name: "John" });
201
201
 
202
202
  // Continue to next middleware/handler
@@ -237,8 +237,8 @@ const Dashboard: Handler<"dashboard"> = (ctx) => {
237
237
  ```
238
238
 
239
239
  This works alongside `ctx.get("key")` / `ctx.set("key", value)` (global typing
240
- via RSCRouter.Vars augmentation). Use `createVar` for route-local or feature-scoped
241
- data; use RSCRouter.Vars for app-wide middleware state.
240
+ via Rango.Vars augmentation). Use `createVar` for route-local or feature-scoped
241
+ data; use Rango.Vars for app-wide middleware state.
242
242
 
243
243
  ## Redirect with State in Middleware
244
244
 
@@ -108,6 +108,33 @@ path.text("/api/data", () => "plain text version", { name: "dataText" }),
108
108
  Without an RSC primary, there is no `text/html` candidate — the Accept header
109
109
  picks among the response-type candidates directly.
110
110
 
111
+ ## Type Safety For Negotiated Paths
112
+
113
+ `router.named-routes.gen.ts` validates route names, params, search, `href()`, and
114
+ the `Rango.Path` type, but it does not carry response payload metadata. For MIME or
115
+ response payload types, use one of these surfaces:
116
+
117
+ - `RouteResponse<typeof patterns, "routeName">` for a specific response variant
118
+ by route name. This is the clearest option when several MIME variants share
119
+ one URL pattern.
120
+ - `Rango.PathResponse<"/products/:id">` (ambient, no import) for global lookup by URL pattern or concrete path after the app
121
+ registers `typeof router.routeMap`:
122
+
123
+ ```typescript
124
+ // router.tsx
125
+ export const router = createRouter({ document: Document }).routes(urlpatterns);
126
+
127
+ declare global {
128
+ namespace Rango {
129
+ interface RegisteredRoutes extends typeof router.routeMap {}
130
+ }
131
+ }
132
+ ```
133
+
134
+ `RegisteredRoutes` is what exposes the richer routeMap entries containing
135
+ response payload metadata. Without it, URL-pattern response lookup has paths but
136
+ no payloads, so response types resolve to `ResponseEnvelope<never>`.
137
+
111
138
  ## How It Works
112
139
 
113
140
  1. **Build time**: `buildRouteTrie()` calls `mergeLeaves()` when multiple routes share a pattern.
@@ -358,16 +358,16 @@ Both error types propagate to the router's `onError` callback with phase
358
358
  The build produces per-URL timing logs:
359
359
 
360
360
  ```
361
- [rsc-router] Pre-rendering 12 URL(s) (concurrency: 4)...
362
- [rsc-router] OK /articles/hello (42ms)
363
- [rsc-router] PASS /articles/remote-only (5ms) - live fallback
364
- [rsc-router] SKIP /articles/draft-post (3ms) - Article is a draft
365
- [rsc-router] Pre-render complete: 11 done, 1 skipped (1204ms total)
366
-
367
- [rsc-router] Rendering 3 static handler(s)...
368
- [rsc-router] OK DocsLayout (28ms)
369
- [rsc-router] SKIP TocSidebar (1ms) - Not ready
370
- [rsc-router] Static render complete: 2 done, 1 skipped (120ms total)
361
+ [rango] Pre-rendering 12 URL(s) (concurrency: 4)...
362
+ [rango] OK /articles/hello (42ms)
363
+ [rango] PASS /articles/remote-only (5ms) - live fallback
364
+ [rango] SKIP /articles/draft-post (3ms) - Article is a draft
365
+ [rango] Pre-render complete: 11 done, 1 skipped (1204ms total)
366
+
367
+ [rango] Rendering 3 static handler(s)...
368
+ [rango] OK DocsLayout (28ms)
369
+ [rango] SKIP TocSidebar (1ms) - Not ready
370
+ [rango] Static render complete: 2 done, 1 skipped (120ms total)
371
371
  ```
372
372
 
373
373
  A `FAIL` line is logged per-URL when a handler throws a non-Skip error. The
@@ -463,9 +463,9 @@ export const Product = Passthrough(ProductDef, async (ctx) => {
463
463
  Passthrough entries are logged distinctly:
464
464
 
465
465
  ```
466
- [rsc-router] OK /blog/a (42ms)
467
- [rsc-router] PASS /blog/b (3ms) - live fallback
468
- [rsc-router] OK /blog/c (38ms)
466
+ [rango] OK /blog/a (42ms)
467
+ [rango] PASS /blog/b (3ms) - live fallback
468
+ [rango] OK /blog/c (38ms)
469
469
  ```
470
470
 
471
471
  ## Edge Cases and Constraints
@@ -266,6 +266,15 @@ Each file is classified by its contents:
266
266
  Directories are scanned recursively for `.ts`/`.tsx` files, skipping `node_modules`,
267
267
  dotfiles, and existing `.gen.` files.
268
268
 
269
+ > The two generated files are **not interchangeable surfaces**.
270
+ > `router.named-routes.gen.ts` augments the global `GeneratedRouteMap` for
271
+ > named-route typing (`Handler<"name">`, `ctx.reverse("name")`, prerender).
272
+ > Per-module `*.gen.ts` exports a local `routes` map for `useReverse(routes)`
273
+ > and explicit local handler typing (`Handler<".name", routes>`). Neither
274
+ > carries response payloads — response/MIME payload inference comes from
275
+ > `typeof router.routeMap` via `RegisteredRoutes`, not `*.named-routes.gen.ts`.
276
+ > See `/typesafety` for the full surface breakdown.
277
+
269
278
  ### Recursive includes
270
279
 
271
280
  The generator follows `include()` calls across files, resolving imports to build
@@ -236,22 +236,69 @@ type ProductsData = RouteResponse<typeof apiPatterns, "products">;
236
236
  // = ResponseEnvelope<{ id: string; name: string; price: number }[]>
237
237
  ```
238
238
 
239
- ### PathResponse (global lookup by URL pattern)
239
+ ### Rango.PathResponse (global lookup by URL pattern or concrete path)
240
240
 
241
- Look up response type from the merged route map by URL pattern:
241
+ `Rango.PathResponse` is ambient (no import) and reads from `RegisteredRoutes`,
242
+ which carries response payload metadata. That surface is **not** auto-wired —
243
+ without the augmentation below, `Rango.PathResponse` falls back to the generated
244
+ path/search map, or to a permissive map when nothing is generated. Either way, it
245
+ has no response payload metadata, so response routes resolve to
246
+ `ResponseEnvelope<never>`:
242
247
 
243
248
  ```typescript
244
- import type { PathResponse } from "@rangojs/router/client";
249
+ // router.tsx
250
+ export const router = createRouter({ document: Document }).routes(urlpatterns);
245
251
 
252
+ declare global {
253
+ namespace Rango {
254
+ interface RegisteredRoutes extends typeof router.routeMap {}
255
+ }
256
+ }
257
+ ```
258
+
259
+ With that in place, look up the response type by URL pattern (ambient, no import):
260
+
261
+ ```typescript
246
262
  // After include("/api", apiPatterns) in main urls
247
- type Health = PathResponse<"/api/health">;
263
+ type Health = Rango.PathResponse<"/api/health">;
248
264
  // = ResponseEnvelope<{ status: string; timestamp: number }>
249
265
 
250
266
  // RSC routes return ResponseEnvelope<never>
251
- type Home = PathResponse<"/">;
267
+ type Home = Rango.PathResponse<"/">;
252
268
  // = ResponseEnvelope<never>
253
269
  ```
254
270
 
271
+ `Rango.PathResponse` also accepts a **concrete path**, so it types a `fetch`
272
+ wrapper whose response is inferred from the path you pass:
273
+
274
+ ```typescript
275
+ import { href } from "@rangojs/router/client";
276
+
277
+ async function get<T extends Rango.Path>(
278
+ path: T,
279
+ ): Promise<Rango.PathResponse<T>> {
280
+ return fetch(href(path)).then((r) => r.json());
281
+ }
282
+
283
+ const product = await get("/api/products/42"); // ResponseEnvelope<Product>
284
+ ```
285
+
286
+ Pattern keys (`/:id`) match exactly; a concrete path under a _nested_ dynamic
287
+ route can match several patterns and union their responses.
288
+
289
+ `Rango.PathResponse` reports the JSON **wire** shape, not the handler's raw
290
+ return: `path.json()` serializes with `JSON.stringify`, so a handler returning
291
+ `{ createdAt: Date }` resolves to `ResponseEnvelope<{ createdAt: string }>`. This
292
+ runs through the ambient `Rango.JsonSerialize<T>` transform (`Date -> string`,
293
+ honors `toJSON()`, drops functions/`undefined`, `bigint -> never`). The
294
+ `RouteResponse` surface below applies the same `Rango.JsonSerialize` transform, so
295
+ both response lookups report the identical wire shape.
296
+
297
+ For local/scoped response typing without global augmentation, prefer
298
+ `RouteResponse<typeof patterns, "routeName">` (see the section above) — it reads
299
+ the response payload straight from the `urls()` patterns and needs no
300
+ `RegisteredRoutes` wiring.
301
+
255
302
  ### ParamsFor with Response Routes
256
303
 
257
304
  ```typescript
@@ -361,14 +408,16 @@ export const urlpatterns = urls(({ path, include }) => [
361
408
 
362
409
  ```typescript
363
410
  import type { RouteResponse } from "@rangojs/router";
364
- import type { PathResponse, ParamsFor } from "@rangojs/router/client";
411
+ import type { ParamsFor } from "@rangojs/router/client";
365
412
 
366
- // Scoped (before mount) -- use the module directly
413
+ // Scoped (before mount) -- use the module directly, no global wiring needed
367
414
  type Stats = RouteResponse<typeof blogApiPatterns, "stats">;
368
415
  // = ResponseEnvelope<{ views: number; visitors: number }>
369
416
 
370
- // After mounting -- names get prefixed
371
- type BlogStats = PathResponse<"/blog/api/stats">;
417
+ // After mounting -- names get prefixed.
418
+ // Rango.PathResponse needs `RegisteredRoutes extends typeof router.routeMap` (see above),
419
+ // otherwise it resolves to ResponseEnvelope<never>.
420
+ type BlogStats = Rango.PathResponse<"/blog/api/stats">;
372
421
  // = ResponseEnvelope<{ views: number; visitors: number }>
373
422
 
374
423
  // Params work through nested includes
@@ -71,7 +71,7 @@ urls(
71
71
  ## Router Options
72
72
 
73
73
  ```typescript
74
- interface RSCRouterOptions<TEnv> {
74
+ interface RangoOptions<TEnv> {
75
75
  // URL patterns from urls() function
76
76
  urls: UrlPatterns;
77
77
 
@@ -405,7 +405,7 @@ interface AppBindings {
405
405
  KV: KVNamespace;
406
406
  }
407
407
 
408
- // Variables declared via module augmentation
408
+ // Variables declared via global namespace augmentation
409
409
  interface AppVariables {
410
410
  user?: { id: string; name: string };
411
411
  }
@@ -417,7 +417,7 @@ const router = createRouter<AppBindings>({
417
417
 
418
418
  // Register types globally for implicit typing
419
419
  declare global {
420
- namespace RSCRouter {
420
+ namespace Rango {
421
421
  interface Env extends AppBindings {}
422
422
  interface Vars extends AppVariables {}
423
423
  }