@rangojs/router 0.0.0-experimental.d20dd405 → 0.0.0-experimental.dacec167
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 +8 -8
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +914 -485
- package/package.json +55 -11
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/hooks/SKILL.md +196 -21
- package/skills/host-router/SKILL.md +45 -20
- package/skills/intercept/SKILL.md +1 -4
- package/skills/layout/SKILL.md +4 -7
- package/skills/links/SKILL.md +22 -10
- package/skills/loader/SKILL.md +149 -6
- package/skills/middleware/SKILL.md +13 -9
- package/skills/migrate-nextjs/SKILL.md +1 -1
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +3 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +58 -9
- package/skills/route/SKILL.md +9 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +53 -41
- package/skills/testing/SKILL.md +778 -0
- package/skills/typesafety/SKILL.md +310 -26
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +85 -3
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/event-controller.ts +42 -66
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +9 -67
- package/src/browser/navigation-client.ts +12 -15
- package/src/browser/navigation-store.ts +7 -8
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +8 -16
- package/src/browser/react/NavigationProvider.tsx +55 -65
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-params.ts +3 -4
- package/src/browser/react/use-reverse.ts +19 -12
- package/src/browser/react/use-router.ts +14 -1
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +30 -16
- package/src/browser/scroll-restoration.ts +30 -25
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +2 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +49 -6
- package/src/client.tsx +5 -7
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +136 -19
- package/src/index.rsc.ts +6 -4
- package/src/index.ts +13 -6
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +21 -6
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +16 -13
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +238 -263
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +37 -14
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +2 -2
- package/src/router/loader-resolution.ts +16 -2
- package/src/router/match-handlers.ts +62 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +32 -30
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +1 -1
- package/src/router/middleware.ts +46 -78
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +43 -1
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +19 -6
- package/src/router/segment-resolution/revalidation.ts +19 -6
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/types.ts +8 -0
- package/src/router.ts +37 -21
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +20 -65
- package/src/rsc/helpers.ts +22 -2
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/response-route-handler.ts +32 -52
- package/src/rsc/rsc-rendering.ts +27 -53
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +13 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +2 -2
- package/src/search-params.ts +4 -4
- package/src/segment-system.tsx +64 -49
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +118 -51
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +10 -0
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +320 -0
- package/src/testing/flight.entry.ts +39 -0
- package/src/testing/flight.ts +197 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +331 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/render-route.tsx +565 -0
- package/src/testing/run-loader.ts +341 -0
- package/src/testing/run-middleware.ts +188 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +270 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +56 -11
- package/src/types/index.ts +1 -0
- package/src/types/segments.ts +18 -1
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +11 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +70 -48
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +19 -25
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +3 -7
- package/src/vite/plugins/client-ref-hashing.ts +12 -1
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
- package/src/vite/plugins/expose-action-id.ts +2 -2
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-internal-ids.ts +47 -67
- package/src/vite/plugins/performance-tracks.ts +12 -16
- package/src/vite/plugins/use-cache-transform.ts +13 -11
- package/src/vite/plugins/version-injector.ts +2 -12
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +67 -15
- package/src/vite/router-discovery.ts +208 -63
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
package/skills/hooks/SKILL.md
CHANGED
|
@@ -190,6 +190,141 @@ function SearchResults() {
|
|
|
190
190
|
}
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
+
**Shared refetch behavior**:
|
|
194
|
+
|
|
195
|
+
When the loader is registered on the route via `loader()`, a plain
|
|
196
|
+
`load()` call (no options, or a trivially-defaulted GET with no
|
|
197
|
+
`params` and no `body`) broadcasts its result to every component
|
|
198
|
+
reading the same loader id. Layout, page, and parallel-slot reads
|
|
199
|
+
all converge on the new value:
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
// Layout button calls load() — the page read below sees the update too.
|
|
203
|
+
function Layout() {
|
|
204
|
+
const { data, load } = useLoader(CartLoader);
|
|
205
|
+
return <button onClick={() => load()}>Refresh ({data.count})</button>;
|
|
206
|
+
}
|
|
207
|
+
function Page() {
|
|
208
|
+
const { data } = useLoader(CartLoader); // updates with the layout's load()
|
|
209
|
+
return <span>{data.count} items</span>;
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`isLoading` and `error` follow the same scope. `throwOnError: true`
|
|
214
|
+
render-throws are scoped to the **originating** hook — sibling readers
|
|
215
|
+
see the error in their `error` state but their boundaries are not
|
|
216
|
+
triggered by someone else's failure. A successful follow-up `load()`
|
|
217
|
+
clears the shared error.
|
|
218
|
+
|
|
219
|
+
**`load()` calls that stay local** (no broadcast, per-hook state, same
|
|
220
|
+
semantics as the old per-component `useState`):
|
|
221
|
+
|
|
222
|
+
- `load({ params: { ... } })` — explicit params.
|
|
223
|
+
- `load({ method: "POST", body })` — mutations.
|
|
224
|
+
- Any `load()` on a `useFetchLoader(loader)` whose loader is **not**
|
|
225
|
+
registered on the current route. Two unrelated components calling
|
|
226
|
+
`load()` on the same fetchable-but-unregistered loader keep
|
|
227
|
+
independent results.
|
|
228
|
+
|
|
229
|
+
So the search/list pattern still works — two components calling
|
|
230
|
+
`load({ params: { q } })` with different `q` values each keep their
|
|
231
|
+
own result; they do not collapse to last-write-wins through a shared
|
|
232
|
+
store.
|
|
233
|
+
|
|
234
|
+
**Scoping refetch with a `key`**:
|
|
235
|
+
|
|
236
|
+
Pass a `key` to partition the shared refresh store. Only hooks using the
|
|
237
|
+
**same** `key` refresh together when one of them calls `load()`. This is a
|
|
238
|
+
client-side refresh identity only — it never changes the request sent to the
|
|
239
|
+
server, and is unrelated to the server `cache({ key })` option and to
|
|
240
|
+
`revalidate()`.
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
// Two independent dashboards using the same loader. Without a key, one
|
|
244
|
+
// dashboard's load() would flip the other's spinner and value. With a key,
|
|
245
|
+
// they refresh independently.
|
|
246
|
+
function Dashboard({ id }: { id: string }) {
|
|
247
|
+
const { data, load } = useLoader(StatsLoader, { key: `dashboard:${id}` });
|
|
248
|
+
return <button onClick={() => load()}>Refresh {data.total}</button>;
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The `key` widens sharing in two ways the default cannot:
|
|
253
|
+
|
|
254
|
+
- **Parameterized GETs share.** `useFetchLoader(SearchLoader, { key: q })`
|
|
255
|
+
with the same `q` in two components share one result and refresh together —
|
|
256
|
+
a keyed `load({ params: { q } })` broadcasts to the group instead of staying
|
|
257
|
+
local. (Mutations — non-GET or `body` — stay local even with a key.)
|
|
258
|
+
- **Unregistered loaders share.** A `key` makes `useFetchLoader` of a loader
|
|
259
|
+
that is **not** registered on the route share too, letting unrelated
|
|
260
|
+
components opt into a common refresh group.
|
|
261
|
+
|
|
262
|
+
Lifecycle: a keyed read of an unregistered loader is reference-counted — its
|
|
263
|
+
shared value lives as long as at least one component using that key is mounted.
|
|
264
|
+
A persistent component (e.g. a header) keeps the value across navigations; a
|
|
265
|
+
route-scoped component's value is reclaimed when it unmounts. Registered-loader
|
|
266
|
+
reads (keyed or not) reset on navigation from fresh route data, as before.
|
|
267
|
+
|
|
268
|
+
**Refreshing multiple loaders together (`refreshGroup` + `useRefreshLoaders`)**:
|
|
269
|
+
|
|
270
|
+
`key` groups readers of one loader. To refresh **different** loaders together,
|
|
271
|
+
tag them with a shared `refreshGroup` name and trigger them with
|
|
272
|
+
`useRefreshLoaders()`. The hook takes no argument; you pass the group(s) to the
|
|
273
|
+
function it returns, so one `useRefreshLoaders()` can refresh different groups
|
|
274
|
+
depending on context. A read may carry **several** tags — pass an array — and is
|
|
275
|
+
refreshed when **any** of its groups is refreshed:
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
function Profile() {
|
|
279
|
+
const { data } = useLoader(ProfileLoader, {
|
|
280
|
+
key: userId,
|
|
281
|
+
refreshGroup: "account",
|
|
282
|
+
});
|
|
283
|
+
return <span>{data.name}</span>;
|
|
284
|
+
}
|
|
285
|
+
function Orders() {
|
|
286
|
+
// Tagged into two groups: refreshed by "account" (the whole set) or the
|
|
287
|
+
// finer "orders" tag.
|
|
288
|
+
const { data } = useLoader(OrdersLoader, {
|
|
289
|
+
key: userId,
|
|
290
|
+
refreshGroup: ["account", "orders"],
|
|
291
|
+
});
|
|
292
|
+
return <span>{data.count} orders</span>;
|
|
293
|
+
}
|
|
294
|
+
function RefreshButtons() {
|
|
295
|
+
const refresh = useRefreshLoaders();
|
|
296
|
+
return (
|
|
297
|
+
<>
|
|
298
|
+
<button onClick={() => refresh("account")}>Refresh account</button>
|
|
299
|
+
<button onClick={() => refresh("orders")}>Refresh orders only</button>
|
|
300
|
+
<button onClick={() => refresh(["account", "orders"])}>
|
|
301
|
+
Refresh both
|
|
302
|
+
</button>
|
|
303
|
+
</>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`refresh(groups)` accepts one name or an array and re-runs every currently-mounted
|
|
309
|
+
member tagged with **any** of them, with a **plain GET** against the current route
|
|
310
|
+
URL — no params, no body, no mutation methods, because a group spans loaders with
|
|
311
|
+
different shapes. A member that sits in two of the requested groups is fetched
|
|
312
|
+
once (members are unioned and deduped by read). It returns a promise that resolves
|
|
313
|
+
when all members settle and **rejects with an `AggregateError`** if any fail;
|
|
314
|
+
group refresh never render-throws, so handle failures at the await site
|
|
315
|
+
(`await refresh("account").catch(...)`). Each failing member also exposes its
|
|
316
|
+
error via its own read's `error`.
|
|
317
|
+
|
|
318
|
+
Multiple tags give you granular vs. whole-set refresh from one place: a coarse
|
|
319
|
+
tag (`"account"`) covers everything, while a finer tag (`"orders"`) targets a
|
|
320
|
+
subset. Sharing within a group is opt-in via `key`: members that share a `key`
|
|
321
|
+
share one value (and one fetch); a grouped reader **without** a `key` gets its own
|
|
322
|
+
private bucket, so a group refresh updates only that read and never leaks into
|
|
323
|
+
unrelated unkeyed reads of the same loader. A bucket may belong to several groups
|
|
324
|
+
at once (one read tagged with multiple names, or different reads tagging the same
|
|
325
|
+
keyed bucket with different names). Keep parameterized loaders on the single-loader
|
|
326
|
+
`key` — a plain-GET group refresh sends no params.
|
|
327
|
+
|
|
193
328
|
**Load options**:
|
|
194
329
|
|
|
195
330
|
```tsx
|
|
@@ -511,6 +646,43 @@ const flash = FlashMessage.read();
|
|
|
511
646
|
const product = ProductState.read();
|
|
512
647
|
```
|
|
513
648
|
|
|
649
|
+
> **Hydration:** `.read()` returns `undefined` on the server but may return
|
|
650
|
+
> a real value on the first client render (history state survives reload).
|
|
651
|
+
> Do not call `.read()` directly during the initial render of a component;
|
|
652
|
+
> call it from an event handler or inside a `useEffect` post-mount. For
|
|
653
|
+
> reactive hydration-safe access, use `useLocationState()` instead.
|
|
654
|
+
|
|
655
|
+
### .write() / .delete() (static, non-reactive)
|
|
656
|
+
|
|
657
|
+
Static counterparts to `.read()`. Both mutate the current history entry's
|
|
658
|
+
`history.state` via `replaceState`, preserving any other keys (router
|
|
659
|
+
bookkeeping, other location state slots). Both are client-only; they throw
|
|
660
|
+
when called on the server.
|
|
661
|
+
|
|
662
|
+
Neither dispatches an event, so components reading via `useLocationState`
|
|
663
|
+
will NOT re-render until the next navigation/popstate. Pair with `.read()`
|
|
664
|
+
(or a fresh mount via back/forward/reload) instead.
|
|
665
|
+
|
|
666
|
+
```tsx
|
|
667
|
+
"use client";
|
|
668
|
+
import { ProductState } from "./state";
|
|
669
|
+
|
|
670
|
+
// Persisted across hard refresh and back/forward of this entry.
|
|
671
|
+
ProductState.write({ name: "Widget", price: 9.99 });
|
|
672
|
+
|
|
673
|
+
// Read later (or on next mount).
|
|
674
|
+
const current = ProductState.read();
|
|
675
|
+
|
|
676
|
+
// Manually clear the slot. Idempotent if it isn't set.
|
|
677
|
+
ProductState.delete();
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
| Method | Updates `history.state` | Fires `useLocationState` rerender | SSR behavior |
|
|
681
|
+
| ----------- | ----------------------- | --------------------------------- | ------------------- |
|
|
682
|
+
| `.read()` | no | n/a (returns snapshot) | returns `undefined` |
|
|
683
|
+
| `.write()` | yes (replace this slot) | no | throws |
|
|
684
|
+
| `.delete()` | yes (remove this slot) | no | throws |
|
|
685
|
+
|
|
514
686
|
## Cache Hooks
|
|
515
687
|
|
|
516
688
|
### useClientCache()
|
|
@@ -696,7 +868,9 @@ function MountInfo() {
|
|
|
696
868
|
|
|
697
869
|
### useReverse(routes)
|
|
698
870
|
|
|
699
|
-
Mount-aware local reverse for client components. Import the generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse("
|
|
871
|
+
Mount-aware local reverse for client components. Import the generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse("name", params?)` — the leading dot is optional. Auto-fills params from `useParams()`; explicit params override.
|
|
872
|
+
|
|
873
|
+
> Per-module `*.gen.ts` files are **CLI opt-in and not Vite-watched** — run `rango generate <urls-file>` (or wire it into `predev`) and re-run it whenever the module's routes change. See `/links` for the full generated-file setup and exposure-boundary rules.
|
|
700
874
|
|
|
701
875
|
```tsx
|
|
702
876
|
"use client";
|
|
@@ -707,8 +881,8 @@ function BlogNav() {
|
|
|
707
881
|
const reverse = useReverse(blogRoutes);
|
|
708
882
|
return (
|
|
709
883
|
<nav>
|
|
710
|
-
<Link to={reverse("
|
|
711
|
-
<Link to={reverse("
|
|
884
|
+
<Link to={reverse("index")}>Blog</Link>
|
|
885
|
+
<Link to={reverse("post", { postId: "hello" })}>Post</Link>
|
|
712
886
|
</nav>
|
|
713
887
|
);
|
|
714
888
|
}
|
|
@@ -718,21 +892,22 @@ See `/links` for the full URL generation guide. `ctx.reverse()` is server-only;
|
|
|
718
892
|
|
|
719
893
|
## Hook Summary
|
|
720
894
|
|
|
721
|
-
| Hook
|
|
722
|
-
|
|
|
723
|
-
| `useParams()`
|
|
724
|
-
| `usePathname()`
|
|
725
|
-
| `useSearchParams()`
|
|
726
|
-
| `useHref()`
|
|
727
|
-
| `useMount()`
|
|
728
|
-
| `useReverse()`
|
|
729
|
-
| `useNavigation()`
|
|
730
|
-
| `useRouter()`
|
|
731
|
-
| `useSegments()`
|
|
732
|
-
| `useLinkStatus()`
|
|
733
|
-
| `useLoader()`
|
|
734
|
-
| `useFetchLoader()`
|
|
735
|
-
| `
|
|
736
|
-
| `
|
|
737
|
-
| `
|
|
738
|
-
| `
|
|
895
|
+
| Hook | Purpose | Returns |
|
|
896
|
+
| --------------------- | --------------------------------- | ------------------------------------------------------------------ |
|
|
897
|
+
| `useParams()` | Route params | `Readonly<T>` (default `Record<string, string>`) or selected value |
|
|
898
|
+
| `usePathname()` | Current pathname | `string` |
|
|
899
|
+
| `useSearchParams()` | URL search params | `ReadonlyURLSearchParams` |
|
|
900
|
+
| `useHref()` | Mount-aware href | `(path) => string` |
|
|
901
|
+
| `useMount()` | Current include() mount path | `string` |
|
|
902
|
+
| `useReverse()` | Local reverse for imported routes | `(name, params?, search?) => string` |
|
|
903
|
+
| `useNavigation()` | Reactive navigation state | state, location, isStreaming |
|
|
904
|
+
| `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward |
|
|
905
|
+
| `useSegments()` | URL path & segment IDs | path, segmentIds, location |
|
|
906
|
+
| `useLinkStatus()` | Link pending state | { pending } |
|
|
907
|
+
| `useLoader()` | Loader data (strict) | data, isLoading, error |
|
|
908
|
+
| `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading |
|
|
909
|
+
| `useRefreshLoaders()` | Refresh cross-loader group(s) | `() => (groups: string \| string[]) => Promise<void>` |
|
|
910
|
+
| `useHandle()` | Accumulated handle data | T (handle type) |
|
|
911
|
+
| `useAction()` | Server action state | state, error, result |
|
|
912
|
+
| `useLocationState()` | History state (persists or flash) | T \| undefined |
|
|
913
|
+
| `useClientCache()` | Cache control | { clear } |
|
|
@@ -22,9 +22,9 @@ import { createHostRouter } from "@rangojs/router/host";
|
|
|
22
22
|
|
|
23
23
|
const router = createHostRouter();
|
|
24
24
|
|
|
25
|
-
router.host(["."]).
|
|
26
|
-
router.host(["admin.*"]).
|
|
27
|
-
router.host(["api.*"]).
|
|
25
|
+
router.host(["."]).lazy(() => import("./apps/main"));
|
|
26
|
+
router.host(["admin.*"]).lazy(() => import("./apps/admin"));
|
|
27
|
+
router.host(["api.*"]).lazy(() => import("./apps/api"));
|
|
28
28
|
|
|
29
29
|
export default {
|
|
30
30
|
fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
@@ -33,7 +33,31 @@ export default {
|
|
|
33
33
|
};
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
## Inline handlers (`.map`) vs lazy mounts (`.lazy`)
|
|
37
|
+
|
|
38
|
+
A host pattern maps to one of two things, and you pick the method by intent:
|
|
39
|
+
|
|
40
|
+
| Method | Argument | Use for |
|
|
41
|
+
| ------- | ------------------------------ | ------------------------------------------------------------ |
|
|
42
|
+
| `.map` | `(request, input) => Response` | An inline request handler that produces a response directly. |
|
|
43
|
+
| `.lazy` | `() => import("./sub-app")` | A lazily-imported handler or nested host router (a sub-app). |
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// Lazy mount: the module's default export is a handler or a HostRouter.
|
|
47
|
+
router.host(["admin.*"]).lazy(() => import("./apps/admin"));
|
|
48
|
+
|
|
49
|
+
// Inline handler: returns a Response itself (sync or async).
|
|
50
|
+
router.host(["health.*"]).map(() => new Response("ok"));
|
|
51
|
+
router
|
|
52
|
+
.host(["echo.*"])
|
|
53
|
+
.map((request) => new Response(new URL(request.url).pathname));
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Why two methods instead of one overloaded `.map()`:
|
|
57
|
+
|
|
58
|
+
- **Build-time discovery** invokes only `.lazy()` mounts (to trigger each sub-app's `createRouter()` registration). Inline `.map()` handlers are never invoked during discovery, so they can't crash it or pollute its errors.
|
|
59
|
+
- `.map(() => import("./sub-app"))` is a **type error** — a lazy import resolves to a module, not a `Response`. Use `.lazy()` for imports. (If the types are bypassed, e.g. from JS, a `.map()` handler that resolves to a module throws a clear `HostRouterError` at request time instead of returning the module.)
|
|
60
|
+
- A lazy loader may declare an ignored parameter (`.lazy((_request?) => import("./x"))`); `.lazy()` accepts it because intent is explicit, not inferred from the signature.
|
|
37
61
|
|
|
38
62
|
## Pattern Syntax
|
|
39
63
|
|
|
@@ -65,8 +89,8 @@ const hosts = defineHosts({
|
|
|
65
89
|
app: [".", "www.*"],
|
|
66
90
|
});
|
|
67
91
|
|
|
68
|
-
router.host(hosts.admin).
|
|
69
|
-
router.host(hosts.app).
|
|
92
|
+
router.host(hosts.admin).lazy(() => import("./apps/admin"));
|
|
93
|
+
router.host(hosts.app).lazy(() => import("./apps/main"));
|
|
70
94
|
```
|
|
71
95
|
|
|
72
96
|
Returns a frozen object — keys are autocompleted by TypeScript.
|
|
@@ -88,7 +112,7 @@ router.use(async (request, input, next) => {
|
|
|
88
112
|
router
|
|
89
113
|
.host(["admin.*"])
|
|
90
114
|
.use(requireAuth)
|
|
91
|
-
.
|
|
115
|
+
.lazy(() => import("./apps/admin"));
|
|
92
116
|
```
|
|
93
117
|
|
|
94
118
|
Middleware signature: `(request: Request, input: RouterRequestInput, next: () => Promise<Response>) => Promise<Response>`
|
|
@@ -179,40 +203,41 @@ const request = createTestRequest({
|
|
|
179
203
|
});
|
|
180
204
|
|
|
181
205
|
// Test which route would match (without executing)
|
|
182
|
-
router.test("admin.example.com"); // { pattern, handler } | null
|
|
206
|
+
router.test("admin.example.com"); // { pattern, handler, kind } | null
|
|
183
207
|
```
|
|
184
208
|
|
|
185
209
|
## Error Types
|
|
186
210
|
|
|
187
211
|
All errors extend `HostRouterError`:
|
|
188
212
|
|
|
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
|
|
213
|
+
| Error | When |
|
|
214
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
215
|
+
| `InvalidPatternError` | Pattern is empty, non-string, or has spaces |
|
|
216
|
+
| `HostOverrideNotAllowedError` | Cookie override from disallowed host |
|
|
217
|
+
| `InvalidHostnameError` | Cookie value isn't a valid hostname |
|
|
218
|
+
| `HostValidationError` | Custom `validate` function threw |
|
|
219
|
+
| `NoRouteMatchError` | No host pattern matched the request |
|
|
220
|
+
| `InvalidHandlerError` | Handler is not a function, or a lazy mount resolved to a module without a usable `default` export |
|
|
221
|
+
| `HostRouterError` | A `.map()` inline handler resolved to a module namespace (a misused lazy import — use `.lazy()`) |
|
|
197
222
|
|
|
198
223
|
See the fallback section above for a `NoRouteMatchError` catch example.
|
|
199
224
|
|
|
200
225
|
## Nesting Host Routers
|
|
201
226
|
|
|
202
|
-
A lazy
|
|
227
|
+
A lazy mount can resolve to another `HostRouter`:
|
|
203
228
|
|
|
204
229
|
```typescript
|
|
205
230
|
// apps/regional.ts
|
|
206
231
|
import { createHostRouter } from "@rangojs/router/host";
|
|
207
232
|
|
|
208
233
|
const regional = createHostRouter();
|
|
209
|
-
regional.host(["us.*"]).
|
|
210
|
-
regional.host(["eu.*"]).
|
|
234
|
+
regional.host(["us.*"]).lazy(() => import("./regions/us"));
|
|
235
|
+
regional.host(["eu.*"]).lazy(() => import("./regions/eu"));
|
|
211
236
|
|
|
212
237
|
export default regional;
|
|
213
238
|
```
|
|
214
239
|
|
|
215
240
|
```typescript
|
|
216
241
|
// host-router.ts
|
|
217
|
-
router.host(["**.regional.example.com"]).
|
|
242
|
+
router.host(["**.regional.example.com"]).lazy(() => import("./apps/regional"));
|
|
218
243
|
```
|
|
@@ -8,9 +8,6 @@ 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
|
-
|
|
14
11
|
## Basic Intercept
|
|
15
12
|
|
|
16
13
|
```typescript
|
|
@@ -111,7 +108,7 @@ consumer when they share `ctx.set()` data:
|
|
|
111
108
|
|
|
112
109
|
```typescript
|
|
113
110
|
export const revalidateProductShell = ({ actionId }) =>
|
|
114
|
-
actionId?.includes("src/actions/product.ts#")
|
|
111
|
+
actionId?.includes("src/actions/product.ts#") || undefined;
|
|
115
112
|
|
|
116
113
|
layout(ProductLayout, () => [
|
|
117
114
|
revalidate(revalidateProductShell), // producer reruns
|
package/skills/layout/SKILL.md
CHANGED
|
@@ -8,9 +8,6 @@ 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
|
-
|
|
14
11
|
## Basic Layout
|
|
15
12
|
|
|
16
13
|
```typescript
|
|
@@ -206,7 +203,7 @@ layout(<ShopLayout />, () => [
|
|
|
206
203
|
|
|
207
204
|
// Or revalidate based on conditions
|
|
208
205
|
layout(<CartLayout />, () => [
|
|
209
|
-
revalidate(({ actionId }) => actionId?.includes("Cart")
|
|
206
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
|
|
210
207
|
|
|
211
208
|
path("/cart", CartPage, { name: "cart" }),
|
|
212
209
|
])
|
|
@@ -225,7 +222,7 @@ them on both producer and consumer segments:
|
|
|
225
222
|
```typescript
|
|
226
223
|
// revalidation-contracts.ts
|
|
227
224
|
export const revalidateCartData = ({ actionId }) =>
|
|
228
|
-
actionId?.includes("src/actions/cart.ts#addToCart")
|
|
225
|
+
actionId?.includes("src/actions/cart.ts#addToCart") || undefined;
|
|
229
226
|
```
|
|
230
227
|
|
|
231
228
|
```typescript
|
|
@@ -247,7 +244,7 @@ You can also package them as importable handoff helpers:
|
|
|
247
244
|
import { revalidate } from "@rangojs/router";
|
|
248
245
|
|
|
249
246
|
export const revalidateAuthData = ({ actionId }) =>
|
|
250
|
-
actionId?.includes("src/actions/auth.ts#")
|
|
247
|
+
actionId?.includes("src/actions/auth.ts#") || undefined;
|
|
251
248
|
export const revalidateAuth = () => [revalidate(revalidateAuthData)];
|
|
252
249
|
```
|
|
253
250
|
|
|
@@ -294,7 +291,7 @@ export const shopPatterns = urls(({ path, layout, parallel, loader, revalidate }
|
|
|
294
291
|
}, () => [
|
|
295
292
|
// Layout loaders
|
|
296
293
|
loader(CartLoader, () => [
|
|
297
|
-
revalidate(({ actionId }) => actionId?.includes("Cart")
|
|
294
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
|
|
298
295
|
]),
|
|
299
296
|
|
|
300
297
|
// Parallel routes
|
package/skills/links/SKILL.md
CHANGED
|
@@ -13,7 +13,7 @@ argument-hint: [ctx.reverse|href|useHref|useMount|useReverse|scopedReverse]
|
|
|
13
13
|
**On the client, two patterns:**
|
|
14
14
|
|
|
15
15
|
1. **Receive URLs as props / loader data / action return.** The default. The server has the full route manifest and handler context — generate URLs there and hand strings to client components.
|
|
16
|
-
2. **`useReverse(routes)`.** Import a generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse("
|
|
16
|
+
2. **`useReverse(routes)`.** Import a generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse("name", params?)` (the leading dot is optional). Mount-aware via `useMount()`, auto-fills params from `useParams()`, fully typed from the imported map. Use this when a client component needs to generate URLs into a known module without round-tripping through the server.
|
|
17
17
|
|
|
18
18
|
`ctx.reverse()` itself is **server-only** — it depends on the full route manifest and handler context. Client components never import or call it.
|
|
19
19
|
|
|
@@ -213,7 +213,17 @@ function GlobalNav() {
|
|
|
213
213
|
}
|
|
214
214
|
```
|
|
215
215
|
|
|
216
|
-
`href()` provides compile-time validation via `
|
|
216
|
+
`href()` provides compile-time validation via the `Rango.Path` type. Paths are validated against registered route patterns using `PatternToPath`.
|
|
217
|
+
|
|
218
|
+
When wrapping `href()`, type the wrapper's path parameter as `Rango.Path` so it
|
|
219
|
+
keeps the same generated-route validation. `Rango.Path` is ambient — no import,
|
|
220
|
+
just like `Rango.Env` / `Rango.Vars`:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { href } from "@rangojs/router/client";
|
|
224
|
+
|
|
225
|
+
export const appHref = (path: Rango.Path): string => href(path);
|
|
226
|
+
```
|
|
217
227
|
|
|
218
228
|
`href()` is a raw path helper — it is **not** basename-aware. It returns the path as-is (or with the include mount prefix via `useHref()`). For basename-aware navigation, use `Link`, `useRouter().push()`, or `reverse()`, which auto-prefix root-relative paths with the router's basename.
|
|
219
229
|
|
|
@@ -263,6 +273,8 @@ function MountInfo() {
|
|
|
263
273
|
|
|
264
274
|
Hook that returns a typed local reverse function for a `routes` map imported from a generated `.gen.ts` next to a `urls()` module. The route map is the **exposure boundary** — `useReverse` only knows about names in that map, never the full app manifest.
|
|
265
275
|
|
|
276
|
+
> **Which map?** `useReverse` accepts any routes map. Prefer the per-module `routes` (e.g. `urls/blog.gen.ts`): it gives **mount-aware** local `.name` reverse (auto-prefixes the `include()` mount) and only that module's names enter the client bundle. You _can_ instead pass `router.named-routes.gen.ts` (`NamedRoutes`) for **global** names (`blog.post`; the leading dot is optional) — it is a plain importable map and works on the client (it is **not** server-only) — but its paths are **absolute** while `useReverse` mount-prefixes, so it is correct only at the root mount (under a non-root mount it double-prefixes), and importing it pulls every route name and pattern in the app into the client bundle (a small names-to-paths map — not components or loaders), versus the per-module map which exposes only one module's names. So the per-module map is preferred for in-module links; the named-routes map is the escape hatch for global names.
|
|
277
|
+
|
|
266
278
|
```tsx
|
|
267
279
|
"use client";
|
|
268
280
|
import { Link, useReverse } from "@rangojs/router/client";
|
|
@@ -273,8 +285,8 @@ export function BlogNav() {
|
|
|
273
285
|
|
|
274
286
|
return (
|
|
275
287
|
<nav>
|
|
276
|
-
<Link to={reverse("
|
|
277
|
-
<Link to={reverse("
|
|
288
|
+
<Link to={reverse("index")}>Blog</Link>
|
|
289
|
+
<Link to={reverse("post", { postId: "hello" })}>Post</Link>
|
|
278
290
|
</nav>
|
|
279
291
|
);
|
|
280
292
|
}
|
|
@@ -282,7 +294,7 @@ export function BlogNav() {
|
|
|
282
294
|
|
|
283
295
|
### How it resolves
|
|
284
296
|
|
|
285
|
-
1. Strips
|
|
297
|
+
1. Strips an optional leading `.` and looks up the name in the imported `routes` map.
|
|
286
298
|
2. Joins the local pattern with the surrounding `useMount()` value — the include's URL pattern.
|
|
287
299
|
3. Substitutes params: explicit params from the call, then auto-filled from `useParams()` for anything still unresolved (mount params like `:tenantId` flow in this way).
|
|
288
300
|
4. Appends a query string if a search object is passed and the route has a `search` schema.
|
|
@@ -350,14 +362,14 @@ reverse(".search", {}, { q: "hello world", page: 2 });
|
|
|
350
362
|
|
|
351
363
|
### Errors
|
|
352
364
|
|
|
353
|
-
- Unknown name: throws `Unknown
|
|
365
|
+
- Unknown name: throws `Unknown route: ".not-a-route"`.
|
|
354
366
|
- Missing required param: throws `Missing param "postId" for route ".detail"`.
|
|
355
367
|
|
|
356
368
|
Both happen synchronously during `reverse()` — wrap calls in try/catch (or an ErrorBoundary if the throw happens during render) when you need to surface them as UI.
|
|
357
369
|
|
|
358
|
-
###
|
|
370
|
+
### The leading dot is optional
|
|
359
371
|
|
|
360
|
-
`
|
|
372
|
+
`reverse("post")` and `reverse(".post")` resolve **identically** — the leading dot is cosmetic. The map you import IS the scope, so there is no separate global namespace to disambiguate and the dot carries no meaning; it exists only as a readability convention and for parity with `ctx.reverse(".name")` on the server. To link into a different module, import that module's `routes`:
|
|
361
373
|
|
|
362
374
|
```tsx
|
|
363
375
|
import { routes as blogRoutes } from "../urls/blog.gen.js";
|
|
@@ -368,8 +380,8 @@ function CrossNav() {
|
|
|
368
380
|
const shop = useReverse(shopRoutes);
|
|
369
381
|
return (
|
|
370
382
|
<nav>
|
|
371
|
-
<Link to={blog("
|
|
372
|
-
<Link to={shop("
|
|
383
|
+
<Link to={blog("index")}>Blog</Link>
|
|
384
|
+
<Link to={shop("cart")}>Cart</Link>
|
|
373
385
|
</nav>
|
|
374
386
|
);
|
|
375
387
|
}
|