@rangojs/router 0.0.0-experimental.b02a2fec → 0.0.0-experimental.bf1b128c
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 +50 -20
- package/dist/vite/index.js +1338 -462
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +7 -5
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +28 -20
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +88 -16
- package/skills/loader/SKILL.md +66 -2
- package/skills/middleware/SKILL.md +32 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +765 -0
- package/skills/parallel/SKILL.md +66 -0
- package/skills/rango/SKILL.md +24 -22
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +24 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +3 -1
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/navigation-bridge.ts +71 -5
- package/src/browser/navigation-client.ts +64 -13
- package/src/browser/navigation-store.ts +25 -1
- package/src/browser/partial-update.ts +34 -3
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +148 -16
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +50 -11
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +11 -1
- package/src/browser/react/use-router.ts +8 -1
- package/src/browser/rsc-router.tsx +34 -6
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/types.ts +13 -0
- package/src/build/route-trie.ts +50 -24
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/client.tsx +82 -174
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +40 -9
- package/src/outlet-context.ts +1 -1
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +7 -3
- package/src/route-definition/dsl-helpers.ts +175 -23
- package/src/route-definition/helpers-types.ts +63 -14
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-types.ts +7 -0
- package/src/router/handler-context.ts +24 -4
- package/src/router/lazy-includes.ts +6 -6
- package/src/router/loader-resolution.ts +3 -0
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +3 -3
- package/src/router/middleware-types.ts +2 -22
- package/src/router/middleware.ts +54 -7
- package/src/router/pattern-matching.ts +60 -9
- package/src/router/revalidation.ts +15 -1
- package/src/router/segment-resolution/revalidation.ts +63 -58
- package/src/router/trie-matching.ts +10 -4
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +1 -2
- package/src/rsc/handler.ts +8 -4
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/progressive-enhancement.ts +2 -0
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +7 -0
- package/src/rsc/server-action.ts +2 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +11 -61
- package/src/server/context.ts +26 -3
- package/src/server/request-context.ts +10 -42
- package/src/types/handler-context.ts +12 -39
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +0 -1
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +30 -4
- package/src/urls/response-types.ts +2 -10
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +31 -3
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +48 -1
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/plugins/cjs-to-esm.ts +5 -0
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +16 -4
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +52 -28
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +516 -486
- package/src/vite/plugins/performance-tracks.ts +17 -9
- package/src/vite/plugins/use-cache-transform.ts +56 -43
- package/src/vite/plugins/version-injector.ts +37 -11
- package/src/vite/rango.ts +49 -14
- package/src/vite/router-discovery.ts +558 -53
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +20 -6
package/skills/links/SKILL.md
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: links
|
|
3
|
-
description: URL generation with ctx.reverse (server), href (client), useHref (mounted), useMount, and scopedReverse
|
|
4
|
-
argument-hint: [href|useHref|useMount|scopedReverse]
|
|
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]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Links & URL Generation
|
|
8
8
|
|
|
9
9
|
@rangojs/router provides different href APIs for server and client contexts.
|
|
10
10
|
|
|
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
|
+
|
|
13
|
+
**`reverse()` is server-only.** It depends on the route manifest and handler context, neither of which are available in the browser. Client components receive URLs as props, loader data, or server-action return values — they never call `reverse` directly.
|
|
14
|
+
|
|
11
15
|
## Server: ctx.reverse()
|
|
12
16
|
|
|
13
|
-
Available in route handlers via HandlerContext. Resolves named routes using the full route map.
|
|
17
|
+
Available in route handlers via HandlerContext. Resolves named routes using the full route map. This is the default way to generate URLs on the server.
|
|
14
18
|
|
|
15
19
|
```typescript
|
|
16
20
|
import { urls, scopedReverse } from "@rangojs/router";
|
|
@@ -103,7 +107,7 @@ path("/search", (ctx) => {
|
|
|
103
107
|
|
|
104
108
|
### scopedReverse() - type-safe ctx.reverse
|
|
105
109
|
|
|
106
|
-
Wraps `ctx.reverse` with local route type information for autocomplete and validation:
|
|
110
|
+
Wraps `ctx.reverse` with local route type information for autocomplete and validation. Runtime behavior is identical to `ctx.reverse` — `scopedReverse` is a type-only cast. The same dot-prefix rule applies: local names use `.name`, global names use `name.sub`.
|
|
107
111
|
|
|
108
112
|
```typescript
|
|
109
113
|
import { scopedReverse } from "@rangojs/router";
|
|
@@ -111,18 +115,83 @@ import { scopedReverse } from "@rangojs/router";
|
|
|
111
115
|
path("/product/:slug", (ctx) => {
|
|
112
116
|
const reverse = scopedReverse<typeof shopPatterns>(ctx.reverse);
|
|
113
117
|
|
|
114
|
-
reverse("cart"); //
|
|
115
|
-
reverse("product", { slug: "widget" }); //
|
|
116
|
-
reverse("blog.post");
|
|
117
|
-
reverse("/about"); // Path-based always allowed
|
|
118
|
+
reverse(".cart"); // Local name (dot-prefixed) — resolves in include scope
|
|
119
|
+
reverse(".product", { slug: "widget" }); // Local name with params
|
|
120
|
+
reverse("blog.post", { slug: "hi" }); // Global name (dotted) — full route map
|
|
118
121
|
|
|
119
122
|
return <ProductPage slug={ctx.params.slug} />;
|
|
120
123
|
}, { name: "product" })
|
|
121
124
|
```
|
|
122
125
|
|
|
126
|
+
`reverse()` does not accept raw path strings (`"/about"`). For static paths in client components, use `href("/about")`; on the server, look up the route by name.
|
|
127
|
+
|
|
128
|
+
## Client components: receive URLs as props
|
|
129
|
+
|
|
130
|
+
`reverse()` is not available inside `"use client"` modules — there is no handler context and no route manifest in the browser bundle. Generate the URL on the server and hand it to the client component.
|
|
131
|
+
|
|
132
|
+
Three patterns, in order of preference:
|
|
133
|
+
|
|
134
|
+
1. Pass as a prop from a server component:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
// server
|
|
138
|
+
function BlogPostPage(ctx: HandlerContext) {
|
|
139
|
+
return <ShareButton url={ctx.reverse(".post", { slug: ctx.params.slug })} />;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
"use client";
|
|
145
|
+
|
|
146
|
+
export function ShareButton({ url }: { url: string }) {
|
|
147
|
+
return (
|
|
148
|
+
<button onClick={() => navigator.clipboard.writeText(url)}>Share</button>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
2. Return from a loader (attached to the route via the DSL):
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
// server — loaders/nav.ts
|
|
157
|
+
export const NavLoader = createLoader((ctx) => ({
|
|
158
|
+
home: ctx.reverse("home"),
|
|
159
|
+
blog: ctx.reverse("blog.index"),
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
// server — urls.tsx: attach the loader so useLoader has data in context
|
|
163
|
+
const urlpatterns = urls(({ path, loader }) => [
|
|
164
|
+
path("/", HomePage, { name: "home" }, () => [loader(NavLoader)]),
|
|
165
|
+
]);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
"use client";
|
|
170
|
+
|
|
171
|
+
function Nav() {
|
|
172
|
+
const { data } = useLoader(NavLoader);
|
|
173
|
+
return <Link to={data.home}>Home</Link>;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`useLoader()` requires the loader to be attached to an active route. If you need on-demand fetching instead, use `useFetchLoader()`.
|
|
178
|
+
|
|
179
|
+
3. Return from a server action:
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
"use server";
|
|
183
|
+
|
|
184
|
+
export async function getProductUrl(slug: string) {
|
|
185
|
+
const ctx = getRequestContext();
|
|
186
|
+
return ctx.reverse("product", { slug });
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
For static path strings (not named routes), client components can use `href()` — see below.
|
|
191
|
+
|
|
123
192
|
## Client: href()
|
|
124
193
|
|
|
125
|
-
Plain function for absolute path-based URLs. No hook needed - works anywhere.
|
|
194
|
+
Plain function for absolute path-based URLs. No hook needed - works anywhere in client components. `href()` validates paths at compile time, but does **not** resolve named routes — for named routes, use one of the patterns above.
|
|
126
195
|
|
|
127
196
|
```typescript
|
|
128
197
|
"use client";
|
|
@@ -187,13 +256,16 @@ function MountInfo() {
|
|
|
187
256
|
|
|
188
257
|
## When to use what
|
|
189
258
|
|
|
190
|
-
| Context | API
|
|
191
|
-
| ---------------- |
|
|
192
|
-
| Server handler | `ctx.reverse("name")`
|
|
193
|
-
| Server handler | `scopedReverse<T>(ctx.reverse)`
|
|
194
|
-
| Client component |
|
|
195
|
-
| Client component | `
|
|
196
|
-
| Client component | `
|
|
259
|
+
| Context | API | Resolves | Use for |
|
|
260
|
+
| ---------------- | -------------------------------------------------- | ------------------------------- | ---------------------------------------------------------------- |
|
|
261
|
+
| Server handler | `ctx.reverse("name")` | Named routes (local + absolute) | **Default** server-side URL generation |
|
|
262
|
+
| Server handler | `scopedReverse<T>(ctx.reverse)` | Same, with type safety | Type-safe server URLs |
|
|
263
|
+
| Client component | (URL passed as prop / loader data / action return) | Named routes | Any URL derived from a named route — generate on server, pass in |
|
|
264
|
+
| Client component | `href("/path")` | Absolute paths (static strings) | Static navigation where no named-route lookup is needed |
|
|
265
|
+
| Client component | `useHref()` | Mount-prefixed paths | Local navigation inside `include()` |
|
|
266
|
+
| Client component | `useMount()` | Raw mount path | Custom mount-aware logic |
|
|
267
|
+
|
|
268
|
+
> `reverse()` is server-only. Client components never import or call it — they receive the already-resolved string.
|
|
197
269
|
|
|
198
270
|
## Complete example: mounted module
|
|
199
271
|
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -139,7 +139,29 @@ same memoized result — loaders never run twice per request.
|
|
|
139
139
|
|
|
140
140
|
## Loader Context
|
|
141
141
|
|
|
142
|
-
Loaders receive the same context as route handlers
|
|
142
|
+
Loaders receive the same context shape as route handlers.
|
|
143
|
+
|
|
144
|
+
### Full field surface
|
|
145
|
+
|
|
146
|
+
| Field | Type | Notes |
|
|
147
|
+
| -------------- | ------------------------------ | --------------------------------------------------------------------------------------------------- |
|
|
148
|
+
| `params` | `TParams` | Merged route + explicit loader params; overridable by fetchable `load({ params })`. |
|
|
149
|
+
| `routeParams` | `Record<string, string>` | Server-trusted route params from URL pattern matching; cannot be overridden. |
|
|
150
|
+
| `request` | `Request` | The incoming `Request` (headers, method, body, `signal` for abort). |
|
|
151
|
+
| `url` | `URL` | Parsed request URL. |
|
|
152
|
+
| `pathname` | `string` | URL pathname (shortcut for `ctx.url.pathname`). |
|
|
153
|
+
| `searchParams` | `URLSearchParams` | Shortcut for `ctx.url.searchParams`. |
|
|
154
|
+
| `search` | `ResolveSearchSchema<TSearch>` | Typed query params when a search schema is declared on the route; `{}` otherwise. |
|
|
155
|
+
| `env` | `TEnv` | Plain bindings from `createRouter<TEnv>()` (DB, KV, secrets, etc.). |
|
|
156
|
+
| `get` | `(key \| ContextVar) => value` | Reads variables/context-vars set by middleware. |
|
|
157
|
+
| `use` | `(loader \| handle) => T` | Access another loader's data (Promise) or a handle's collected data (after `await ctx.rendered()`). |
|
|
158
|
+
| `rendered` | `() => Promise<void>` | **Experimental.** DSL loaders only — waits for non-loader segments before reading handle data. |
|
|
159
|
+
| `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders. |
|
|
160
|
+
| `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls. |
|
|
161
|
+
| `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission. |
|
|
162
|
+
| `reverse` | `ScopedReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers). |
|
|
163
|
+
|
|
164
|
+
### Example
|
|
143
165
|
|
|
144
166
|
```typescript
|
|
145
167
|
export const ProductLoader = createLoader(async (ctx) => {
|
|
@@ -163,10 +185,21 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
163
185
|
// Variables set by middleware (from RSCRouter.Vars augmentation)
|
|
164
186
|
const user = ctx.get("user");
|
|
165
187
|
|
|
166
|
-
|
|
188
|
+
// Type-checked URLs for payloads. `.name` resolves within the current
|
|
189
|
+
// include() scope; a bare `name` resolves globally. See /route and
|
|
190
|
+
// /typesafety for scope rules and route-name autocomplete.
|
|
191
|
+
const detailUrl = ctx.reverse(".detail", { slug });
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
product: await fetchProduct(slug),
|
|
195
|
+
links: { self: detailUrl },
|
|
196
|
+
};
|
|
167
197
|
});
|
|
168
198
|
```
|
|
169
199
|
|
|
200
|
+
See `/route` for the full handler-context contract (shared with loaders) and
|
|
201
|
+
`/typesafety` for route-name typing that powers `ctx.reverse` autocomplete.
|
|
202
|
+
|
|
170
203
|
### params vs routeParams
|
|
171
204
|
|
|
172
205
|
- `ctx.params` — merged route params + explicit loader params. For fetchable
|
|
@@ -215,6 +248,37 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
215
248
|
]);
|
|
216
249
|
```
|
|
217
250
|
|
|
251
|
+
### `revalidate()` return shapes
|
|
252
|
+
|
|
253
|
+
A `revalidate(fn)` callback can return one of four shapes. The chain
|
|
254
|
+
processes revalidators in order; each call's return controls how the
|
|
255
|
+
chain continues:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// 1) Hard decision — short-circuits the chain, used as the final answer.
|
|
259
|
+
revalidate(() => true);
|
|
260
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
|
|
261
|
+
|
|
262
|
+
// 2) Soft decision — updates the running suggestion for downstream
|
|
263
|
+
// revalidators on the same segment, chain continues.
|
|
264
|
+
revalidate(({ defaultShouldRevalidate }) => ({
|
|
265
|
+
defaultShouldRevalidate: !defaultShouldRevalidate,
|
|
266
|
+
}));
|
|
267
|
+
|
|
268
|
+
// 3) Defer (no opinion) — leaves the running suggestion unchanged and
|
|
269
|
+
// continues to the next revalidator. Implicit return / null /
|
|
270
|
+
// undefined are all equivalent and consumer-friendly.
|
|
271
|
+
revalidate(({ actionId }) => {
|
|
272
|
+
if (actionId?.includes("Cart")) return true; // hard for this branch only
|
|
273
|
+
// implicit return — let downstream revalidators or the segment default decide
|
|
274
|
+
});
|
|
275
|
+
revalidate(() => undefined); // explicit defer
|
|
276
|
+
revalidate(() => null); // explicit defer
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
If every revalidator on a segment defers, the segment-type default
|
|
280
|
+
(e.g. params-changed for routes, `false` for parallels) is used.
|
|
281
|
+
|
|
218
282
|
### Revalidation Contracts for Loader Dependencies
|
|
219
283
|
|
|
220
284
|
If a loader reads `ctx.get()` data produced by an outer handler/layout, share
|
|
@@ -137,17 +137,46 @@ export const urlpatterns = urls(({ path, layout, middleware }) => [
|
|
|
137
137
|
## Middleware with Multiple Handlers
|
|
138
138
|
|
|
139
139
|
```typescript
|
|
140
|
-
//
|
|
140
|
+
// Group multiple middleware in an array
|
|
141
141
|
export const shopMiddleware = [loggerMiddleware, mockAuthMiddleware];
|
|
142
142
|
|
|
143
|
-
// In routes
|
|
143
|
+
// In routes — pass the array directly
|
|
144
144
|
layout(<ShopLayout />, () => [
|
|
145
|
-
middleware(
|
|
145
|
+
middleware(shopMiddleware),
|
|
146
146
|
|
|
147
147
|
path("/shop", ShopIndex, { name: "shop" }),
|
|
148
148
|
])
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
+
## Wrapping Middleware (Scoped to Children)
|
|
152
|
+
|
|
153
|
+
Use the wrapping form to scope middleware to a subset of routes without
|
|
154
|
+
introducing a visible layout:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
urls(({ path, middleware }) => [
|
|
158
|
+
// authMw only applies to /admin and /admin/settings
|
|
159
|
+
middleware(authMw, () => [
|
|
160
|
+
path("/admin", AdminPage, { name: "admin" }),
|
|
161
|
+
path("/admin/settings", SettingsPage, { name: "settings" }),
|
|
162
|
+
]),
|
|
163
|
+
|
|
164
|
+
// Public route — no authMw
|
|
165
|
+
path("/", HomePage, { name: "home" }),
|
|
166
|
+
]);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Multiple middleware with wrapping:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
middleware([authMw, loggingMw], () => [
|
|
173
|
+
path("/admin", AdminPage, { name: "admin" }),
|
|
174
|
+
]);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
This creates a transparent layout (`<Outlet />`) that carries the middleware.
|
|
178
|
+
The middleware does not affect sibling routes outside the callback.
|
|
179
|
+
|
|
151
180
|
## Middleware Context
|
|
152
181
|
|
|
153
182
|
```typescript
|