@rangojs/router 0.0.0-experimental.105 → 0.0.0-experimental.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +207 -28
- package/skills/caching/SKILL.md +1 -1
- package/skills/composability/SKILL.md +27 -2
- package/skills/intercept/SKILL.md +1 -4
- package/skills/layout/SKILL.md +4 -7
- package/skills/loader/SKILL.md +127 -5
- package/skills/middleware/SKILL.md +10 -6
- package/skills/migrate-nextjs/SKILL.md +1 -1
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +3 -6
- package/skills/prerender/SKILL.md +1 -20
- package/skills/rango/SKILL.md +201 -27
- package/skills/route/SKILL.md +9 -4
- package/skills/server-actions/SKILL.md +53 -41
- package/skills/typesafety/SKILL.md +42 -0
- package/src/context-var.ts +5 -5
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +1 -0
- package/src/route-definition/helpers-types.ts +4 -2
- package/src/router/revalidation.ts +43 -1
- package/src/types/handler-context.ts +48 -6
- package/src/types/index.ts +1 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: observability
|
|
3
|
+
description: Debug Rango request performance with debugPerformance, Server-Timing, structured telemetry, and tracing
|
|
4
|
+
argument-hint:
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Observability
|
|
8
|
+
|
|
9
|
+
Use this when you need to understand request latency, cache decisions,
|
|
10
|
+
revalidation behavior, loader overlap, or production traces.
|
|
11
|
+
|
|
12
|
+
Rango exposes two complementary observability surfaces:
|
|
13
|
+
|
|
14
|
+
1. **Performance timeline** (`debugPerformance`) — per-request waterfall for
|
|
15
|
+
local or targeted debugging. It prints to the console and emits
|
|
16
|
+
`Server-Timing`.
|
|
17
|
+
2. **Structured telemetry** (`telemetry`) — lifecycle events sent to a pluggable
|
|
18
|
+
sink for production monitoring, OpenTelemetry, or custom metrics.
|
|
19
|
+
|
|
20
|
+
The essentials are below. The exported `TelemetryEvent` union type
|
|
21
|
+
(`import type { TelemetryEvent } from "@rangojs/router"`) is the full event
|
|
22
|
+
contract — every event kind and its fields are typed there.
|
|
23
|
+
|
|
24
|
+
## Performance timeline
|
|
25
|
+
|
|
26
|
+
Enable globally while debugging:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { createRouter } from "@rangojs/router";
|
|
30
|
+
|
|
31
|
+
const router = createRouter({
|
|
32
|
+
document: Document,
|
|
33
|
+
urls: urlpatterns,
|
|
34
|
+
debugPerformance: true,
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or enable for selected requests from middleware:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
middleware(async (ctx, next) => {
|
|
42
|
+
if (ctx.url.searchParams.has("debug")) {
|
|
43
|
+
ctx.debugPerformance();
|
|
44
|
+
}
|
|
45
|
+
await next();
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Call `ctx.debugPerformance()` before `await next()`. The request then prints a
|
|
50
|
+
shared-axis waterfall and adds a `Server-Timing` header.
|
|
51
|
+
|
|
52
|
+
Read the timeline as intervals:
|
|
53
|
+
|
|
54
|
+
- `handler:total` is the whole router request.
|
|
55
|
+
- `render:total` / `ssr-render-html` show the render pass.
|
|
56
|
+
- `loader:*` rows should overlap render work. If a loader starts only after the
|
|
57
|
+
render bar, it is serialized latency.
|
|
58
|
+
- Cache, route matching, middleware pre/post, RSC serialization, and SSR phases
|
|
59
|
+
appear as separate spans, so the slow phase is visible without guessing.
|
|
60
|
+
|
|
61
|
+
## Structured telemetry
|
|
62
|
+
|
|
63
|
+
Use telemetry when you want durable production events rather than a one-request
|
|
64
|
+
debug waterfall.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { createRouter, createConsoleSink } from "@rangojs/router";
|
|
68
|
+
|
|
69
|
+
const router = createRouter({
|
|
70
|
+
document: Document,
|
|
71
|
+
urls: urlpatterns,
|
|
72
|
+
telemetry: createConsoleSink(),
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
For OpenTelemetry:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { createRouter, createOTelSink } from "@rangojs/router";
|
|
80
|
+
import { trace } from "@opentelemetry/api";
|
|
81
|
+
|
|
82
|
+
const router = createRouter({
|
|
83
|
+
document: Document,
|
|
84
|
+
urls: urlpatterns,
|
|
85
|
+
telemetry: createOTelSink(trace.getTracer("my-app")),
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Custom sinks implement `emit(event)`:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { createRouter } from "@rangojs/router";
|
|
93
|
+
|
|
94
|
+
const router = createRouter({
|
|
95
|
+
document: Document,
|
|
96
|
+
urls: urlpatterns,
|
|
97
|
+
telemetry: {
|
|
98
|
+
emit(event) {
|
|
99
|
+
myMetrics.record(event);
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Events include `request.start/end/error`, `loader.start/end/error`,
|
|
106
|
+
`handler.error`, `cache.decision`, and `revalidation.decision`.
|
|
107
|
+
|
|
108
|
+
## Debugging revalidation and stale data
|
|
109
|
+
|
|
110
|
+
When stale UI or unexpected partial renders are the question, use all three
|
|
111
|
+
layers together:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { createConsoleSink, createRouter } from "@rangojs/router";
|
|
115
|
+
|
|
116
|
+
const router = createRouter({
|
|
117
|
+
document: Document,
|
|
118
|
+
urls: urlpatterns,
|
|
119
|
+
debugPerformance: true,
|
|
120
|
+
telemetry: createConsoleSink(),
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Then inspect:
|
|
125
|
+
|
|
126
|
+
- `revalidation.decision` telemetry to see which segment re-ran or skipped.
|
|
127
|
+
- cache spans / `cache.decision` events to see hit, miss, stale, and background
|
|
128
|
+
revalidation behavior.
|
|
129
|
+
- loader spans to confirm live loaders overlap the render rather than blocking
|
|
130
|
+
first paint.
|
|
131
|
+
- the `Server-Timing` header to compare local logs with browser-network timing.
|
|
132
|
+
|
|
133
|
+
## Zero-overhead defaults
|
|
134
|
+
|
|
135
|
+
`debugPerformance` is off by default, and `telemetry` emits nothing unless a sink
|
|
136
|
+
is configured. Per-request `ctx.debugPerformance()` lets you turn on the
|
|
137
|
+
waterfall only for the route, user, or query param you are investigating.
|
package/skills/parallel/SKILL.md
CHANGED
|
@@ -8,9 +8,6 @@ argument-hint: [@slot-name]
|
|
|
8
8
|
|
|
9
9
|
Parallel routes render multiple components simultaneously in named slots.
|
|
10
10
|
|
|
11
|
-
Canonical semantics reference:
|
|
12
|
-
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
13
|
-
|
|
14
11
|
## Basic Parallel Routes
|
|
15
12
|
|
|
16
13
|
```typescript
|
|
@@ -340,7 +337,7 @@ parallel(
|
|
|
340
337
|
() => [
|
|
341
338
|
loader(CartLoader),
|
|
342
339
|
// Revalidate when cart actions occur
|
|
343
|
-
revalidate(({ actionId }) => actionId?.includes("Cart")
|
|
340
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
|
|
344
341
|
]
|
|
345
342
|
)
|
|
346
343
|
```
|
|
@@ -364,7 +361,7 @@ the parallel consumer:
|
|
|
364
361
|
```typescript
|
|
365
362
|
// revalidation-contracts.ts
|
|
366
363
|
export const revalidateCartData = ({ actionId }) =>
|
|
367
|
-
actionId?.includes("src/actions/cart.ts#")
|
|
364
|
+
actionId?.includes("src/actions/cart.ts#") || undefined;
|
|
368
365
|
|
|
369
366
|
layout(CartLayout, () => [
|
|
370
367
|
revalidate(revalidateCartData), // producer reruns
|
|
@@ -482,7 +479,7 @@ export const shopPatterns = urls(({
|
|
|
482
479
|
() => [
|
|
483
480
|
loader(CartLoader),
|
|
484
481
|
loading(<CartSkeleton />),
|
|
485
|
-
revalidate(({ actionId }) => actionId?.includes("Cart")
|
|
482
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
|
|
486
483
|
]
|
|
487
484
|
),
|
|
488
485
|
|
|
@@ -11,9 +11,6 @@ deserialization path, same segment system. The worker handles every request --
|
|
|
11
11
|
there are NO static .html or .rsc files served from assets. The worker reads
|
|
12
12
|
pre-computed Flight payloads instead of executing handler code.
|
|
13
13
|
|
|
14
|
-
Canonical semantics reference:
|
|
15
|
-
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
16
|
-
|
|
17
14
|
## API: Prerender
|
|
18
15
|
|
|
19
16
|
### Static Route (no params)
|
|
@@ -640,16 +637,7 @@ At runtime, the cache-lookup middleware uses these flags:
|
|
|
640
637
|
|
|
641
638
|
## Contributor Checklist
|
|
642
639
|
|
|
643
|
-
Before changing prerender behavior,
|
|
644
|
-
|
|
645
|
-
### Docs to re-read
|
|
646
|
-
|
|
647
|
-
- [Prerender API design](../../docs/prerender-api-design.md) -- canonical
|
|
648
|
-
architecture: build-time flow, runtime flow, storage, Passthrough, intercept
|
|
649
|
-
- [Execution model](../../docs/internal/execution-model.md) -- handler-first
|
|
650
|
-
ordering, middleware scope, context visibility rules
|
|
651
|
-
- [Semantic change checklist](../../docs/internal/semantic-change-checklist.md)
|
|
652
|
-
-- gate for any change to execution semantics
|
|
640
|
+
Before changing prerender behavior, run these tests.
|
|
653
641
|
|
|
654
642
|
### Tests to run
|
|
655
643
|
|
|
@@ -676,10 +664,3 @@ pnpm --filter @rangojs/router exec playwright test handler-first
|
|
|
676
664
|
dev/build-only and do not need a production counterpart.
|
|
677
665
|
- Behavioral assertions (rendered content, loader freshness, Passthrough
|
|
678
666
|
fallback, intercept variant selection) must work in the production build.
|
|
679
|
-
|
|
680
|
-
## Maintenance References
|
|
681
|
-
|
|
682
|
-
- [Stability next steps plan](../../docs/internal/stability-next-steps-plan.md)
|
|
683
|
-
-- completed parity and cleanup pass (reference for decisions made)
|
|
684
|
-
- [Test quality baseline](../../docs/internal/test-quality-baseline.md) --
|
|
685
|
-
measured test inventory, sleep debt, production coverage gaps
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -8,35 +8,209 @@ argument-hint:
|
|
|
8
8
|
|
|
9
9
|
Django-inspired RSC router with composable URL patterns, type-safe href, and server components.
|
|
10
10
|
|
|
11
|
+
This page is the mental model to read **before** the catalog. A flat list of
|
|
12
|
+
skills gives nothing to slot details into, so a reader free-associates from local
|
|
13
|
+
vocabulary — which is exactly how `revalidate()` gets misread as caching. Start
|
|
14
|
+
with the shape, then pick a primitive.
|
|
15
|
+
|
|
16
|
+
## The shape of rango (read first)
|
|
17
|
+
|
|
18
|
+
- **Routes are expressed, not configured.** The `urls()` tree shows where every
|
|
19
|
+
route, layout, loader, and cache lives. No file-system convention, no hunting.
|
|
20
|
+
- **Two freshness axes, orthogonal:**
|
|
21
|
+
- _stored-value freshness_ — `"use cache"`, `cache()`, loader `cache()`
|
|
22
|
+
(SWR is first-class where the store supports it; `"use cache"` ships a
|
|
23
|
+
default SWR window; see `/cache-guide`)
|
|
24
|
+
- _client-update selection_ — `revalidate()`
|
|
25
|
+
- **Loaders are the live data layer** — fresh every request by default, even
|
|
26
|
+
inside a cached render. They run **in parallel** right after middleware and
|
|
27
|
+
**stream**, so data latency overlaps first paint instead of blocking it (a
|
|
28
|
+
cache hit streams UI instantly while loaders resolve fresh alongside). Opt into
|
|
29
|
+
caching explicitly. See `/loader` → "Parallel and streaming".
|
|
30
|
+
- **One identity, one store** — loaders, handles, cached fns, and actions are all
|
|
31
|
+
`path#export`; all caches share one store; `revalidateTag` cuts across them.
|
|
32
|
+
- **Type-safe end to end** — route names, params, search schemas, loader return
|
|
33
|
+
types, context vars, and `href` / `reverse` are checked at compile time
|
|
34
|
+
(`/typesafety`).
|
|
35
|
+
- **See where time goes** — turn on `debugPerformance` early (router option, or
|
|
36
|
+
`ctx.debugPerformance()` in middleware for per-request opt-in). It prints a
|
|
37
|
+
per-request waterfall + `Server-Timing` header; loaders should overlap the
|
|
38
|
+
render bar, not serialize after it. For production, wire `telemetry` to a
|
|
39
|
+
console, OpenTelemetry, or custom sink. See `/observability`.
|
|
40
|
+
|
|
41
|
+
Most features are **just-in-time**: the core is `urls()`, `path()`, `layout()`,
|
|
42
|
+
`include()`, and `reverse()`. Caching, parallel routes, intercepts, prerender,
|
|
43
|
+
i18n, themes, and the rest are opt-in — reach for them when a requirement
|
|
44
|
+
appears, not up front.
|
|
45
|
+
|
|
46
|
+
## Composability: structure vs config
|
|
47
|
+
|
|
48
|
+
- `path()` / `include()` are **structure** — they define URLs and must stay
|
|
49
|
+
visible in `urls()`. They cannot be hidden in a factory. `include()` composes
|
|
50
|
+
whole modules (separation of real concerns); `path()` places a leaf.
|
|
51
|
+
- Everything else — `cache`, `loader`, `loading`, `middleware`, `revalidate`,
|
|
52
|
+
`parallel`, `intercept`, `errorBoundary`, … — is **config**. It attaches to a
|
|
53
|
+
node via its `use` callback, is importable, and extracts into factories that
|
|
54
|
+
return arrays (`withAuth()`, `withCaching()`), flattened automatically.
|
|
55
|
+
|
|
56
|
+
To decide where something can live: **does it define a URL? structure, stays in
|
|
57
|
+
`urls()`. Does it modify a node? config, compose freely.**
|
|
58
|
+
|
|
59
|
+
## Pick a primitive
|
|
60
|
+
|
|
61
|
+
| I need to… | Use | Skill |
|
|
62
|
+
| ------------------------------------- | -------------------------------- | ----------------------- |
|
|
63
|
+
| render data fresh every request | `loader()` + `useLoader()` | /loader |
|
|
64
|
+
| cache a rendered subtree | `cache()` on a segment | /caching |
|
|
65
|
+
| cache one function/component's result | `"use cache"` | /use-cache |
|
|
66
|
+
| cache a loader's data | `loader(L, () => [cache()])` | /loader, /caching |
|
|
67
|
+
| re-render a segment after an action | `revalidate()` | /loader |
|
|
68
|
+
| mutate | `"use server"` action | /server-actions |
|
|
69
|
+
| debug a slow request | `debugPerformance` / telemetry | /observability |
|
|
70
|
+
| share config across routes | factory returning a helper array | /composability |
|
|
71
|
+
| compose a sub-app / module | `include()` | /route |
|
|
72
|
+
| modal / soft navigation | `intercept()` | /intercept |
|
|
73
|
+
| pre-render a route at build time | `Prerender(...)` wrapper | /prerender |
|
|
74
|
+
| stream SSE / upgrade a WebSocket | `path.stream()` / `path.any()` | /streams-and-websockets |
|
|
75
|
+
|
|
76
|
+
## Invariants
|
|
77
|
+
|
|
78
|
+
- `path()`/`include()` are always visible in `urls()`; config helpers are extractable.
|
|
79
|
+
- **Cache decides freshness; `revalidate()` decides client-update.** Orthogonal; compose.
|
|
80
|
+
- Loaders resolve fresh every request (even inside `cache()`) and never run twice/request.
|
|
81
|
+
- Inside `"use cache"`: `cookies()`/`headers()` and `ctx` side-effects
|
|
82
|
+
(`set`/`header`/`setTheme`/`onResponse`/`setLocationState`) throw; `ctx.use(Handle)`
|
|
83
|
+
is captured on miss and replayed on hit. (The non-cacheable read guard is a
|
|
84
|
+
separate `cache()`-boundary check — see the correctness bullet below.)
|
|
85
|
+
- One identity `path#export` (`functionId`/`$$id`/`actionId`); one store;
|
|
86
|
+
`revalidateTag` cuts across all cache mechanisms.
|
|
87
|
+
- `useLoader` / `useHandle` / `useFetchLoader` are client-only.
|
|
88
|
+
- Caches are correctness-first: persistent store keys are version-segmented (no
|
|
89
|
+
cross-deploy drift), the forward/back cache is mutation-aware, and
|
|
90
|
+
`createVar({ cache: false })` throws on a **direct** read inside a `cache()`
|
|
91
|
+
boundary (a deliberately non-propagating guard). See `/cache-guide` →
|
|
92
|
+
"Correctness & invalidation".
|
|
93
|
+
- Nested caches: the outer cache window bounds the inner — an inner shorter TTL
|
|
94
|
+
only applies when the enclosing cache recomputes; put a value in a loader if it
|
|
95
|
+
must be fresher. See `/cache-guide` → "Combining Both".
|
|
96
|
+
|
|
97
|
+
## Don't confuse
|
|
98
|
+
|
|
99
|
+
- `revalidate()` ≠ cache invalidation — partial-render selection vs value freshness.
|
|
100
|
+
- host router `.lazy()` (lazy import of a handler/sub-app) vs `.map()` (inline Response).
|
|
101
|
+
- `cache()` (segment, in the DSL) vs `"use cache"` (function/component directive).
|
|
102
|
+
- `loader()` registration (server) vs `useLoader()` consumption (client).
|
|
103
|
+
|
|
104
|
+
### Coming from another framework (false friends)
|
|
105
|
+
|
|
106
|
+
Same words, different jobs — this is the most common source of the
|
|
107
|
+
`revalidate()`-is-caching misread.
|
|
108
|
+
|
|
109
|
+
| You may know | Maps to Rango axis | Watch out |
|
|
110
|
+
| ------------------------------------------ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
111
|
+
| Next.js `export const revalidate = N` | **Axis 1** (cache) | Same word, opposite meaning. Next's `revalidate` is time-based cache expiry; Rango's `revalidate()` is **axis 2**. Use `cache({ ttl })` for the Next behavior. |
|
|
112
|
+
| Next.js `revalidatePath` / `revalidateTag` | **Axis 1** (cache) | Cache busting. Rango's tag bust is `revalidateTag`; there is no `revalidatePath`. |
|
|
113
|
+
| React Router / Remix `shouldRevalidate` | **Axis 2** | This is the correct mental model for Rango's `revalidate()`. |
|
|
114
|
+
| HTTP `Cache-Control` / ISR | **Axis 1** | Edge/document layer — see `/document-cache`. Separate from both `cache()` and `revalidate()`. |
|
|
115
|
+
| Remix/RR `loader` | live data | Like Rango loaders, fresh per request — but Rango loaders run in parallel and stream (latency overlaps first paint), and can opt into caching on demand. |
|
|
116
|
+
|
|
117
|
+
See `/cache-guide` for the axis-1 decision guide, `/loader` and `/route` for
|
|
118
|
+
`revalidate()` (axis 2), and `/document-cache` for the edge layer.
|
|
119
|
+
|
|
120
|
+
## Canonical shape
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
|
|
124
|
+
layout(<ShopLayout />, () => [ // structure: wraps children
|
|
125
|
+
loader(CartLoader, () => [ // config: live data
|
|
126
|
+
// partial-render axis: re-run on cart actions, defer otherwise.
|
|
127
|
+
// ctx.isAction() matches by reference (rename-safe), not by string.
|
|
128
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
129
|
+
]),
|
|
130
|
+
path("/shop/:slug", ProductPage, { name: "product" }, () => [ // structure: leaf
|
|
131
|
+
loader(ProductLoader, () => [cache({ ttl: 60 })]), // config: cache loader DATA
|
|
132
|
+
loading(<ProductSkeleton />), // config
|
|
133
|
+
withRecs(), // composed factory (config array)
|
|
134
|
+
]),
|
|
135
|
+
]),
|
|
136
|
+
]);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
One tree, both axes visible: structure (`layout`/`path`) vs config (everything
|
|
140
|
+
else), freshness (`cache`) vs client-update (`revalidate`). Actions are matched
|
|
141
|
+
by reference with `ctx.isAction(Action)` (rename-safe, where `CartActions` is an
|
|
142
|
+
`import * as CartActions from "./actions/cart"`); see `/typesafety` → "Stable
|
|
143
|
+
identity".
|
|
144
|
+
|
|
145
|
+
**The source is the source of truth.** Structure, types, and update policy are
|
|
146
|
+
visible and local in the tree — read top-down, no hidden global model to hold in
|
|
147
|
+
your head. A snippet earns its place only if, from the code alone, you can answer:
|
|
148
|
+
_what URLs exist and who owns them?_ (composition), _can I trust this reference
|
|
149
|
+
without leaving the call site?_ (type-safety), _what re-renders after this
|
|
150
|
+
action?_ (partial rendering). If any answer needs another file, it isn't legible
|
|
151
|
+
yet.
|
|
152
|
+
|
|
11
153
|
## Skills
|
|
12
154
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
| `/
|
|
20
|
-
| `/
|
|
21
|
-
| `/
|
|
22
|
-
| `/
|
|
23
|
-
| `/
|
|
24
|
-
| `/
|
|
25
|
-
| `/
|
|
26
|
-
| `/
|
|
27
|
-
| `/
|
|
28
|
-
| `/
|
|
29
|
-
| `/
|
|
30
|
-
| `/
|
|
31
|
-
| `/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
| `/
|
|
38
|
-
| `/
|
|
39
|
-
| `/
|
|
155
|
+
Grouped by concern — read when you need to…
|
|
156
|
+
|
|
157
|
+
**Structure & routing** — shape URLs, layouts, navigation, and request processing:
|
|
158
|
+
|
|
159
|
+
| Skill | Description |
|
|
160
|
+
| ------------------------- | -------------------------------------------------------------------------- |
|
|
161
|
+
| `/router-setup` | Create and configure the RSC router |
|
|
162
|
+
| `/route` | Define routes with `urls()`, `path()`, and `include()` |
|
|
163
|
+
| `/layout` | Layouts that wrap child routes |
|
|
164
|
+
| `/parallel` | Multi-column layouts and sidebars |
|
|
165
|
+
| `/intercept` | Modal/slide-over patterns for soft navigation |
|
|
166
|
+
| `/middleware` | Request processing and authentication |
|
|
167
|
+
| `/host-router` | Multi-app host routing with domain/subdomain patterns |
|
|
168
|
+
| `/links` | URL generation: ctx.reverse, href, useHref, useMount, scopedReverse |
|
|
169
|
+
| `/response-routes` | JSON/text/HTML/XML/stream endpoints with `path.json()`, `path.text()` |
|
|
170
|
+
| `/mime-routes` | Content negotiation — same URL, different response types via Accept header |
|
|
171
|
+
| `/streams-and-websockets` | SSE via `path.stream` and WebSocket upgrades via `path.any` |
|
|
172
|
+
| `/handler-use` | Attach default loaders/middleware to a handler via `handler.use` |
|
|
173
|
+
| `/composability` | Reusable route-helper factories (structure vs config) |
|
|
174
|
+
|
|
175
|
+
**Data & caching** — fetch, mutate, and cache:
|
|
176
|
+
|
|
177
|
+
| Skill | Description |
|
|
178
|
+
| ----------------- | ----------------------------------------------------------------------- |
|
|
179
|
+
| `/loader` | Data loaders with `createLoader()` and `revalidate()` |
|
|
180
|
+
| `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation |
|
|
181
|
+
| `/caching` | Segment caching with memory or KV stores |
|
|
182
|
+
| `/use-cache` | Function-level caching with `"use cache"` directive |
|
|
183
|
+
| `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide |
|
|
184
|
+
| `/document-cache` | Edge caching with Cache-Control headers |
|
|
185
|
+
| `/prerender` | Pre-render route segments at build time (Passthrough live fallback) |
|
|
186
|
+
|
|
187
|
+
**Client & presentation** — build the client-side UX:
|
|
188
|
+
|
|
189
|
+
| Skill | Description |
|
|
190
|
+
| ------------------- | ------------------------------------------------------------------------- |
|
|
191
|
+
| `/hooks` | Client-side React hooks |
|
|
192
|
+
| `/theme` | Light/dark mode with FOUC prevention |
|
|
193
|
+
| `/i18n` | Locale routing with `:locale?`, resolution chains, react-intl integration |
|
|
194
|
+
| `/fonts` | Load web fonts with preload hints |
|
|
195
|
+
| `/tailwind` | Set up Tailwind CSS v4 with `?url` imports |
|
|
196
|
+
| `/view-transitions` | React View Transitions on layouts, routes, and parallel slots |
|
|
197
|
+
| `/breadcrumbs` | Built-in Breadcrumbs handle for breadcrumb navigation |
|
|
198
|
+
|
|
199
|
+
**Observability & production health**:
|
|
200
|
+
|
|
201
|
+
| Skill | Description |
|
|
202
|
+
| ------------------ | ------------------------------------------------------------------------ |
|
|
203
|
+
| `/observability` | `debugPerformance`, `Server-Timing`, structured telemetry, tracing |
|
|
204
|
+
| `/bundle-analysis` | Audit your app's production bundle for server leaks and oversized chunks |
|
|
205
|
+
| `/debug-manifest` | Inspect route manifest structure |
|
|
206
|
+
|
|
207
|
+
**Setup, types & migration**:
|
|
208
|
+
|
|
209
|
+
| Skill | Description |
|
|
210
|
+
| ----------------------- | ----------------------------------------------- |
|
|
211
|
+
| `/typesafety` | Type-safe routes, params, href, and environment |
|
|
212
|
+
| `/migrate-nextjs` | Migrate a Next.js App Router project to Rango |
|
|
213
|
+
| `/migrate-react-router` | Migrate a React Router / Remix project to Rango |
|
|
40
214
|
|
|
41
215
|
## Quick Start
|
|
42
216
|
|
package/skills/route/SKILL.md
CHANGED
|
@@ -234,14 +234,22 @@ Cacheable vars (the default) can be read freely inside cache scopes.
|
|
|
234
234
|
|
|
235
235
|
### Revalidation Contracts for Handler Data
|
|
236
236
|
|
|
237
|
+
> **Scope: `revalidate()` is a partial-render concern, not a cache concern.**
|
|
238
|
+
> It decides whether this segment re-runs and streams to the client on a
|
|
239
|
+
> navigation or action — never whether a cached value is stale. The cache
|
|
240
|
+
> decides hit/miss/ttl/swr independently and never reads `revalidate()`. See
|
|
241
|
+
> `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
|
|
242
|
+
|
|
237
243
|
Handler-first guarantees apply within a single full render pass. For partial
|
|
238
244
|
action revalidation, define named revalidation contracts and reuse them on both
|
|
239
245
|
the producer route and the consumer child segments.
|
|
240
246
|
|
|
241
247
|
```typescript
|
|
242
248
|
// revalidation-contracts.ts
|
|
249
|
+
// Defer (|| undefined), not ?? false: a hard `false` short-circuits the chain,
|
|
250
|
+
// so when the same segment composes multiple contracts the later ones never run.
|
|
243
251
|
export const revalidateCheckoutData = ({ actionId }) =>
|
|
244
|
-
actionId?.includes("src/actions/checkout.ts#")
|
|
252
|
+
actionId?.includes("src/actions/checkout.ts#") || undefined;
|
|
245
253
|
|
|
246
254
|
path("/checkout", CheckoutPage, { name: "checkout" }, () => [
|
|
247
255
|
revalidate(revalidateCheckoutData), // producer (route handler) reruns
|
|
@@ -270,9 +278,6 @@ path("/checkout", CheckoutPage, { name: "checkout" }, () => [
|
|
|
270
278
|
]);
|
|
271
279
|
```
|
|
272
280
|
|
|
273
|
-
For scope/revalidation guarantees and non-guarantees, see:
|
|
274
|
-
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
275
|
-
|
|
276
281
|
## Redirects
|
|
277
282
|
|
|
278
283
|
### Basic redirect
|
|
@@ -32,35 +32,37 @@ Actions mutate state; route handlers and loaders read the latest state. After
|
|
|
32
32
|
an action finishes, Rango performs a server-side revalidation render for the
|
|
33
33
|
matched route so the UI receives fresh segment output and loader data.
|
|
34
34
|
|
|
35
|
-
The main control point is `revalidate((
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
The main control point is `revalidate((ctx) => ...)` on the segment that owns
|
|
36
|
+
the data. Match specific actions by imported reference with `ctx.isAction()`;
|
|
37
|
+
use raw `actionId` only when you intentionally need path or directory matching.
|
|
38
|
+
This applies to `path()` handlers, `layout()` handlers, `parallel()` slots,
|
|
39
|
+
`intercept()` routes, and loader registrations:
|
|
38
40
|
|
|
39
41
|
```typescript
|
|
40
42
|
// urls.tsx — path/layout/parallel/intercept/loader/revalidate are passed in by urls()
|
|
41
43
|
import { urls } from "@rangojs/router";
|
|
44
|
+
import * as CartActions from "./actions/cart";
|
|
42
45
|
|
|
43
46
|
export const urlpatterns = urls(({ path, loader, revalidate }) => [
|
|
44
47
|
// The loader belongs to the route that consumes its data — nest it inside
|
|
45
48
|
// the owning path() so the segment owns its data dependency.
|
|
46
49
|
path("/cart", CartPage, { name: "cart" }, () => [
|
|
47
|
-
revalidate(
|
|
48
|
-
({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
|
|
49
|
-
),
|
|
50
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
50
51
|
loader(CartLoader, () => [
|
|
51
|
-
revalidate(
|
|
52
|
-
({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
|
|
53
|
-
),
|
|
52
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
54
53
|
]),
|
|
55
54
|
]),
|
|
56
55
|
]);
|
|
57
56
|
```
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
`ctx.isAction()` resolves the imported action reference the same way the router
|
|
59
|
+
derives `actionId`, so it matches in both dev and production and survives action
|
|
60
|
+
renames/moves as type errors instead of silent substring drift.
|
|
61
|
+
|
|
62
|
+
For module-level `"use server"` files, the raw `actionId` passed to every
|
|
60
63
|
server-side `revalidate()` predicate is path-bearing in the server/RSC
|
|
61
|
-
environment in both dev and production: `src/actions/cart.ts#addToCart`. This
|
|
62
|
-
|
|
63
|
-
predicates can filter by action file, directory, or export name.
|
|
64
|
+
environment in both dev and production: `src/actions/cart.ts#addToCart`. This is
|
|
65
|
+
the escape hatch for broad filters by action file, directory, or export name.
|
|
64
66
|
|
|
65
67
|
Actions and the follow-up revalidation render share one request context.
|
|
66
68
|
Values written in the action with `ctx.set(MyVar, value)` or `ctx.set("key",
|
|
@@ -91,15 +93,18 @@ export async function switchTenant(tenantId: string) {
|
|
|
91
93
|
```typescript
|
|
92
94
|
// urls.tsx
|
|
93
95
|
import { urls } from "@rangojs/router";
|
|
96
|
+
import * as TenantActions from "./actions/tenant";
|
|
94
97
|
import { ChangedTenant } from "./context";
|
|
95
98
|
|
|
96
99
|
export const urlpatterns = urls(({ path, revalidate }) => [
|
|
97
100
|
path("/dashboard/:tenantId", DashboardPage, { name: "dashboard" }, () => [
|
|
98
|
-
revalidate(
|
|
99
|
-
(
|
|
100
|
-
|
|
101
|
-
context.get(ChangedTenant) === context.params.tenantId
|
|
102
|
-
|
|
101
|
+
revalidate((ctx) => {
|
|
102
|
+
if (!ctx.isAction(TenantActions)) return undefined;
|
|
103
|
+
return (
|
|
104
|
+
ctx.context.get(ChangedTenant) === ctx.context.params.tenantId ||
|
|
105
|
+
undefined
|
|
106
|
+
);
|
|
107
|
+
}),
|
|
103
108
|
]),
|
|
104
109
|
]);
|
|
105
110
|
```
|
|
@@ -380,13 +385,18 @@ re-render so the UI updates. Rango runs the action, then evaluates
|
|
|
380
385
|
`revalidate()` on matched segments and loaders. Each path, layout, parallel,
|
|
381
386
|
intercept, or loader rule decides whether that piece re-renders/re-resolves.
|
|
382
387
|
|
|
383
|
-
|
|
384
|
-
|
|
388
|
+
Use `ctx.isAction()` for specific actions or modules. It accepts one action,
|
|
389
|
+
several actions, or a namespace import (`import * as CartActions`). Pair it with
|
|
390
|
+
`|| undefined` for "revalidate on match, otherwise defer to defaults/downstream
|
|
391
|
+
rules."
|
|
385
392
|
|
|
386
393
|
```typescript
|
|
387
394
|
// urls.tsx — inside the urls() callback. Nest each loader inside the path(),
|
|
388
395
|
// layout(), or parallel() that owns its data so the route tree mirrors the
|
|
389
396
|
// data dependencies.
|
|
397
|
+
import * as AccountActions from "./actions/account";
|
|
398
|
+
import * as CartActions from "./actions/cart";
|
|
399
|
+
|
|
390
400
|
urls(({ path, loader, revalidate }) => [
|
|
391
401
|
path("/", HomePage, { name: "home" }, () => [
|
|
392
402
|
// Loader data re-runs by default after any action. Opt out with revalidate(() => false).
|
|
@@ -395,36 +405,37 @@ urls(({ path, loader, revalidate }) => [
|
|
|
395
405
|
|
|
396
406
|
// Re-render the cart page handler AND re-resolve its loader after cart actions
|
|
397
407
|
path("/cart", CartPage, { name: "cart" }, () => [
|
|
398
|
-
revalidate(
|
|
399
|
-
({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
|
|
400
|
-
),
|
|
408
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
401
409
|
loader(CartLoader, () => [
|
|
402
|
-
revalidate(
|
|
403
|
-
({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
|
|
404
|
-
),
|
|
410
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
405
411
|
]),
|
|
406
412
|
]),
|
|
407
413
|
|
|
408
|
-
// Re-run after any action
|
|
414
|
+
// Re-run after any action exported by the account actions module
|
|
409
415
|
path("/account", AccountPage, { name: "account" }, () => [
|
|
410
416
|
loader(AccountLoader, () => [
|
|
411
|
-
revalidate(
|
|
412
|
-
({ actionId }) => actionId?.startsWith("src/actions/account/") ?? false,
|
|
413
|
-
),
|
|
417
|
+
revalidate((ctx) => ctx.isAction(AccountActions) || undefined),
|
|
414
418
|
]),
|
|
415
419
|
]),
|
|
416
420
|
]);
|
|
417
421
|
```
|
|
418
422
|
|
|
419
|
-
`actionId`
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
423
|
+
The raw `actionId` string stays available for broad path filters:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
// Match any action under src/actions/account/, including modules not imported here.
|
|
427
|
+
revalidate(
|
|
428
|
+
({ actionId }) => actionId?.startsWith("src/actions/account/") || undefined,
|
|
429
|
+
);
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
For actions exported from a module-level `"use server"` file, the ID is prefixed
|
|
433
|
+
with the source file path (`src/actions/cart.ts#addToCart`). **Inline `"use
|
|
434
|
+
server"` actions** (declared inside an RSC component) intentionally keep their
|
|
435
|
+
hashed IDs — file paths are withheld from the client for security. If you need
|
|
436
|
+
file-path-based revalidation predicates, define the action in a module-level
|
|
437
|
+
`"use server"` file rather than inline. See `/loader` for the full revalidation
|
|
438
|
+
contract (deferred returns, soft suggestions).
|
|
428
439
|
|
|
429
440
|
### Cross-segment dependencies
|
|
430
441
|
|
|
@@ -434,8 +445,9 @@ stale context. Share the same `revalidate` predicate on both producer and
|
|
|
434
445
|
consumer:
|
|
435
446
|
|
|
436
447
|
```typescript
|
|
437
|
-
|
|
438
|
-
|
|
448
|
+
import * as CartActions from "./actions/cart";
|
|
449
|
+
|
|
450
|
+
const revalidateCart = (ctx) => ctx.isAction(CartActions) || undefined;
|
|
439
451
|
|
|
440
452
|
urls(({ path, layout, loader, revalidate }) => [
|
|
441
453
|
layout(CartLayout, () => [
|