@rangojs/router 0.0.0-experimental.fb4fdc18 → 0.0.0-experimental.fce7fbd1
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/README.md +9 -9
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +914 -485
- package/package.json +55 -11
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +214 -18
- package/skills/host-router/SKILL.md +45 -20
- package/skills/intercept/SKILL.md +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +149 -6
- package/skills/middleware/SKILL.md +13 -9
- package/skills/migrate-nextjs/SKILL.md +1 -1
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +5 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +58 -9
- package/skills/route/SKILL.md +13 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +53 -41
- package/skills/testing/SKILL.md +599 -0
- package/skills/typesafety/SKILL.md +310 -26
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/event-controller.ts +42 -66
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +6 -6
- package/src/browser/navigation-client.ts +12 -15
- package/src/browser/navigation-store.ts +7 -8
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +9 -19
- package/src/browser/react/NavigationProvider.tsx +29 -40
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-params.ts +3 -4
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +14 -1
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +30 -16
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +2 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +49 -6
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +10 -8
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +6 -4
- package/src/index.ts +13 -6
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -41
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +238 -263
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +37 -14
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +4 -42
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +2 -2
- package/src/router/loader-resolution.ts +16 -2
- package/src/router/match-handlers.ts +62 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +32 -30
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +1 -1
- package/src/router/middleware.ts +46 -78
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +43 -1
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +19 -6
- package/src/router/segment-resolution/revalidation.ts +19 -6
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/types.ts +8 -0
- package/src/router.ts +37 -21
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +20 -65
- package/src/rsc/helpers.ts +22 -2
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/response-route-handler.ts +32 -52
- package/src/rsc/rsc-rendering.ts +27 -53
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +13 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +2 -2
- package/src/search-params.ts +4 -4
- package/src/segment-system.tsx +121 -65
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +118 -51
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +10 -0
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +193 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +56 -11
- package/src/types/index.ts +1 -0
- package/src/types/segments.ts +18 -1
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +11 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +70 -48
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +19 -25
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +3 -7
- package/src/vite/plugins/client-ref-hashing.ts +12 -1
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
- package/src/vite/plugins/expose-action-id.ts +2 -2
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-internal-ids.ts +47 -67
- package/src/vite/plugins/performance-tracks.ts +12 -16
- package/src/vite/plugins/use-cache-transform.ts +13 -11
- package/src/vite/plugins/version-injector.ts +2 -12
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +67 -15
- package/src/vite/router-discovery.ts +208 -63
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
package/skills/links/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: links
|
|
3
|
-
description: URL generation with ctx.reverse (server default), href (client), useHref (mounted), useMount, and scopedReverse
|
|
4
|
-
argument-hint: [ctx.reverse|href|useHref|useMount|scopedReverse]
|
|
3
|
+
description: URL generation with ctx.reverse (server default), href (client), useHref (mounted), useMount, useReverse, and scopedReverse
|
|
4
|
+
argument-hint: [ctx.reverse|href|useHref|useMount|useReverse|scopedReverse]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Links & URL Generation
|
|
@@ -10,7 +10,12 @@ argument-hint: [ctx.reverse|href|useHref|useMount|scopedReverse]
|
|
|
10
10
|
|
|
11
11
|
**Default server API: `ctx.reverse()`.** Generate URLs from the handler context — it's typed, auto-fills mount params, and resolves local (`.name`) and absolute (`name.sub`) names.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
**On the client, two patterns:**
|
|
14
|
+
|
|
15
|
+
1. **Receive URLs as props / loader data / action return.** The default. The server has the full route manifest and handler context — generate URLs there and hand strings to client components.
|
|
16
|
+
2. **`useReverse(routes)`.** Import a generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse("name", params?)` (the leading dot is optional). Mount-aware via `useMount()`, auto-fills params from `useParams()`, fully typed from the imported map. Use this when a client component needs to generate URLs into a known module without round-tripping through the server.
|
|
17
|
+
|
|
18
|
+
`ctx.reverse()` itself is **server-only** — it depends on the full route manifest and handler context. Client components never import or call it.
|
|
14
19
|
|
|
15
20
|
## Server: ctx.reverse()
|
|
16
21
|
|
|
@@ -127,9 +132,7 @@ path("/product/:slug", (ctx) => {
|
|
|
127
132
|
|
|
128
133
|
## Client components: receive URLs as props
|
|
129
134
|
|
|
130
|
-
`reverse()` is not available inside `"use client"` modules — there is no handler context
|
|
131
|
-
|
|
132
|
-
Three patterns, in order of preference:
|
|
135
|
+
`ctx.reverse()` is not available inside `"use client"` modules — there is no handler context in the browser bundle. For in-module names, prefer `useReverse(routes)` (see below) and import the relevant `urls/*.gen.js`. For cross-module URLs or one-off names, generate the URL on the server and hand it to the client component using one of these three patterns:
|
|
133
136
|
|
|
134
137
|
1. Pass as a prop from a server component:
|
|
135
138
|
|
|
@@ -210,7 +213,17 @@ function GlobalNav() {
|
|
|
210
213
|
}
|
|
211
214
|
```
|
|
212
215
|
|
|
213
|
-
`href()` provides compile-time validation via `
|
|
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
|
+
```
|
|
214
227
|
|
|
215
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.
|
|
216
229
|
|
|
@@ -256,18 +269,161 @@ function MountInfo() {
|
|
|
256
269
|
|
|
257
270
|
`useMount()` reads from `MountContext`, which is automatically set by `include()` in the segment tree.
|
|
258
271
|
|
|
259
|
-
##
|
|
272
|
+
## Client: useReverse(routes)
|
|
273
|
+
|
|
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.
|
|
275
|
+
|
|
276
|
+
> **Which map?** `useReverse` accepts any routes map. Prefer the per-module `routes` (e.g. `urls/blog.gen.ts`): it gives **mount-aware** local `.name` reverse (auto-prefixes the `include()` mount) and only that module's names enter the client bundle. You _can_ instead pass `router.named-routes.gen.ts` (`NamedRoutes`) for **global** names (`blog.post`; the leading dot is optional) — it is a plain importable map and works on the client (it is **not** server-only) — but its paths are **absolute** while `useReverse` mount-prefixes, so it is correct only at the root mount (under a non-root mount it double-prefixes), and importing it pulls every route name and pattern in the app into the client bundle (a small names-to-paths map — not components or loaders), versus the per-module map which exposes only one module's names. So the per-module map is preferred for in-module links; the named-routes map is the escape hatch for global names.
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
"use client";
|
|
280
|
+
import { Link, useReverse } from "@rangojs/router/client";
|
|
281
|
+
import { routes as blogRoutes } from "../urls/blog.gen.js";
|
|
282
|
+
|
|
283
|
+
export function BlogNav() {
|
|
284
|
+
const reverse = useReverse(blogRoutes);
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<nav>
|
|
288
|
+
<Link to={reverse("index")}>Blog</Link>
|
|
289
|
+
<Link to={reverse("post", { postId: "hello" })}>Post</Link>
|
|
290
|
+
</nav>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### How it resolves
|
|
296
|
+
|
|
297
|
+
1. Strips an optional leading `.` and looks up the name in the imported `routes` map.
|
|
298
|
+
2. Joins the local pattern with the surrounding `useMount()` value — the include's URL pattern.
|
|
299
|
+
3. Substitutes params: explicit params from the call, then auto-filled from `useParams()` for anything still unresolved (mount params like `:tenantId` flow in this way).
|
|
300
|
+
4. Appends a query string if a search object is passed and the route has a `search` schema.
|
|
301
|
+
|
|
302
|
+
### Mount-relativity
|
|
303
|
+
|
|
304
|
+
Patterns in the generated `routes` map are **mount-relative** — they're the patterns as defined inside the `urls()` module, _not_ the full app paths. Mount-joining happens at runtime via `useMount()`, so the same component works under any include:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
// urls/blog.tsx
|
|
308
|
+
export const blogPatterns = urls(({ path }) => [
|
|
309
|
+
path("/", BlogIndex, { name: "index" }),
|
|
310
|
+
path("/:postId", BlogPost, { name: "post" }),
|
|
311
|
+
]);
|
|
312
|
+
|
|
313
|
+
// Generated urls/blog.gen.ts
|
|
314
|
+
// export const routes = { index: "/", post: "/:postId" } as const;
|
|
315
|
+
|
|
316
|
+
// urls.tsx — same module mounted twice
|
|
317
|
+
include("/news", blogPatterns, { name: "news" }), // <BlogNav> renders /news, /news/hello
|
|
318
|
+
include("/journal", blogPatterns, { name: "diary" }), // <BlogNav> renders /journal, /journal/hello
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The `/` pattern under a non-root mount collapses cleanly: under `/news`, `reverse(".index")` returns `/news` (no trailing slash), matching `ctx.reverse(".index")` on the server.
|
|
322
|
+
|
|
323
|
+
### Auto-filled params (mount params)
|
|
324
|
+
|
|
325
|
+
When the include itself carries `:params`, those are auto-filled from `useParams()` so the caller doesn't have to thread them through:
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// urls.tsx
|
|
329
|
+
include("/tenant/:tenantId", clientReversePatterns, { name: "tenant" });
|
|
330
|
+
```
|
|
260
331
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
332
|
+
```tsx
|
|
333
|
+
// At /tenant/acme/posts/p1, useParams() = { tenantId: "acme", postId: "p1" }
|
|
334
|
+
const reverse = useReverse(clientReverseRoutes);
|
|
335
|
+
|
|
336
|
+
reverse(".index"); // "/tenant/acme"
|
|
337
|
+
reverse(".post", { postId: "p2" }); // "/tenant/acme/posts/p2" (tenantId auto-filled)
|
|
338
|
+
reverse(".post", { tenantId: "other", postId: "p2" }); // "/tenant/other/posts/p2" (explicit override)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Auto-fill follows soft navigation — when the matched route changes, `useReverse` re-renders with the new params.
|
|
342
|
+
|
|
343
|
+
### Search schemas
|
|
344
|
+
|
|
345
|
+
Routes declared with a `search` schema accept a typed search object as the third argument:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// urls/blog.tsx
|
|
349
|
+
path("/search", SearchPage, {
|
|
350
|
+
name: "search",
|
|
351
|
+
search: { q: "string", page: "number?" },
|
|
352
|
+
}),
|
|
353
|
+
|
|
354
|
+
// Generated as: search: { path: "/search", search: { q: "string", page: "number?" } }
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
const reverse = useReverse(blogRoutes);
|
|
359
|
+
reverse(".search", {}, { q: "hello world", page: 2 });
|
|
360
|
+
// "/news/search?q=hello%20world&page=2"
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Errors
|
|
364
|
+
|
|
365
|
+
- Unknown name: throws `Unknown route: ".not-a-route"`.
|
|
366
|
+
- Missing required param: throws `Missing param "postId" for route ".detail"`.
|
|
367
|
+
|
|
368
|
+
Both happen synchronously during `reverse()` — wrap calls in try/catch (or an ErrorBoundary if the throw happens during render) when you need to surface them as UI.
|
|
369
|
+
|
|
370
|
+
### The leading dot is optional
|
|
371
|
+
|
|
372
|
+
`reverse("post")` and `reverse(".post")` resolve **identically** — the leading dot is cosmetic. The map you import IS the scope, so there is no separate global namespace to disambiguate and the dot carries no meaning; it exists only as a readability convention and for parity with `ctx.reverse(".name")` on the server. To link into a different module, import that module's `routes`:
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
import { routes as blogRoutes } from "../urls/blog.gen.js";
|
|
376
|
+
import { routes as shopRoutes } from "../urls/shop.gen.js";
|
|
377
|
+
|
|
378
|
+
function CrossNav() {
|
|
379
|
+
const blog = useReverse(blogRoutes);
|
|
380
|
+
const shop = useReverse(shopRoutes);
|
|
381
|
+
return (
|
|
382
|
+
<nav>
|
|
383
|
+
<Link to={blog("index")}>Blog</Link>
|
|
384
|
+
<Link to={shop("cart")}>Cart</Link>
|
|
385
|
+
</nav>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Codegen
|
|
391
|
+
|
|
392
|
+
Each `urls()` module gets a sibling `.gen.ts` with the local route names and patterns, produced by `rango generate`:
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
pnpm exec rango generate src/urls/blog.tsx
|
|
396
|
+
# or generate everything under a directory:
|
|
397
|
+
pnpm exec rango generate src/urls --static
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Don't edit the file by hand — re-run codegen when patterns change.
|
|
401
|
+
|
|
402
|
+
**Today the Vite plugin only regenerates the router-level `*.named-routes.gen.ts`.** Per-module `urls/*.gen.ts` files are emitted only by the CLI (or `writePerModuleRouteTypesForFile` programmatically). Commit the generated files and re-run `rango generate` whenever a `urls()` module's `path()`/`include()` shape changes. A common workflow is to wire it into a `predev` script:
|
|
403
|
+
|
|
404
|
+
```jsonc
|
|
405
|
+
// package.json
|
|
406
|
+
{
|
|
407
|
+
"scripts": {
|
|
408
|
+
"predev": "rango generate src",
|
|
409
|
+
"dev": "vite",
|
|
410
|
+
},
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## When to use what
|
|
269
415
|
|
|
270
|
-
|
|
416
|
+
| Context | API | Resolves | Use for |
|
|
417
|
+
| ---------------- | -------------------------------------------------- | ----------------------------------------- | ---------------------------------------------------------------- |
|
|
418
|
+
| Server handler | `ctx.reverse("name")` | Named routes (local + absolute) | **Default** server-side URL generation |
|
|
419
|
+
| Server handler | `scopedReverse<T>(ctx.reverse)` | Same, with type safety | Type-safe server URLs |
|
|
420
|
+
| Client component | `useReverse(routes)` | Local names from an imported `routes` map | Typed in-module URL generation without round-tripping the server |
|
|
421
|
+
| Client component | (URL passed as prop / loader data / action return) | Named routes | Cross-module URLs or one-off names you don't want to import |
|
|
422
|
+
| Client component | `href("/path")` | Absolute paths (static strings) | Static navigation where no named-route lookup is needed |
|
|
423
|
+
| Client component | `useHref()` | Mount-prefixed paths | Local navigation inside `include()` |
|
|
424
|
+
| Client component | `useMount()` | Raw mount path | Custom mount-aware logic |
|
|
425
|
+
|
|
426
|
+
> `ctx.reverse()` is server-only. On the client, either generate URLs on the server and pass them in, or import the `routes` map and use `useReverse(routes)` for in-module names.
|
|
271
427
|
|
|
272
428
|
## Complete example: mounted module
|
|
273
429
|
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -91,6 +91,20 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
91
91
|
]);
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
> **Client refresh `key` vs. server `cache({ key })` vs. `revalidate()`.** Three
|
|
95
|
+
> different "what refreshes" knobs that are easy to confuse:
|
|
96
|
+
>
|
|
97
|
+
> - `useLoader(Loader, { key })` / `useFetchLoader(Loader, { key })` — a
|
|
98
|
+
> **client** refresh identity. It groups which mounted reads of one loader
|
|
99
|
+
> refresh together when one calls `load()`. It never touches the server
|
|
100
|
+
> request. For refreshing **different** loaders together, tag them with
|
|
101
|
+
> `{ refreshGroup }` (one name or several) and call `useRefreshLoaders()(name)`
|
|
102
|
+
> (plain GET only). See the hooks skill ("Scoping refetch with a `key`" and
|
|
103
|
+
> "Refreshing multiple loaders together").
|
|
104
|
+
> - `cache({ key })` — a **server** cache identity (storage hit/miss/ttl/swr).
|
|
105
|
+
> - `revalidate()` — which **server** segments/loaders recompute during
|
|
106
|
+
> navigation and action refreshes.
|
|
107
|
+
|
|
94
108
|
DSL loaders are the **live data layer** — they resolve fresh on every
|
|
95
109
|
request, even when the route is inside a `cache()` boundary. The router
|
|
96
110
|
excludes them from the segment cache at storage time and re-resolves them
|
|
@@ -185,7 +199,7 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
185
199
|
// Request headers
|
|
186
200
|
const auth = ctx.request.headers.get("Authorization");
|
|
187
201
|
|
|
188
|
-
// Variables set by middleware (from
|
|
202
|
+
// Variables set by middleware (from Rango.Vars augmentation)
|
|
189
203
|
const user = ctx.get("user");
|
|
190
204
|
|
|
191
205
|
// Type-checked URLs for payloads. `.name` resolves within the current
|
|
@@ -244,15 +258,23 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
244
258
|
revalidate(() => false), // Never revalidate
|
|
245
259
|
]),
|
|
246
260
|
|
|
247
|
-
// Loader that revalidates after cart actions
|
|
261
|
+
// Loader that revalidates after cart actions (defer otherwise — keeps the
|
|
262
|
+
// permissive loader defaults for navigation and other actions intact)
|
|
248
263
|
loader(CartLoader, () => [
|
|
249
|
-
revalidate(({ actionId }) => actionId?.includes("Cart")
|
|
264
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
|
|
250
265
|
]),
|
|
251
266
|
]);
|
|
252
267
|
```
|
|
253
268
|
|
|
254
269
|
### `revalidate()` return shapes
|
|
255
270
|
|
|
271
|
+
> **Scope: `revalidate()` is a partial-render concern, not a cache concern.**
|
|
272
|
+
> It decides whether a segment (here, a loader) re-runs and streams to the
|
|
273
|
+
> client on a navigation or action — never whether a cached value is stale. The
|
|
274
|
+
> cache decides hit/miss/ttl/swr independently and never reads `revalidate()`.
|
|
275
|
+
> Caching a loader is a separate, opt-in step (`loader(Fn, () => [cache({...})])`).
|
|
276
|
+
> See `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
|
|
277
|
+
|
|
256
278
|
A `revalidate(fn)` callback can return one of four shapes. The chain
|
|
257
279
|
processes revalidators in order; each call's return controls how the
|
|
258
280
|
chain continues:
|
|
@@ -282,6 +304,58 @@ revalidate(() => null); // explicit defer
|
|
|
282
304
|
If every revalidator on a segment defers, the segment-type default
|
|
283
305
|
(e.g. params-changed for routes, `false` for parallels) is used.
|
|
284
306
|
|
|
307
|
+
#### `|| undefined` (defer) vs `?? false` (hard) — pick deliberately
|
|
308
|
+
|
|
309
|
+
A boolean return — including `false` — is a **hard** decision: it short-circuits
|
|
310
|
+
the chain and overrides the segment default. `undefined` **defers** to the
|
|
311
|
+
running suggestion / segment default. They are not interchangeable:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
// Defer: "revalidate on match, otherwise let the default/downstream decide."
|
|
315
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") || undefined);
|
|
316
|
+
|
|
317
|
+
// Hard: "revalidate ONLY on match, suppress everything else."
|
|
318
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
This matters most for loaders, whose defaults are permissive: a loader defaults
|
|
322
|
+
to revalidating on **any** action (`POST`) and on **param/search changes**
|
|
323
|
+
during navigation. So `?? false` on a loader silently suppresses both — the
|
|
324
|
+
loader will not refetch when you navigate to a different `:id`. Use
|
|
325
|
+
`|| undefined` when you want to _add_ a revalidation signal on top of the
|
|
326
|
+
sensible defaults, and reserve `?? false` for the rare case where you genuinely
|
|
327
|
+
want the loader to refetch on nothing but your matched action.
|
|
328
|
+
|
|
329
|
+
When **composing multiple revalidators** on one segment (see below), defer is
|
|
330
|
+
mandatory: the first hard `?? false` ends the chain and the later contracts
|
|
331
|
+
never run.
|
|
332
|
+
|
|
333
|
+
#### Matching actions: `ctx.isAction()`
|
|
334
|
+
|
|
335
|
+
To revalidate after specific server actions, match them by **reference** with
|
|
336
|
+
`ctx.isAction()` rather than hand-written `actionId` substrings. A rename or
|
|
337
|
+
moved file then becomes a type error instead of silently failing to match:
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import { addToCart, removeFromCart } from "../actions/cart";
|
|
341
|
+
import * as CartActions from "../actions/cart";
|
|
342
|
+
|
|
343
|
+
loader(CartLoader, () => [
|
|
344
|
+
revalidate((ctx) => ctx.isAction(addToCart) || undefined), // one action
|
|
345
|
+
]);
|
|
346
|
+
revalidate((ctx) => ctx.isAction(addToCart, removeFromCart) || undefined); // several
|
|
347
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined); // any action in the module
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`isAction()` is a method on the revalidate predicate's **context argument** —
|
|
351
|
+
there is no standalone `isAction` import; you always reach it through the callback
|
|
352
|
+
parameter (`revalidate((ctx) => ctx.isAction(...))`). It returns a raw boolean, so
|
|
353
|
+
pair it with `|| undefined` for the usual "revalidate on match, else defer"
|
|
354
|
+
intent. It returns `false` on plain navigation and on non-matches, and resolves
|
|
355
|
+
the reference the same way the router derives `actionId` (`$id` in production,
|
|
356
|
+
`$$id` in dev), so it matches in both modes. The raw `actionId` string stays
|
|
357
|
+
available on the same context as an escape hatch.
|
|
358
|
+
|
|
285
359
|
### Revalidation Contracts for Loader Dependencies
|
|
286
360
|
|
|
287
361
|
If a loader reads `ctx.get()` data produced by an outer handler/layout, share
|
|
@@ -289,8 +363,12 @@ the same named revalidation contract across producer and consumer segments.
|
|
|
289
363
|
|
|
290
364
|
```typescript
|
|
291
365
|
// revalidation-contracts.ts
|
|
292
|
-
|
|
293
|
-
|
|
366
|
+
import * as AccountActions from "./actions/account";
|
|
367
|
+
|
|
368
|
+
// Match by reference with ctx.isAction() (rename-safe), and defer (|| undefined)
|
|
369
|
+
// so these contracts compose — a hard `false` would short-circuit the rest.
|
|
370
|
+
export const revalidateAccountScope = (ctx) =>
|
|
371
|
+
ctx.isAction(AccountActions) || undefined;
|
|
294
372
|
|
|
295
373
|
layout(AccountLayout, () => [
|
|
296
374
|
revalidate(revalidateAccountScope), // producer reruns
|
|
@@ -333,6 +411,64 @@ follows the same rule: at build time, loaders are skipped entirely (there is no
|
|
|
333
411
|
real request context), and at runtime the worker resolves them fresh against
|
|
334
412
|
the live database.
|
|
335
413
|
|
|
414
|
+
### Parallel and streaming — latency overlaps first paint
|
|
415
|
+
|
|
416
|
+
Loaders do not block the page. As the render pass begins — the pass that route
|
|
417
|
+
middleware wraps, so loaders run right after middleware, not in a later
|
|
418
|
+
phase — every matched loader is kicked off **concurrently** (their promises start in the
|
|
419
|
+
same tick), and each result is **streamed** to the client as its own RSC Flight
|
|
420
|
+
chunk rather than awaited up front. Pair a loader with `loading()` (or a
|
|
421
|
+
client `<Suspense>`) and the shell paints immediately while the data streams in.
|
|
422
|
+
|
|
423
|
+
This is why **"cached UI still pays full data latency" is the wrong intuition**:
|
|
424
|
+
on a `cache()` hit the UI segments stream instantly from cache while the live
|
|
425
|
+
loaders resolve fresh **in parallel** — data latency _overlaps_ first paint
|
|
426
|
+
instead of being added on top of it. (Without a `loading()` / `<Suspense>`
|
|
427
|
+
boundary a parallel loader blocks its parent, so add one to keep the overlap.)
|
|
428
|
+
|
|
429
|
+
If you come from a framework where the loader is a blocking step that runs
|
|
430
|
+
before the response is built, this is the shift to internalize: here the
|
|
431
|
+
response starts streaming first and loader data fills in.
|
|
432
|
+
|
|
433
|
+
### See it: `debugPerformance`
|
|
434
|
+
|
|
435
|
+
Turn on the per-request performance timeline early — it is the fastest way to
|
|
436
|
+
confirm loaders overlap rather than serialize, and to find the real bottleneck
|
|
437
|
+
locally instead of guessing:
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
const router = createRouter({ document: Document, debugPerformance: true });
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Or enable it per-request from middleware (e.g. only when `?debug` is present) by
|
|
444
|
+
calling `ctx.debugPerformance()` **before** `await next()`. Each HTML request
|
|
445
|
+
then prints a shared-axis waterfall (and emits a `Server-Timing` header):
|
|
446
|
+
|
|
447
|
+
```
|
|
448
|
+
[RSC Perf] GET /product/widget (24.53ms)
|
|
449
|
+
start dur span timeline
|
|
450
|
+
0.08ms 3.20ms route-matching |#####...................................|
|
|
451
|
+
3.40ms 8.70ms ssr-render-html |.....##############.....................|
|
|
452
|
+
3.42ms 11.90ms loader:…#ProductLoader |.....###################................|
|
|
453
|
+
3.45ms 11.40ms loader:…#ReviewsLoader |.....##################.................|
|
|
454
|
+
0.00ms 24.53ms handler:total |########################################|
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
How to read it:
|
|
458
|
+
|
|
459
|
+
- **Humans:** scan the `#` bars on the shared axis. Bars that start at the same
|
|
460
|
+
offset and run side by side are executing **in parallel** — loaders should
|
|
461
|
+
overlap `ssr-render-html` / `render:total`, not sit alone to the right of
|
|
462
|
+
everything. A lone `loader:*` bar past the render bar is serialized latency to
|
|
463
|
+
chase. `handler:total` is the whole request; `render:total` is the render pass.
|
|
464
|
+
- **LLMs / programmatic:** read each row as `{ start, dur, label }`. A loader
|
|
465
|
+
overlaps paint when its `[start, start+dur]` interval intersects
|
|
466
|
+
`render:total` / `ssr-render-html`. Flag a regression when a `loader:*`
|
|
467
|
+
interval is **disjoint from and starts after** `render:total`, or when its
|
|
468
|
+
`dur` approaches `handler:total` — that loader is on the critical path instead
|
|
469
|
+
of overlapping it. Two `loader:*` rows with near-equal `start` confirm
|
|
470
|
+
parallel execution.
|
|
471
|
+
|
|
336
472
|
### Opting a Loader into Caching
|
|
337
473
|
|
|
338
474
|
To cache a specific loader's data, attach a `cache()` child:
|
|
@@ -606,6 +742,13 @@ export const FileUploadLoader = createLoader(async (ctx) => {
|
|
|
606
742
|
|
|
607
743
|
Client usage — see `/hooks useFetchLoader` for the full client-side pattern.
|
|
608
744
|
|
|
745
|
+
> **Refetch sharing**: when the loader is registered on the route via
|
|
746
|
+
> `loader()`, a plain `load()` call (no `params`, no `body`) broadcasts
|
|
747
|
+
> the new value to every component reading the same loader id —
|
|
748
|
+
> `useLoader` reads in layouts, pages, and parallel slots all converge.
|
|
749
|
+
> Calls with `params` or a non-GET method stay local to the call site.
|
|
750
|
+
> See `/hooks` → "Shared refetch behavior" for the full contract.
|
|
751
|
+
|
|
609
752
|
## Complete Example
|
|
610
753
|
|
|
611
754
|
```typescript
|
|
@@ -641,7 +784,7 @@ export const CartLoader = createLoader(async (ctx) => {
|
|
|
641
784
|
export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
|
|
642
785
|
layout(<ShopLayout />, () => [
|
|
643
786
|
loader(CartLoader, () => [
|
|
644
|
-
revalidate(({ actionId }) => actionId?.includes("Cart")
|
|
787
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
|
|
645
788
|
]),
|
|
646
789
|
|
|
647
790
|
path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
|
|
@@ -10,9 +10,6 @@ Middleware runs before/after route handlers using the onion model.
|
|
|
10
10
|
|
|
11
11
|
## Execution Model
|
|
12
12
|
|
|
13
|
-
Canonical semantics reference:
|
|
14
|
-
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
15
|
-
|
|
16
13
|
There are two levels of middleware with different execution scopes:
|
|
17
14
|
|
|
18
15
|
### Global middleware (`router.use()`)
|
|
@@ -36,15 +33,22 @@ Registered inside `urls()` callback. Wraps **rendering only** -- it does NOT wra
|
|
|
36
33
|
|
|
37
34
|
```
|
|
38
35
|
Request flow (with action):
|
|
39
|
-
global mw -> action executes -> route mw ->
|
|
36
|
+
global mw -> action executes -> route mw -> render pass
|
|
40
37
|
|
|
41
38
|
Request flow (no action):
|
|
42
|
-
global mw -> route mw ->
|
|
39
|
+
global mw -> route mw -> render pass
|
|
43
40
|
|
|
44
41
|
Progressive enhancement (no-JS form POST):
|
|
45
42
|
global mw -> action executes -> route mw -> full page re-render
|
|
46
43
|
```
|
|
47
44
|
|
|
45
|
+
The **render pass** resolves handler, layouts, parallels, and loaders together —
|
|
46
|
+
it is not a handler-then-loaders sequence. Handler-first ordering is guaranteed
|
|
47
|
+
only between a route handler and its child/orphan layouts and parallels (so
|
|
48
|
+
`ctx.set` is visible); loaders run **concurrently** and stream their results, so
|
|
49
|
+
their latency overlaps rendering rather than blocking it. See `/loader` →
|
|
50
|
+
"Parallel and streaming".
|
|
51
|
+
|
|
48
52
|
The contract is: **route middleware wraps rendering regardless of transport** (JS-enabled RSC stream or no-JS HTML). During PE re-render, route middleware observes action-set state (cookies, context variables) the same way it does during JS-enabled post-action revalidation.
|
|
49
53
|
|
|
50
54
|
Revalidation is still partial. Route middleware wraps the render pass that
|
|
@@ -64,7 +68,7 @@ and consumer segments, even when middleware is present in the chain.
|
|
|
64
68
|
|
|
65
69
|
```typescript
|
|
66
70
|
export const revalidateCartData = ({ actionId }) =>
|
|
67
|
-
actionId?.includes("src/actions/cart.ts#")
|
|
71
|
+
actionId?.includes("src/actions/cart.ts#") || undefined;
|
|
68
72
|
|
|
69
73
|
layout(CartLayout, () => [
|
|
70
74
|
middleware(cartRenderMiddleware),
|
|
@@ -192,7 +196,7 @@ export const myMiddleware: Middleware = async (ctx, next) => {
|
|
|
192
196
|
ctx.env.DB; // D1Database
|
|
193
197
|
ctx.env.KV; // KVNamespace
|
|
194
198
|
|
|
195
|
-
// Set variables for downstream handlers (typed via
|
|
199
|
+
// Set variables for downstream handlers (typed via Rango.Vars)
|
|
196
200
|
ctx.set("user", { id: "123", name: "John" });
|
|
197
201
|
|
|
198
202
|
// Continue to next middleware/handler
|
|
@@ -233,8 +237,8 @@ const Dashboard: Handler<"dashboard"> = (ctx) => {
|
|
|
233
237
|
```
|
|
234
238
|
|
|
235
239
|
This works alongside `ctx.get("key")` / `ctx.set("key", value)` (global typing
|
|
236
|
-
via
|
|
237
|
-
data; use
|
|
240
|
+
via Rango.Vars augmentation). Use `createVar` for route-local or feature-scoped
|
|
241
|
+
data; use Rango.Vars for app-wide middleware state.
|
|
238
242
|
|
|
239
243
|
## Redirect with State in Middleware
|
|
240
244
|
|
|
@@ -302,7 +302,7 @@ re-rendering. This is about the segment tree, not cache invalidation:
|
|
|
302
302
|
```typescript
|
|
303
303
|
// Re-run this layout when a blog action fires
|
|
304
304
|
layout(BlogLayout, () => [
|
|
305
|
-
revalidate(({ actionId }) => actionId?.includes("updateBlog")
|
|
305
|
+
revalidate(({ actionId }) => actionId?.includes("updateBlog") || undefined),
|
|
306
306
|
path("/blog/:slug", BlogPost, { name: "blogPost" }),
|
|
307
307
|
]);
|
|
308
308
|
|
|
@@ -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.
|