@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.4
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/CLAUDE.md +40 -0
- package/dist/vite/index.js +53 -4
- package/package.json +13 -3
- package/skills/caching/SKILL.md +5 -5
- package/skills/document-cache/SKILL.md +7 -7
- package/skills/hooks/SKILL.md +83 -0
- package/skills/intercept/SKILL.md +2 -2
- package/skills/layout/SKILL.md +2 -2
- package/skills/links/SKILL.md +180 -0
- package/skills/loader/SKILL.md +32 -5
- package/skills/middleware/SKILL.md +4 -4
- package/skills/parallel/SKILL.md +2 -2
- package/skills/rango/SKILL.md +2 -1
- package/skills/route/SKILL.md +2 -2
- package/skills/router-setup/SKILL.md +83 -14
- package/skills/theme/SKILL.md +4 -4
- package/skills/typesafety/SKILL.md +140 -41
- package/src/browser/partial-update.ts +24 -24
- package/src/browser/react/NavigationProvider.tsx +0 -18
- package/src/browser/react/mount-context.ts +14 -0
- package/src/browser/react/use-handle.ts +34 -3
- package/src/browser/react/use-href.tsx +20 -188
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +10 -20
- package/src/browser/rsc-router.tsx +4 -0
- package/src/browser/segment-structure-assert.ts +67 -0
- package/src/browser/server-action-bridge.ts +18 -3
- package/src/browser/types.ts +2 -10
- package/src/build/generate-manifest.ts +227 -0
- package/src/build/index.ts +22 -0
- package/src/client.rsc.tsx +9 -14
- package/src/client.tsx +10 -10
- package/src/handle.ts +33 -4
- package/src/handles/MetaTags.tsx +4 -0
- package/src/href-client.ts +17 -21
- package/src/href.ts +1 -1
- package/src/index.rsc.ts +2 -3
- package/src/index.ts +1 -1
- package/src/loader.rsc.ts +6 -0
- package/src/route-content-wrapper.tsx +3 -8
- package/src/route-definition.ts +48 -8
- package/src/route-map-builder.ts +43 -2
- package/src/route-types.ts +12 -2
- package/src/router/handler-context.ts +1 -1
- package/src/router/manifest.ts +33 -23
- package/src/router/match-context.ts +1 -1
- package/src/router/match-result.ts +0 -1
- package/src/router/pattern-matching.ts +124 -1
- package/src/router.ts +763 -210
- package/src/rsc/handler.ts +48 -30
- package/src/rsc/index.ts +5 -5
- package/src/rsc/types.ts +3 -4
- package/src/segment-system.tsx +31 -50
- package/src/server/context.ts +20 -0
- package/src/server/route-manifest-cache.ts +173 -0
- package/src/server.ts +9 -0
- package/src/ssr/index.tsx +66 -4
- package/src/types.ts +57 -11
- package/src/urls.ts +114 -38
- package/src/vite/expose-loader-id.ts +71 -2
- package/src/vite/virtual-entries.ts +4 -1
- package/src/warmup/connection-warmup.tsx +94 -0
- package/src/warmup/warmup-context.ts +35 -0
package/CLAUDE.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
1
|
# @rangojs/router
|
|
2
2
|
|
|
3
3
|
Run `/rango` first to understand the API. Skills are in `node_modules/@rangojs/router/skills/`.
|
|
4
|
+
|
|
5
|
+
## Tree-Structure-Critical Files (DO NOT MODIFY without understanding)
|
|
6
|
+
|
|
7
|
+
The following files control the React tree structure. Changing the tree structure
|
|
8
|
+
(element types, nesting depth, or keys at any position) between SSR, navigation,
|
|
9
|
+
and action renders will cause React to remount components, destroying client state
|
|
10
|
+
like `useActionState`, refs, and local state. This is extremely hard to debug.
|
|
11
|
+
|
|
12
|
+
**Protected files:**
|
|
13
|
+
|
|
14
|
+
- `src/segment-system.tsx` - `renderSegments()` builds the React tree from segments.
|
|
15
|
+
The `loading` property determines tree structure:
|
|
16
|
+
- `undefined` / `null` -> OutletProvider directly (no boundary)
|
|
17
|
+
- `false` -> LoaderBoundary + OutletProvider (boundary, no RouteContentWrapper)
|
|
18
|
+
- truthy (ReactNode) -> LoaderBoundary + OutletProvider + RouteContentWrapper
|
|
19
|
+
|
|
20
|
+
- `src/route-content-wrapper.tsx` - `LoaderBoundary` and `RouteContentWrapper`.
|
|
21
|
+
These add structural depth (Suspense boundaries) to the React tree.
|
|
22
|
+
|
|
23
|
+
- `src/browser/server-action-bridge.ts` - Merges server action segments with
|
|
24
|
+
cached segments. Must preserve cached `loading` values to prevent tree drift.
|
|
25
|
+
|
|
26
|
+
- `src/browser/partial-update.ts` - Merges navigation segments with cached segments.
|
|
27
|
+
|
|
28
|
+
**Rules:**
|
|
29
|
+
|
|
30
|
+
1. Never change the conditional logic in `renderSegments()` that decides between
|
|
31
|
+
LoaderBoundary/RouteContentWrapper/OutletProvider without verifying all three
|
|
32
|
+
render paths (SSR, navigation, action) produce identical tree structures.
|
|
33
|
+
|
|
34
|
+
2. Never add or remove wrapper elements (Suspense, div, Fragment) around segment
|
|
35
|
+
content without checking that the same wrappers exist in ALL render paths.
|
|
36
|
+
|
|
37
|
+
3. When merging segments (action bridge, partial update), always preserve the
|
|
38
|
+
cached `loading` value if it differs from the server value. The server may
|
|
39
|
+
return different `loading` values based on `isSSR` context.
|
|
40
|
+
|
|
41
|
+
4. Run `pnpm --filter @rangojs/router exec playwright test loader-behavior` after
|
|
42
|
+
any changes to these files. The skipSSR action tests specifically catch tree
|
|
43
|
+
structure regressions.
|
package/dist/vite/index.js
CHANGED
|
@@ -211,6 +211,36 @@ function countCreateLoaderArgs(code, startPos, endPos) {
|
|
|
211
211
|
}
|
|
212
212
|
return hasContent ? argCount + 1 : 0;
|
|
213
213
|
}
|
|
214
|
+
function generateClientLoaderStubs(code, filePath, isBuild) {
|
|
215
|
+
const loaderPattern = /export\s+const\s+(\w+)\s*=\s*createLoader\s*\(/g;
|
|
216
|
+
const loaders = [];
|
|
217
|
+
let match;
|
|
218
|
+
while ((match = loaderPattern.exec(code)) !== null) {
|
|
219
|
+
loaders.push(match[1]);
|
|
220
|
+
}
|
|
221
|
+
if (loaders.length === 0) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const allExports = /export\s+(const|let|var|function|class|default)\s+(\w+)/g;
|
|
225
|
+
let exportMatch;
|
|
226
|
+
const nonLoaderExports = [];
|
|
227
|
+
while ((exportMatch = allExports.exec(code)) !== null) {
|
|
228
|
+
const name = exportMatch[2];
|
|
229
|
+
if (!loaders.includes(name)) {
|
|
230
|
+
nonLoaderExports.push(name);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (nonLoaderExports.length > 0) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const stubs = loaders.map((name) => {
|
|
237
|
+
const loaderId = isBuild ? hashLoaderId(filePath, name) : `${filePath}#${name}`;
|
|
238
|
+
return `export const ${name} = { __brand: "loader", $$id: "${loaderId}" };`;
|
|
239
|
+
});
|
|
240
|
+
return {
|
|
241
|
+
code: stubs.join("\n") + "\n"
|
|
242
|
+
};
|
|
243
|
+
}
|
|
214
244
|
function transformLoaderExports(code, filePath, sourceId, isBuild = false) {
|
|
215
245
|
if (!code.includes("createLoader")) {
|
|
216
246
|
return null;
|
|
@@ -376,6 +406,12 @@ ${lazyImports.join(",\n")}
|
|
|
376
406
|
loaderRegistry.set(hashedId, { filePath: relativePath, exportName });
|
|
377
407
|
}
|
|
378
408
|
}
|
|
409
|
+
if (!isRscEnv) {
|
|
410
|
+
const stubResult = generateClientLoaderStubs(code, relativePath, isBuild);
|
|
411
|
+
if (stubResult) {
|
|
412
|
+
return stubResult;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
379
415
|
return transformLoaderExports(code, relativePath, id, isBuild);
|
|
380
416
|
}
|
|
381
417
|
};
|
|
@@ -634,7 +670,7 @@ import {
|
|
|
634
670
|
decodeFormState,
|
|
635
671
|
} from "@rangojs/router/internal/deps/rsc";
|
|
636
672
|
import { router } from "${routerPath}";
|
|
637
|
-
import { createRSCHandler } from "@rangojs/router/rsc";
|
|
673
|
+
import { createRSCHandler } from "@rangojs/router/internal/rsc-handler";
|
|
638
674
|
import { VERSION } from "@rangojs/router:version";
|
|
639
675
|
|
|
640
676
|
// Import loader manifest to ensure all fetchable loaders are registered at startup
|
|
@@ -642,6 +678,9 @@ import { VERSION } from "@rangojs/router:version";
|
|
|
642
678
|
// might not be imported before a GET request arrives
|
|
643
679
|
import "virtual:rsc-router/loader-manifest";
|
|
644
680
|
|
|
681
|
+
// Route manifest is now loaded at runtime on first request via getRouteManifestData()
|
|
682
|
+
// This eliminates the need for build-time manifest generation
|
|
683
|
+
|
|
645
684
|
export default createRSCHandler({
|
|
646
685
|
router,
|
|
647
686
|
version: VERSION,
|
|
@@ -675,7 +714,7 @@ import { resolve } from "node:path";
|
|
|
675
714
|
// package.json
|
|
676
715
|
var package_default = {
|
|
677
716
|
name: "@rangojs/router",
|
|
678
|
-
version: "0.0.0-experimental.
|
|
717
|
+
version: "0.0.0-experimental.4",
|
|
679
718
|
type: "module",
|
|
680
719
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
681
720
|
author: "Ivo Todorov",
|
|
@@ -760,6 +799,11 @@ var package_default = {
|
|
|
760
799
|
types: "./src/deps/html-stream-server.ts",
|
|
761
800
|
default: "./src/deps/html-stream-server.ts"
|
|
762
801
|
},
|
|
802
|
+
"./internal/rsc-handler": {
|
|
803
|
+
"react-server": "./src/rsc/handler.ts",
|
|
804
|
+
types: "./src/rsc/handler.ts",
|
|
805
|
+
default: "./src/rsc/handler.ts"
|
|
806
|
+
},
|
|
763
807
|
"./cache": {
|
|
764
808
|
"react-server": "./src/cache/index.ts",
|
|
765
809
|
types: "./src/cache/index.ts",
|
|
@@ -768,6 +812,10 @@ var package_default = {
|
|
|
768
812
|
"./theme": {
|
|
769
813
|
types: "./src/theme/index.ts",
|
|
770
814
|
default: "./src/theme/index.ts"
|
|
815
|
+
},
|
|
816
|
+
"./build": {
|
|
817
|
+
types: "./src/build/index.ts",
|
|
818
|
+
import: "./src/build/index.ts"
|
|
771
819
|
}
|
|
772
820
|
},
|
|
773
821
|
files: [
|
|
@@ -814,12 +862,13 @@ var package_default = {
|
|
|
814
862
|
"@types/node": "^24.10.1",
|
|
815
863
|
"@types/react": "catalog:",
|
|
816
864
|
"@types/react-dom": "catalog:",
|
|
865
|
+
esbuild: "^0.27.0",
|
|
866
|
+
jiti: "^2.6.1",
|
|
817
867
|
react: "catalog:",
|
|
818
868
|
"react-dom": "catalog:",
|
|
819
|
-
esbuild: "^0.27.0",
|
|
820
869
|
tinyexec: "^0.3.2",
|
|
821
870
|
typescript: "^5.3.0",
|
|
822
|
-
vitest: "^
|
|
871
|
+
vitest: "^4.0.0"
|
|
823
872
|
}
|
|
824
873
|
};
|
|
825
874
|
|
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.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
6
6
|
"author": "Ivo Todorov",
|
|
@@ -85,6 +85,11 @@
|
|
|
85
85
|
"types": "./src/deps/html-stream-server.ts",
|
|
86
86
|
"default": "./src/deps/html-stream-server.ts"
|
|
87
87
|
},
|
|
88
|
+
"./internal/rsc-handler": {
|
|
89
|
+
"react-server": "./src/rsc/handler.ts",
|
|
90
|
+
"types": "./src/rsc/handler.ts",
|
|
91
|
+
"default": "./src/rsc/handler.ts"
|
|
92
|
+
},
|
|
88
93
|
"./cache": {
|
|
89
94
|
"react-server": "./src/cache/index.ts",
|
|
90
95
|
"types": "./src/cache/index.ts",
|
|
@@ -93,6 +98,10 @@
|
|
|
93
98
|
"./theme": {
|
|
94
99
|
"types": "./src/theme/index.ts",
|
|
95
100
|
"default": "./src/theme/index.ts"
|
|
101
|
+
},
|
|
102
|
+
"./build": {
|
|
103
|
+
"types": "./src/build/index.ts",
|
|
104
|
+
"import": "./src/build/index.ts"
|
|
96
105
|
}
|
|
97
106
|
},
|
|
98
107
|
"files": [
|
|
@@ -130,12 +139,13 @@
|
|
|
130
139
|
"@types/node": "^24.10.1",
|
|
131
140
|
"@types/react": "^19.2.7",
|
|
132
141
|
"@types/react-dom": "^19.2.3",
|
|
142
|
+
"esbuild": "^0.27.0",
|
|
143
|
+
"jiti": "^2.6.1",
|
|
133
144
|
"react": "^19.2.1",
|
|
134
145
|
"react-dom": "^19.2.1",
|
|
135
|
-
"esbuild": "^0.27.0",
|
|
136
146
|
"tinyexec": "^0.3.2",
|
|
137
147
|
"typescript": "^5.3.0",
|
|
138
|
-
"vitest": "^
|
|
148
|
+
"vitest": "^4.0.0"
|
|
139
149
|
},
|
|
140
150
|
"scripts": {
|
|
141
151
|
"build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external",
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -13,7 +13,7 @@ argument-hint: [setup]
|
|
|
13
13
|
Use the `cache()` DSL function to cache routes:
|
|
14
14
|
|
|
15
15
|
```typescript
|
|
16
|
-
import { urls } from "@rangojs/router
|
|
16
|
+
import { urls } from "@rangojs/router";
|
|
17
17
|
|
|
18
18
|
export const urlpatterns = urls(({ path, cache }) => [
|
|
19
19
|
// Cache these routes for 60 seconds, SWR for 5 minutes
|
|
@@ -59,14 +59,14 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
59
59
|
Configure a cache store in the router:
|
|
60
60
|
|
|
61
61
|
```typescript
|
|
62
|
-
import {
|
|
62
|
+
import { createRouter } from "@rangojs/router";
|
|
63
63
|
import { MemorySegmentCacheStore } from "@rangojs/router/rsc";
|
|
64
64
|
|
|
65
65
|
const store = new MemorySegmentCacheStore({
|
|
66
66
|
defaults: { ttl: 60, swr: 300 },
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
const router =
|
|
69
|
+
const router = createRouter({
|
|
70
70
|
document: Document,
|
|
71
71
|
urls: urlpatterns,
|
|
72
72
|
cache: {
|
|
@@ -98,7 +98,7 @@ For distributed caching on Cloudflare Workers:
|
|
|
98
98
|
```typescript
|
|
99
99
|
import { CFCacheStore } from "@rangojs/router/cache/cf";
|
|
100
100
|
|
|
101
|
-
const router =
|
|
101
|
+
const router = createRouter({
|
|
102
102
|
document: Document,
|
|
103
103
|
urls: urlpatterns,
|
|
104
104
|
cache: (env) => ({
|
|
@@ -145,7 +145,7 @@ cache({ store: checkoutCache }, () => [
|
|
|
145
145
|
## Complete Example
|
|
146
146
|
|
|
147
147
|
```typescript
|
|
148
|
-
import { urls } from "@rangojs/router
|
|
148
|
+
import { urls } from "@rangojs/router";
|
|
149
149
|
import { MemorySegmentCacheStore } from "@rangojs/router/rsc";
|
|
150
150
|
|
|
151
151
|
// Custom store for checkout (short TTL)
|
|
@@ -13,11 +13,11 @@ Caches complete HTTP responses (HTML/RSC) at the edge based on Cache-Control hea
|
|
|
13
13
|
Configure document cache in router:
|
|
14
14
|
|
|
15
15
|
```typescript
|
|
16
|
-
import {
|
|
16
|
+
import { createRouter } from "@rangojs/router";
|
|
17
17
|
import { CFCacheStore } from "@rangojs/router/cache/cf";
|
|
18
18
|
import { urlpatterns } from "./urls";
|
|
19
19
|
|
|
20
|
-
const router =
|
|
20
|
+
const router = createRouter<AppEnv>({
|
|
21
21
|
document: Document,
|
|
22
22
|
urls: urlpatterns,
|
|
23
23
|
documentCache: (env) => ({
|
|
@@ -35,7 +35,7 @@ export default router;
|
|
|
35
35
|
Routes opt-in to document caching using the `cache()` DSL with `documentCache` option:
|
|
36
36
|
|
|
37
37
|
```typescript
|
|
38
|
-
import { urls } from "@rangojs/router
|
|
38
|
+
import { urls } from "@rangojs/router";
|
|
39
39
|
|
|
40
40
|
export const urlpatterns = urls(({ path, cache }) => [
|
|
41
41
|
// Cache full page for 5 min, serve stale for 1 hour
|
|
@@ -56,7 +56,7 @@ export const urlpatterns = urls(({ path, cache }) => [
|
|
|
56
56
|
## Document Cache Options
|
|
57
57
|
|
|
58
58
|
```typescript
|
|
59
|
-
|
|
59
|
+
createRouter({
|
|
60
60
|
// ...
|
|
61
61
|
documentCache: (env) => ({
|
|
62
62
|
// Cache store (required)
|
|
@@ -131,11 +131,11 @@ Segment hash ensures different cached responses for navigations from different s
|
|
|
131
131
|
|
|
132
132
|
```typescript
|
|
133
133
|
// router.tsx
|
|
134
|
-
import {
|
|
134
|
+
import { createRouter } from "@rangojs/router";
|
|
135
135
|
import { CFCacheStore } from "@rangojs/router/cache/cf";
|
|
136
136
|
import { urlpatterns } from "./urls";
|
|
137
137
|
|
|
138
|
-
const router =
|
|
138
|
+
const router = createRouter<AppEnv>({
|
|
139
139
|
document: Document,
|
|
140
140
|
urls: urlpatterns,
|
|
141
141
|
documentCache: (env) => ({
|
|
@@ -148,7 +148,7 @@ const router = createRSCRouter<AppEnv>({
|
|
|
148
148
|
export default router;
|
|
149
149
|
|
|
150
150
|
// urls.tsx
|
|
151
|
-
import { urls } from "@rangojs/router
|
|
151
|
+
import { urls } from "@rangojs/router";
|
|
152
152
|
|
|
153
153
|
export const urlpatterns = urls(({ path, layout, cache, loader }) => [
|
|
154
154
|
// Blog with document caching
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -108,6 +108,20 @@ function ProductPrice() {
|
|
|
108
108
|
|
|
109
109
|
**Precondition**: Loader must be registered on route via `loader()` helper.
|
|
110
110
|
|
|
111
|
+
Loaders can also be passed as props from server to client components:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
"use client";
|
|
115
|
+
import { useLoader } from "@rangojs/router/client";
|
|
116
|
+
import type { ProductLoader } from "../loaders";
|
|
117
|
+
|
|
118
|
+
// typeof infers the full data type from the loader definition
|
|
119
|
+
function ProductCard({ loader }: { loader: typeof ProductLoader }) {
|
|
120
|
+
const { data } = useLoader(loader);
|
|
121
|
+
return <h2>{data.product.name}</h2>;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
111
125
|
### useFetchLoader()
|
|
112
126
|
|
|
113
127
|
Access loader with on-demand fetching (flexible):
|
|
@@ -194,6 +208,31 @@ function BreadcrumbNav() {
|
|
|
194
208
|
const lastCrumb = useHandle(Breadcrumbs, data => data.at(-1));
|
|
195
209
|
```
|
|
196
210
|
|
|
211
|
+
Handles can be passed as props from server to client components:
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
// Server component
|
|
215
|
+
path("/dashboard", (ctx) => {
|
|
216
|
+
const push = ctx.use(Breadcrumbs);
|
|
217
|
+
push({ label: "Dashboard", href: "/dashboard" });
|
|
218
|
+
return <DashboardNav handle={Breadcrumbs} />;
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// Client component — typeof infers the full Handle<T> type
|
|
222
|
+
"use client";
|
|
223
|
+
import { useHandle } from "@rangojs/router/client";
|
|
224
|
+
import type { Breadcrumbs } from "../handles";
|
|
225
|
+
|
|
226
|
+
function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
227
|
+
const crumbs = useHandle(handle);
|
|
228
|
+
return <nav>{crumbs.map(c => <a href={c.href}>{c.label}</a>)}</nav>;
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
RSC serialization strips the `collect` function via `toJSON()`. On the client,
|
|
233
|
+
`useHandle()` recovers it from the module-level registry (populated when
|
|
234
|
+
`createHandle()` runs during module initialization).
|
|
235
|
+
|
|
197
236
|
## Action Hooks
|
|
198
237
|
|
|
199
238
|
### useAction()
|
|
@@ -343,10 +382,54 @@ function ConditionalLayout() {
|
|
|
343
382
|
}
|
|
344
383
|
```
|
|
345
384
|
|
|
385
|
+
## URL Hooks
|
|
386
|
+
|
|
387
|
+
### useHref()
|
|
388
|
+
|
|
389
|
+
Mount-aware href for client components inside `include()` scopes:
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
"use client";
|
|
393
|
+
import { useHref, href, Link } from "@rangojs/router/client";
|
|
394
|
+
|
|
395
|
+
// Inside include("/shop", shopPatterns)
|
|
396
|
+
function ShopNav() {
|
|
397
|
+
const href = useHref();
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<>
|
|
401
|
+
{/* Local paths - auto-prefixed with /shop */}
|
|
402
|
+
<Link to={href("/cart")}>Cart</Link>
|
|
403
|
+
<Link to={href("/product/widget")}>Widget</Link>
|
|
404
|
+
</>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths.
|
|
410
|
+
|
|
411
|
+
### useMount()
|
|
412
|
+
|
|
413
|
+
Returns the current `include()` mount path:
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
"use client";
|
|
417
|
+
import { useMount } from "@rangojs/router/client";
|
|
418
|
+
|
|
419
|
+
function MountInfo() {
|
|
420
|
+
const mount = useMount(); // "/shop" inside include("/shop", ...)
|
|
421
|
+
return <span>Mounted at: {mount}</span>;
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
See `/links` for full URL generation guide including server-side `ctx.href`.
|
|
426
|
+
|
|
346
427
|
## Hook Summary
|
|
347
428
|
|
|
348
429
|
| Hook | Purpose | Returns |
|
|
349
430
|
|------|---------|---------|
|
|
431
|
+
| `useHref()` | Mount-aware href | `(path) => string` |
|
|
432
|
+
| `useMount()` | Current include() mount path | `string` |
|
|
350
433
|
| `useNavigation()` | Navigation state & control | state, navigate, refresh |
|
|
351
434
|
| `useSegments()` | URL path & segment IDs | path, segmentIds, location |
|
|
352
435
|
| `useLinkStatus()` | Link pending state | { pending } |
|
|
@@ -11,7 +11,7 @@ Intercept routes render a different component during soft navigation (client-sid
|
|
|
11
11
|
## Basic Intercept
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import { urls } from "@rangojs/router
|
|
14
|
+
import { urls } from "@rangojs/router";
|
|
15
15
|
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
16
16
|
|
|
17
17
|
function ShopLayout() {
|
|
@@ -149,7 +149,7 @@ function ModalWrapper({ children }) {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
// urls/shop.tsx
|
|
152
|
-
import { urls } from "@rangojs/router
|
|
152
|
+
import { urls } from "@rangojs/router";
|
|
153
153
|
|
|
154
154
|
export const shopPatterns = urls(({
|
|
155
155
|
path,
|
package/skills/layout/SKILL.md
CHANGED
|
@@ -11,7 +11,7 @@ Layouts wrap child routes and persist during navigation within their scope.
|
|
|
11
11
|
## Basic Layout
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import { urls } from "@rangojs/router
|
|
14
|
+
import { urls } from "@rangojs/router";
|
|
15
15
|
import { Outlet } from "@rangojs/router/client";
|
|
16
16
|
|
|
17
17
|
function ShopLayout() {
|
|
@@ -164,7 +164,7 @@ layout(<CartLayout />, () => [
|
|
|
164
164
|
## Complete Example
|
|
165
165
|
|
|
166
166
|
```typescript
|
|
167
|
-
import { urls } from "@rangojs/router
|
|
167
|
+
import { urls } from "@rangojs/router";
|
|
168
168
|
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
169
169
|
|
|
170
170
|
function ShopLayout() {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: links
|
|
3
|
+
description: URL generation with ctx.href (server), href (client), useHref (mounted), useMount, and scopedHref
|
|
4
|
+
argument-hint: [href|useHref|useMount|scopedHref]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Links & URL Generation
|
|
8
|
+
|
|
9
|
+
@rangojs/router provides different href APIs for server and client contexts.
|
|
10
|
+
|
|
11
|
+
## Server: ctx.href()
|
|
12
|
+
|
|
13
|
+
Available in route handlers via HandlerContext. Resolves named routes using the full route map.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { urls, scopedHref } from "@rangojs/router";
|
|
17
|
+
|
|
18
|
+
export const shopPatterns = urls(({ path, layout }) => [
|
|
19
|
+
layout(<ShopLayout />, () => [
|
|
20
|
+
path("/", ShopIndex, { name: "index" }),
|
|
21
|
+
path("/cart", CartPage, { name: "cart" }),
|
|
22
|
+
path("/product/:slug", ProductPage, { name: "product" }),
|
|
23
|
+
]),
|
|
24
|
+
]);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Resolution priority
|
|
28
|
+
|
|
29
|
+
1. **Path-based** (`/...`) - returned as-is
|
|
30
|
+
2. **Absolute name** (contains dot: `blog.post`) - global lookup
|
|
31
|
+
3. **Local name** (`cart`) - resolved relative to current route's namespace
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// Inside a handler within shopPatterns (mounted at /shop)
|
|
35
|
+
path("/product/:slug", (ctx) => {
|
|
36
|
+
ctx.href("cart"); // "/shop/cart" (local)
|
|
37
|
+
ctx.href("product", { slug: "widget" }); // "/shop/product/widget" (local + params)
|
|
38
|
+
ctx.href("blog.post", { slug: "hi" }); // "/blog/hi" (absolute)
|
|
39
|
+
ctx.href("/about"); // "/about" (path-based)
|
|
40
|
+
|
|
41
|
+
return <ProductPage slug={ctx.params.slug} />;
|
|
42
|
+
}, { name: "product" })
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### scopedHref() - type-safe ctx.href
|
|
46
|
+
|
|
47
|
+
Wraps `ctx.href` with local route type information for autocomplete and validation:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { scopedHref } from "@rangojs/router";
|
|
51
|
+
|
|
52
|
+
path("/product/:slug", (ctx) => {
|
|
53
|
+
const href = scopedHref<typeof shopPatterns>(ctx.href);
|
|
54
|
+
|
|
55
|
+
href("cart"); // Type-safe local name
|
|
56
|
+
href("product", { slug: "widget" }); // Type-safe with params
|
|
57
|
+
href("blog.post"); // Absolute names (dot notation) always allowed
|
|
58
|
+
href("/about"); // Path-based always allowed
|
|
59
|
+
|
|
60
|
+
return <ProductPage slug={ctx.params.slug} />;
|
|
61
|
+
}, { name: "product" })
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Client: href()
|
|
65
|
+
|
|
66
|
+
Plain function for absolute path-based URLs. No hook needed - works anywhere.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
"use client";
|
|
70
|
+
import { href, Link } from "@rangojs/router/client";
|
|
71
|
+
|
|
72
|
+
function GlobalNav() {
|
|
73
|
+
return (
|
|
74
|
+
<nav>
|
|
75
|
+
<Link to={href("/")}>Home</Link>
|
|
76
|
+
<Link to={href("/about")}>About</Link>
|
|
77
|
+
<Link to={href("/blog/hello")}>Post</Link>
|
|
78
|
+
</nav>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`href()` is an identity function at runtime but provides compile-time validation via `ValidPaths` type. Paths are validated against registered route patterns using `PatternToPath`.
|
|
84
|
+
|
|
85
|
+
## Client: useHref()
|
|
86
|
+
|
|
87
|
+
Hook that returns a mount-aware href function. Automatically prepends the `include()` mount prefix.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
"use client";
|
|
91
|
+
import { useHref, href, Link } from "@rangojs/router/client";
|
|
92
|
+
|
|
93
|
+
// Inside include("/shop", shopPatterns)
|
|
94
|
+
function ShopNav() {
|
|
95
|
+
const href = useHref();
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<nav>
|
|
99
|
+
<Link to={href("/")}>Shop Home</Link> {/* "/shop/" */}
|
|
100
|
+
<Link to={href("/cart")}>Cart</Link> {/* "/shop/cart" */}
|
|
101
|
+
<Link to={href("/product/widget")}>W</Link> {/* "/shop/product/widget" */}
|
|
102
|
+
</nav>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use `useHref()` for local navigation within a mounted module. Use the bare `href()` function for absolute paths outside the current mount.
|
|
108
|
+
|
|
109
|
+
## Client: useMount()
|
|
110
|
+
|
|
111
|
+
Returns the current `include()` mount path. Useful for building custom logic based on mount location.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
"use client";
|
|
115
|
+
import { useMount } from "@rangojs/router/client";
|
|
116
|
+
|
|
117
|
+
function MountInfo() {
|
|
118
|
+
const mount = useMount(); // "/shop" inside include("/shop", ...)
|
|
119
|
+
// "/" at root level
|
|
120
|
+
|
|
121
|
+
return <span>Mounted at: {mount}</span>;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`useMount()` reads from `MountContext`, which is automatically set by `include()` in the segment tree.
|
|
126
|
+
|
|
127
|
+
## When to use what
|
|
128
|
+
|
|
129
|
+
| Context | API | Resolves | Use for |
|
|
130
|
+
|---------|-----|----------|---------|
|
|
131
|
+
| Server handler | `ctx.href("name")` | Named routes (local + absolute) | Server-side URL generation |
|
|
132
|
+
| Server handler | `scopedHref<T>(ctx.href)` | Same, with type safety | Type-safe server URLs |
|
|
133
|
+
| Client component | `href("/path")` | Absolute paths | Global navigation |
|
|
134
|
+
| Client component | `useHref()` | Mount-prefixed paths | Local navigation inside `include()` |
|
|
135
|
+
| Client component | `useMount()` | Raw mount path | Custom mount-aware logic |
|
|
136
|
+
|
|
137
|
+
## Complete example: mounted module
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// urls/shop.tsx (server)
|
|
141
|
+
import { urls, scopedHref } from "@rangojs/router";
|
|
142
|
+
|
|
143
|
+
export const shopPatterns = urls(({ path, layout }) => [
|
|
144
|
+
layout((ctx) => {
|
|
145
|
+
const href = scopedHref<typeof shopPatterns>(ctx.href);
|
|
146
|
+
return <ShopLayout cartUrl={href("cart")} />;
|
|
147
|
+
}, () => [
|
|
148
|
+
path("/", ShopIndex, { name: "index" }),
|
|
149
|
+
path("/cart", CartPage, { name: "cart" }),
|
|
150
|
+
path("/product/:slug", ProductPage, { name: "product" }),
|
|
151
|
+
]),
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
// urls.tsx (server)
|
|
155
|
+
export const urlpatterns = urls(({ path, include }) => [
|
|
156
|
+
path("/", HomePage, { name: "home" }),
|
|
157
|
+
include("/shop", shopPatterns),
|
|
158
|
+
]);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
// components/ShopNav.tsx (client)
|
|
163
|
+
"use client";
|
|
164
|
+
import { useHref, href, Link } from "@rangojs/router/client";
|
|
165
|
+
|
|
166
|
+
export function ShopNav() {
|
|
167
|
+
const localHref = useHref();
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<nav>
|
|
171
|
+
{/* Local paths - auto-prefixed with /shop */}
|
|
172
|
+
<Link to={localHref("/cart")}>Cart</Link>
|
|
173
|
+
<Link to={localHref("/product/widget")}>Widget</Link>
|
|
174
|
+
|
|
175
|
+
{/* Absolute path - no prefix */}
|
|
176
|
+
<Link to={href("/")}>Home</Link>
|
|
177
|
+
</nav>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
```
|