@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
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 +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +170 -18
- package/src/build/runtime-discovery.ts +13 -1
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/index.ts +0 -3
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.0.0-experimental.
|
|
3
|
+
"version": "0.0.0-experimental.26",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"!src/**/*.test.tsx",
|
|
32
32
|
"dist",
|
|
33
33
|
"skills",
|
|
34
|
-
"
|
|
34
|
+
"AGENTS.md",
|
|
35
35
|
"README.md"
|
|
36
36
|
],
|
|
37
37
|
"type": "module",
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
"vitest": "^4.0.0"
|
|
153
153
|
},
|
|
154
154
|
"peerDependencies": {
|
|
155
|
-
"@cloudflare/vite-plugin": "^1.25.
|
|
155
|
+
"@cloudflare/vite-plugin": "^1.25.0",
|
|
156
156
|
"@vitejs/plugin-rsc": "^0.5.14",
|
|
157
157
|
"react": "^18.0.0 || ^19.0.0",
|
|
158
158
|
"vite": "^7.3.0"
|
|
@@ -67,9 +67,11 @@ HIT → function body skipped, calling code runs, handle data replayed
|
|
|
67
67
|
MISS → function body runs, return value + handle data cached
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
Runtime guards throw if you call
|
|
70
|
+
Runtime guards throw if you call cookies(), headers(), ctx.header(), ctx.set(),
|
|
71
71
|
ctx.onResponse(), ctx.setTheme(), or ctx.setLocationState() inside a "use cache"
|
|
72
|
-
function.
|
|
72
|
+
function. cookies() and headers() are blocked because per-request data is not in the
|
|
73
|
+
cache key. Side-effect methods are blocked because their effects are lost on hit.
|
|
74
|
+
Use ctx.use(Handle) instead for data — handle data is captured and replayed.
|
|
73
75
|
|
|
74
76
|
## When to Use cache()
|
|
75
77
|
|
|
@@ -149,8 +151,8 @@ Neither mechanism caches response headers or cookies.
|
|
|
149
151
|
- **cache()**: Headers set by handlers are naturally absent on hit because no
|
|
150
152
|
handler runs. If you need headers on every response, set them in middleware
|
|
151
153
|
(which runs before cache lookup).
|
|
152
|
-
- **"use cache"**:
|
|
153
|
-
|
|
154
|
+
- **"use cache"**: cookies() and headers() throw inside the cached function
|
|
155
|
+
(both reads and writes). ctx.header() also throws. Move them outside.
|
|
154
156
|
|
|
155
157
|
```typescript
|
|
156
158
|
// Set headers that must appear on every response in middleware
|
|
@@ -234,7 +236,9 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
234
236
|
```
|
|
235
237
|
|
|
236
238
|
This attaches the cache config directly to the loader entry. The loader's
|
|
237
|
-
data is cached independently from the route's segment cache.
|
|
239
|
+
data is cached independently from the route's segment cache. Loader caching
|
|
240
|
+
supports custom keys, tags, SWR, conditional bypass, and per-loader store
|
|
241
|
+
overrides — see `/loader` for the full reference.
|
|
238
242
|
|
|
239
243
|
## Decision Flowchart
|
|
240
244
|
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -89,7 +89,7 @@ Configure a cache store in the router:
|
|
|
89
89
|
|
|
90
90
|
```typescript
|
|
91
91
|
import { createRouter } from "@rangojs/router";
|
|
92
|
-
import { MemorySegmentCacheStore } from "@rangojs/router/
|
|
92
|
+
import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
93
93
|
|
|
94
94
|
const store = new MemorySegmentCacheStore({
|
|
95
95
|
defaults: { ttl: 60, swr: 300 },
|
|
@@ -112,7 +112,7 @@ const router = createRouter({
|
|
|
112
112
|
For single-instance deployments:
|
|
113
113
|
|
|
114
114
|
```typescript
|
|
115
|
-
import { MemorySegmentCacheStore } from "@rangojs/router/
|
|
115
|
+
import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
116
116
|
|
|
117
117
|
const store = new MemorySegmentCacheStore({
|
|
118
118
|
defaults: { ttl: 60, swr: 300 },
|
|
@@ -125,7 +125,7 @@ const store = new MemorySegmentCacheStore({
|
|
|
125
125
|
For distributed caching on Cloudflare Workers:
|
|
126
126
|
|
|
127
127
|
```typescript
|
|
128
|
-
import { CFCacheStore } from "@rangojs/router/cache
|
|
128
|
+
import { CFCacheStore } from "@rangojs/router/cache";
|
|
129
129
|
|
|
130
130
|
const router = createRouter<AppBindings>({
|
|
131
131
|
document: Document,
|
|
@@ -175,7 +175,7 @@ cache({ store: checkoutCache }, () => [
|
|
|
175
175
|
|
|
176
176
|
```typescript
|
|
177
177
|
import { urls } from "@rangojs/router";
|
|
178
|
-
import { MemorySegmentCacheStore } from "@rangojs/router/
|
|
178
|
+
import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
179
179
|
|
|
180
180
|
// Custom store for checkout (short TTL)
|
|
181
181
|
const checkoutCache = new MemorySegmentCacheStore({
|
|
@@ -14,7 +14,7 @@ Configure document cache in router:
|
|
|
14
14
|
|
|
15
15
|
```typescript
|
|
16
16
|
import { createRouter } from "@rangojs/router";
|
|
17
|
-
import { CFCacheStore } from "@rangojs/router/cache
|
|
17
|
+
import { CFCacheStore } from "@rangojs/router/cache";
|
|
18
18
|
import { urlpatterns } from "./urls";
|
|
19
19
|
|
|
20
20
|
const router = createRouter<AppBindings>({
|
|
@@ -134,7 +134,7 @@ Segment hash ensures different cached responses for navigations from different s
|
|
|
134
134
|
```typescript
|
|
135
135
|
// router.tsx
|
|
136
136
|
import { createRouter } from "@rangojs/router";
|
|
137
|
-
import { CFCacheStore } from "@rangojs/router/cache
|
|
137
|
+
import { CFCacheStore } from "@rangojs/router/cache";
|
|
138
138
|
import { urlpatterns } from "./urls";
|
|
139
139
|
|
|
140
140
|
const router = createRouter<AppBindings>({
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -6,7 +6,8 @@ argument-hint: [hook-name]
|
|
|
6
6
|
|
|
7
7
|
# Client-Side React Hooks
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Import the hooks and components in this skill from `@rangojs/router/client`.
|
|
10
|
+
The root `@rangojs/router` entrypoint is for server/RSC APIs and shared types.
|
|
10
11
|
|
|
11
12
|
## Navigation Hooks
|
|
12
13
|
|
|
@@ -63,7 +64,7 @@ Access current URL path and matched route segments:
|
|
|
63
64
|
|
|
64
65
|
```tsx
|
|
65
66
|
"use client";
|
|
66
|
-
import { useSegments } from "@rangojs/router";
|
|
67
|
+
import { useSegments } from "@rangojs/router/client";
|
|
67
68
|
|
|
68
69
|
function Breadcrumbs() {
|
|
69
70
|
const { path, segmentIds, location } = useSegments();
|
|
@@ -107,7 +108,7 @@ Access loader data (strict - data guaranteed):
|
|
|
107
108
|
|
|
108
109
|
```tsx
|
|
109
110
|
"use client";
|
|
110
|
-
import { useLoader } from "@rangojs/router";
|
|
111
|
+
import { useLoader } from "@rangojs/router/client";
|
|
111
112
|
import { ProductLoader } from "../loaders/product";
|
|
112
113
|
|
|
113
114
|
function ProductPrice() {
|
|
@@ -143,7 +144,7 @@ Access loader with on-demand fetching (flexible):
|
|
|
143
144
|
|
|
144
145
|
```tsx
|
|
145
146
|
"use client";
|
|
146
|
-
import { useFetchLoader } from "@rangojs/router";
|
|
147
|
+
import { useFetchLoader } from "@rangojs/router/client";
|
|
147
148
|
import { SearchLoader } from "../loaders/search";
|
|
148
149
|
|
|
149
150
|
function SearchResults() {
|
|
@@ -197,7 +198,7 @@ server, JSON bodies are available via `ctx.body` and FormData bodies via `ctx.fo
|
|
|
197
198
|
|
|
198
199
|
```tsx
|
|
199
200
|
"use client";
|
|
200
|
-
import { useFetchLoader } from "@rangojs/router";
|
|
201
|
+
import { useFetchLoader } from "@rangojs/router/client";
|
|
201
202
|
import { FileUploadLoader } from "../loaders/upload";
|
|
202
203
|
|
|
203
204
|
function FileUploader() {
|
|
@@ -238,22 +239,6 @@ export const FileUploadLoader = createLoader(async (ctx) => {
|
|
|
238
239
|
}, true); // true = fetchable (can be called from the client via load())
|
|
239
240
|
```
|
|
240
241
|
|
|
241
|
-
### useLoaderData()
|
|
242
|
-
|
|
243
|
-
Get all loader data in current context:
|
|
244
|
-
|
|
245
|
-
```tsx
|
|
246
|
-
"use client";
|
|
247
|
-
import { useLoaderData } from "@rangojs/router";
|
|
248
|
-
|
|
249
|
-
function DebugPanel() {
|
|
250
|
-
const allData = useLoaderData();
|
|
251
|
-
// Record<string, any> - Map of loader ID to data
|
|
252
|
-
|
|
253
|
-
return <pre>{JSON.stringify(allData, null, 2)}</pre>;
|
|
254
|
-
}
|
|
255
|
-
```
|
|
256
|
-
|
|
257
242
|
## Handle Hooks
|
|
258
243
|
|
|
259
244
|
### useHandle()
|
|
@@ -262,7 +247,7 @@ Access accumulated handle data from route segments:
|
|
|
262
247
|
|
|
263
248
|
```tsx
|
|
264
249
|
"use client";
|
|
265
|
-
import { useHandle } from "@rangojs/router";
|
|
250
|
+
import { useHandle } from "@rangojs/router/client";
|
|
266
251
|
import { Breadcrumbs } from "../handles/breadcrumbs";
|
|
267
252
|
|
|
268
253
|
function BreadcrumbNav() {
|
|
@@ -324,7 +309,7 @@ Track state of server action invocations:
|
|
|
324
309
|
|
|
325
310
|
```tsx
|
|
326
311
|
"use client";
|
|
327
|
-
import { useAction } from "@rangojs/router";
|
|
312
|
+
import { useAction } from "@rangojs/router/client";
|
|
328
313
|
import { addToCart } from "../actions/cart";
|
|
329
314
|
|
|
330
315
|
function AddToCartButton({ productId }: { productId: string }) {
|
|
@@ -359,7 +344,7 @@ Read type-safe state from history:
|
|
|
359
344
|
|
|
360
345
|
```tsx
|
|
361
346
|
"use client";
|
|
362
|
-
import { useLocationState, createLocationState } from "@rangojs/router";
|
|
347
|
+
import { useLocationState, createLocationState } from "@rangojs/router/client";
|
|
363
348
|
|
|
364
349
|
// Define typed state (all export patterns supported)
|
|
365
350
|
// Keys are auto-injected by the Vite plugin -- no manual key needed.
|
|
@@ -398,6 +383,33 @@ import { ProductState } from "./state";
|
|
|
398
383
|
</Link>;
|
|
399
384
|
```
|
|
400
385
|
|
|
386
|
+
Pass typed state just in time (getter evaluated at click time, not render time):
|
|
387
|
+
|
|
388
|
+
```tsx
|
|
389
|
+
"use client"; // JIT state requires a client component (getter can't cross RSC boundary)
|
|
390
|
+
|
|
391
|
+
import { Link } from "@rangojs/router/client";
|
|
392
|
+
import { ProductState } from "./state";
|
|
393
|
+
|
|
394
|
+
// The getter is stored lazily and only called when the user clicks the link.
|
|
395
|
+
// This is useful for capturing values that change after render (e.g., scroll
|
|
396
|
+
// position, form state, ref values).
|
|
397
|
+
<Link
|
|
398
|
+
to="/product/123"
|
|
399
|
+
state={[ProductState(() => ({ name: product.name, price: product.price }))]}
|
|
400
|
+
>
|
|
401
|
+
View Product
|
|
402
|
+
</Link>;
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Plain state can also be evaluated just in time (also requires a client component):
|
|
406
|
+
|
|
407
|
+
```tsx
|
|
408
|
+
<Link to="/product/123" state={() => ({ from: window.location.pathname })}>
|
|
409
|
+
View Product
|
|
410
|
+
</Link>
|
|
411
|
+
```
|
|
412
|
+
|
|
401
413
|
### Flash State (read-once)
|
|
402
414
|
|
|
403
415
|
Create a location state with `{ flash: true }` for read-once state that
|
|
@@ -457,7 +469,7 @@ Or via `ctx.setLocationState()` on any response:
|
|
|
457
469
|
|
|
458
470
|
```tsx
|
|
459
471
|
(ctx) => {
|
|
460
|
-
ctx.setLocationState(
|
|
472
|
+
ctx.setLocationState(FlashMessage({ text: "Welcome back!" }));
|
|
461
473
|
return <Dashboard />;
|
|
462
474
|
};
|
|
463
475
|
```
|
|
@@ -482,7 +494,7 @@ Manually control client-side navigation cache:
|
|
|
482
494
|
|
|
483
495
|
```tsx
|
|
484
496
|
"use client";
|
|
485
|
-
import { useClientCache } from "@rangojs/router";
|
|
497
|
+
import { useClientCache } from "@rangojs/router/client";
|
|
486
498
|
|
|
487
499
|
function SaveButton() {
|
|
488
500
|
const { clear } = useClientCache();
|
|
@@ -510,7 +522,7 @@ function SaveButton() {
|
|
|
510
522
|
Render child content in layouts:
|
|
511
523
|
|
|
512
524
|
```tsx
|
|
513
|
-
import { Outlet, ParallelOutlet } from "@rangojs/router";
|
|
525
|
+
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
514
526
|
|
|
515
527
|
function DashboardLayout({ children }: { children?: React.ReactNode }) {
|
|
516
528
|
return (
|
|
@@ -531,7 +543,7 @@ Access outlet content programmatically:
|
|
|
531
543
|
|
|
532
544
|
```tsx
|
|
533
545
|
"use client";
|
|
534
|
-
import { useOutlet } from "@rangojs/router";
|
|
546
|
+
import { useOutlet } from "@rangojs/router/client";
|
|
535
547
|
|
|
536
548
|
function ConditionalLayout() {
|
|
537
549
|
const outlet = useOutlet();
|
|
@@ -668,7 +680,6 @@ See `/links` for full URL generation guide including server-side `ctx.reverse`.
|
|
|
668
680
|
| `useLinkStatus()` | Link pending state | { pending } |
|
|
669
681
|
| `useLoader()` | Loader data (strict) | data, isLoading, error |
|
|
670
682
|
| `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading |
|
|
671
|
-
| `useLoaderData()` | All loader data | Record<string, any> |
|
|
672
683
|
| `useHandle()` | Accumulated handle data | T (handle type) |
|
|
673
684
|
| `useAction()` | Server action state | state, error, result |
|
|
674
685
|
| `useLocationState()` | History state (persists or flash) | T \| undefined |
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: host-router
|
|
3
|
+
description: Multi-app host routing with domain/subdomain patterns
|
|
4
|
+
argument-hint:
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Host Router
|
|
8
|
+
|
|
9
|
+
Route requests to different apps based on domain, subdomain, or path prefix patterns. Supports middleware, lazy loading, cookie-based host override for dev, and a fallback handler.
|
|
10
|
+
|
|
11
|
+
## Import
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createHostRouter, defineHosts } from "@rangojs/router/host";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Basic Setup
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// host-router.ts
|
|
21
|
+
import { createHostRouter } from "@rangojs/router/host";
|
|
22
|
+
|
|
23
|
+
const router = createHostRouter();
|
|
24
|
+
|
|
25
|
+
router.host(["."]).map(() => import("./apps/main"));
|
|
26
|
+
router.host(["admin.*"]).map(() => import("./apps/admin"));
|
|
27
|
+
router.host(["api.*"]).map(() => import("./apps/api"));
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
31
|
+
return router.match(request, { env, ctx });
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Each `.map()` receives either a direct handler `(request, input) => Response` or a lazy import `() => import(...)`. Lazy imports resolve a module with a `default` export that is either a handler function or another `HostRouter` (for nesting).
|
|
37
|
+
|
|
38
|
+
## Pattern Syntax
|
|
39
|
+
|
|
40
|
+
| Pattern | Matches |
|
|
41
|
+
| ----------------- | ---------------------------------------------- |
|
|
42
|
+
| `.` or `*` | Any apex domain (`example.com`) |
|
|
43
|
+
| `**` | Any domain (apex + all subdomains) |
|
|
44
|
+
| `*.` | Any single-level subdomain (`www.example.com`) |
|
|
45
|
+
| `**. ` | Any multi-level subdomain (`a.b.example.com`) |
|
|
46
|
+
| `example.com` | Exact domain |
|
|
47
|
+
| `*.com` | Any apex `.com` domain |
|
|
48
|
+
| `*.example.com` | Single subdomain of `example.com` |
|
|
49
|
+
| `**.example.com` | Any depth subdomain of `example.com` |
|
|
50
|
+
| `admin.*` | `admin` subdomain of any apex domain |
|
|
51
|
+
| `admin.**` | `admin` subdomain of any domain |
|
|
52
|
+
| `admin.` | `admin` subdomain of any apex (no wildcard) |
|
|
53
|
+
| `example.com/api` | Domain + path prefix (prefix match) |
|
|
54
|
+
|
|
55
|
+
Patterns are tested in registration order. First match wins.
|
|
56
|
+
|
|
57
|
+
## `defineHosts` for Type Safety
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { defineHosts } from "@rangojs/router/host";
|
|
61
|
+
|
|
62
|
+
const hosts = defineHosts({
|
|
63
|
+
admin: "admin.*",
|
|
64
|
+
api: "api.*",
|
|
65
|
+
app: [".", "www.*"],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
router.host(hosts.admin).map(() => import("./apps/admin"));
|
|
69
|
+
router.host(hosts.app).map(() => import("./apps/main"));
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Returns a frozen object — keys are autocompleted by TypeScript.
|
|
73
|
+
|
|
74
|
+
## Middleware
|
|
75
|
+
|
|
76
|
+
Global middleware runs for every matched route. Per-route middleware runs only for that host pattern.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const router = createHostRouter();
|
|
80
|
+
|
|
81
|
+
// Global — runs for all routes
|
|
82
|
+
router.use(async (request, input, next) => {
|
|
83
|
+
console.log(`[${new Date().toISOString()}] ${request.url}`);
|
|
84
|
+
return next();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Per-route
|
|
88
|
+
router
|
|
89
|
+
.host(["admin.*"])
|
|
90
|
+
.use(requireAuth)
|
|
91
|
+
.map(() => import("./apps/admin"));
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Middleware signature: `(request: Request, input: RouterRequestInput, next: () => Promise<Response>) => Promise<Response>`
|
|
95
|
+
|
|
96
|
+
Calling `next()` more than once throws.
|
|
97
|
+
|
|
98
|
+
## Fallback Handler
|
|
99
|
+
|
|
100
|
+
Handles cookie-override errors when `hostOverride` is configured (e.g., override from a disallowed host, invalid cookie hostname). The fallback does **not** catch unmatched hosts — those throw `NoRouteMatchError`. Catch that at the worker level if you need a 404.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const router = createHostRouter({
|
|
104
|
+
hostOverride: { cookieName: "x-dev-host", allowedHosts: ["localhost"] },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Called when cookie override fails (not for general unmatched hosts)
|
|
108
|
+
router.fallback().map((request) => {
|
|
109
|
+
return new Response("Invalid host override", { status: 400 });
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
For unmatched hosts without `hostOverride`, catch `NoRouteMatchError` in your worker fetch:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { NoRouteMatchError } from "@rangojs/router/host";
|
|
117
|
+
|
|
118
|
+
export default {
|
|
119
|
+
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
120
|
+
try {
|
|
121
|
+
return await router.match(request, { env, ctx });
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (err instanceof NoRouteMatchError) {
|
|
124
|
+
return new Response("Not Found", { status: 404 });
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Cookie-Based Host Override
|
|
133
|
+
|
|
134
|
+
For development: route requests to a different app based on a cookie value, allowing developers to test different host routes from a single domain.
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const router = createHostRouter({
|
|
138
|
+
hostOverride: {
|
|
139
|
+
cookieName: "x-dev-host",
|
|
140
|
+
allowedHosts: ["localhost", "**.dev.example.com"],
|
|
141
|
+
validate: (request, cookieValue, input) => {
|
|
142
|
+
// Optional custom validation — return the effective hostname
|
|
143
|
+
return cookieValue;
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
When a request arrives:
|
|
150
|
+
|
|
151
|
+
1. If no cookie → use actual hostname
|
|
152
|
+
2. If cookie present and host is in `allowedHosts` → use cookie value as hostname
|
|
153
|
+
3. If cookie present but host not allowed → throw `HostOverrideNotAllowedError`
|
|
154
|
+
|
|
155
|
+
Without a custom `validate`, the cookie value is validated as a hostname via `new URL()`.
|
|
156
|
+
|
|
157
|
+
## Debug Mode
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const router = createHostRouter({ debug: true });
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Logs pattern matching, route registration, and cookie override decisions to console.
|
|
164
|
+
|
|
165
|
+
## Testing
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { createTestRequest, testPattern } from "@rangojs/router/host/testing";
|
|
169
|
+
|
|
170
|
+
// Test pattern matching
|
|
171
|
+
testPattern("admin.*", "admin.example.com"); // true
|
|
172
|
+
testPattern([".", "www.*"], "example.com"); // true
|
|
173
|
+
|
|
174
|
+
// Create requests for integration tests
|
|
175
|
+
const request = createTestRequest({
|
|
176
|
+
host: "admin.example.com",
|
|
177
|
+
path: "/dashboard",
|
|
178
|
+
cookies: { "x-dev-host": "api.example.com" },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Test which route would match (without executing)
|
|
182
|
+
router.test("admin.example.com"); // { pattern, handler } | null
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Error Types
|
|
186
|
+
|
|
187
|
+
All errors extend `HostRouterError`:
|
|
188
|
+
|
|
189
|
+
| Error | When |
|
|
190
|
+
| ----------------------------- | ------------------------------------------- |
|
|
191
|
+
| `InvalidPatternError` | Pattern is empty, non-string, or has spaces |
|
|
192
|
+
| `HostOverrideNotAllowedError` | Cookie override from disallowed host |
|
|
193
|
+
| `InvalidHostnameError` | Cookie value isn't a valid hostname |
|
|
194
|
+
| `HostValidationError` | Custom `validate` function threw |
|
|
195
|
+
| `NoRouteMatchError` | No host pattern matched the request |
|
|
196
|
+
| `InvalidHandlerError` | Handler is not a function |
|
|
197
|
+
|
|
198
|
+
See the fallback section above for a `NoRouteMatchError` catch example.
|
|
199
|
+
|
|
200
|
+
## Nesting Host Routers
|
|
201
|
+
|
|
202
|
+
A lazy handler can resolve to another `HostRouter`:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// apps/regional.ts
|
|
206
|
+
import { createHostRouter } from "@rangojs/router/host";
|
|
207
|
+
|
|
208
|
+
const regional = createHostRouter();
|
|
209
|
+
regional.host(["us.*"]).map(() => import("./regions/us"));
|
|
210
|
+
regional.host(["eu.*"]).map(() => import("./regions/eu"));
|
|
211
|
+
|
|
212
|
+
export default regional;
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// host-router.ts
|
|
217
|
+
router.host(["**.regional.example.com"]).map(() => import("./apps/regional"));
|
|
218
|
+
```
|
|
@@ -8,6 +8,9 @@ argument-hint: [@slot-name] [route-to-intercept]
|
|
|
8
8
|
|
|
9
9
|
Intercept routes render a different component during soft navigation (client-side) while preserving the background route. Hard navigation (direct URL) shows the full page.
|
|
10
10
|
|
|
11
|
+
Canonical semantics reference:
|
|
12
|
+
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
13
|
+
|
|
11
14
|
## Basic Intercept
|
|
12
15
|
|
|
13
16
|
```typescript
|
|
@@ -68,6 +71,78 @@ intercept(
|
|
|
68
71
|
)
|
|
69
72
|
```
|
|
70
73
|
|
|
74
|
+
## Intercept Middleware
|
|
75
|
+
|
|
76
|
+
Intercepts support their own middleware chain via the use callback. The full chain for an intercept request is:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
global mw (router.use) -> route mw (urls middleware()) -> intercept mw -> intercept handler -> intercept loaders
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
intercept(
|
|
84
|
+
"@modal",
|
|
85
|
+
"product",
|
|
86
|
+
<ProductModal />,
|
|
87
|
+
() => [
|
|
88
|
+
middleware(async (ctx, next) => {
|
|
89
|
+
// Runs only for this intercept, after global and route middleware
|
|
90
|
+
ctx.set("interceptSource", "modal");
|
|
91
|
+
await next();
|
|
92
|
+
}),
|
|
93
|
+
loader(ProductLoader),
|
|
94
|
+
]
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The intercept handler can read context variables set by all upstream middleware layers (global, route, and intercept-specific).
|
|
99
|
+
|
|
100
|
+
Handler/layout `ctx.set()` data follows the same rule as elsewhere:
|
|
101
|
+
intercepts see data produced in the current render pass, but partial
|
|
102
|
+
action revalidation only recomputes segments that actually revalidate.
|
|
103
|
+
If an intercept depends on data established by an outer layout/handler,
|
|
104
|
+
revalidate that outer segment too or reload/guard the data inside the
|
|
105
|
+
intercept.
|
|
106
|
+
|
|
107
|
+
### Revalidation Contracts for Intercept Dependencies
|
|
108
|
+
|
|
109
|
+
Use named revalidation contracts on both the outer producer and the intercept
|
|
110
|
+
consumer when they share `ctx.set()` data:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
export const revalidateProductShell = ({ actionId }) =>
|
|
114
|
+
actionId?.includes("src/actions/product.ts#") ?? false;
|
|
115
|
+
|
|
116
|
+
layout(ProductLayout, () => [
|
|
117
|
+
revalidate(revalidateProductShell), // producer reruns
|
|
118
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
119
|
+
revalidate(revalidateProductShell), // consumer reruns
|
|
120
|
+
loader(ProductLoader),
|
|
121
|
+
]),
|
|
122
|
+
]);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Compose multiple contracts if the intercept depends on multiple upstream
|
|
126
|
+
domains.
|
|
127
|
+
|
|
128
|
+
Helper handoff style keeps intercept trees terse:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { revalidate } from "@rangojs/router";
|
|
132
|
+
|
|
133
|
+
export const revalidateProduct = () => [
|
|
134
|
+
revalidate(revalidateProductShell),
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
layout(ProductLayout, () => [
|
|
138
|
+
revalidateProduct(),
|
|
139
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
140
|
+
revalidateProduct(),
|
|
141
|
+
loader(ProductLoader),
|
|
142
|
+
]),
|
|
143
|
+
]);
|
|
144
|
+
```
|
|
145
|
+
|
|
71
146
|
## Conditional Intercept with when()
|
|
72
147
|
|
|
73
148
|
Only intercept based on navigation context:
|
|
@@ -166,6 +241,10 @@ Runtime behavior:
|
|
|
166
241
|
Loaders inside the intercept always run fresh at request time, same as regular
|
|
167
242
|
pre-rendered routes.
|
|
168
243
|
|
|
244
|
+
During action-driven partial revalidation, this same partial rule applies:
|
|
245
|
+
refreshing the intercept does not implicitly rebuild non-revalidated outer
|
|
246
|
+
segments.
|
|
247
|
+
|
|
169
248
|
## Complete Example
|
|
170
249
|
|
|
171
250
|
```typescript
|
package/skills/layout/SKILL.md
CHANGED
|
@@ -8,6 +8,9 @@ argument-hint: [component]
|
|
|
8
8
|
|
|
9
9
|
Layouts wrap child routes and persist during navigation within their scope.
|
|
10
10
|
|
|
11
|
+
Canonical semantics reference:
|
|
12
|
+
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
13
|
+
|
|
11
14
|
## Basic Layout
|
|
12
15
|
|
|
13
16
|
```typescript
|
|
@@ -145,6 +148,13 @@ A layout as a child of `path()` wraps the route content and can read
|
|
|
145
148
|
data set by the route handler via `ctx.get()`. The handler always
|
|
146
149
|
executes before its children.
|
|
147
150
|
|
|
151
|
+
This handler-first guarantee applies to a single full render pass
|
|
152
|
+
(initial render, prerender, or full HTML re-render). During partial
|
|
153
|
+
action revalidation, only the segments that revalidate are recomputed.
|
|
154
|
+
If an orphan layout depends on data established by an outer handler or
|
|
155
|
+
layout, that outer segment must also revalidate, or the orphan must
|
|
156
|
+
guard/reload the data independently.
|
|
157
|
+
|
|
148
158
|
```typescript
|
|
149
159
|
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
150
160
|
|
|
@@ -175,8 +185,10 @@ urls(({ path, layout, parallel }) => [
|
|
|
175
185
|
])
|
|
176
186
|
```
|
|
177
187
|
|
|
178
|
-
Orphan layouts
|
|
179
|
-
|
|
188
|
+
Orphan layouts can call `ctx.get()` to read data set by their parent
|
|
189
|
+
handler. They can also call `ctx.set()`, though the primary pattern is
|
|
190
|
+
for route handlers and middleware to write context variables and for
|
|
191
|
+
orphan layouts to read them.
|
|
180
192
|
|
|
181
193
|
## Layout Revalidation
|
|
182
194
|
|
|
@@ -198,6 +210,54 @@ layout(<CartLayout />, () => [
|
|
|
198
210
|
])
|
|
199
211
|
```
|
|
200
212
|
|
|
213
|
+
If child segments read data that was established by this layout or by a
|
|
214
|
+
route handler above them, revalidate the outer segment too. Partial
|
|
215
|
+
revalidation does not re-run non-revalidated ancestors just to rebuild
|
|
216
|
+
their `ctx.set()` state.
|
|
217
|
+
|
|
218
|
+
### Revalidation Contracts
|
|
219
|
+
|
|
220
|
+
For shared upstream data, define named revalidation functions and reuse
|
|
221
|
+
them on both producer and consumer segments:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// revalidation-contracts.ts
|
|
225
|
+
export const revalidateCartData = ({ actionId }) =>
|
|
226
|
+
actionId?.includes("src/actions/cart.ts#addToCart") ?? false;
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
layout(<CartLayout />, () => [
|
|
231
|
+
revalidate(revalidateCartData), // producer
|
|
232
|
+
path("/cart", CartPage, { name: "cart" }, () => [
|
|
233
|
+
revalidate(revalidateCartData), // consumer
|
|
234
|
+
]),
|
|
235
|
+
]);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
If a segment depends on multiple upstream domains, compose multiple
|
|
239
|
+
contracts (`revalidateAuthData`, `revalidateCartData`, and so on).
|
|
240
|
+
|
|
241
|
+
You can also package them as importable handoff helpers:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// revalidation-contracts.ts
|
|
245
|
+
import { revalidate } from "@rangojs/router";
|
|
246
|
+
|
|
247
|
+
export const revalidateAuthData = ({ actionId }) =>
|
|
248
|
+
actionId?.includes("src/actions/auth.ts#") ?? false;
|
|
249
|
+
export const revalidateAuth = () => [revalidate(revalidateAuthData)];
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
layout(<ShellLayout />, () => [
|
|
254
|
+
revalidateAuth(),
|
|
255
|
+
path("/account", AccountPage, { name: "account" }, () => [
|
|
256
|
+
revalidateAuth(),
|
|
257
|
+
]),
|
|
258
|
+
]);
|
|
259
|
+
```
|
|
260
|
+
|
|
201
261
|
## Complete Example
|
|
202
262
|
|
|
203
263
|
```typescript
|