@rangojs/router 0.0.0-experimental.21 → 0.0.0-experimental.22

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 CHANGED
@@ -45,6 +45,30 @@ For Cloudflare Workers:
45
45
  npm install @cloudflare/vite-plugin
46
46
  ```
47
47
 
48
+ ## Import Paths
49
+
50
+ Use these import paths consistently:
51
+
52
+ - `@rangojs/router` — server/RSC router APIs, route DSL, `createRouter`, `urls`, `redirect`, `Prerender`, `Static`, shared types
53
+ - `@rangojs/router/client` — hooks and components such as `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `useAction`, `useLocationState`
54
+ - `@rangojs/router/cache` — public cache APIs such as `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware`
55
+ - `@rangojs/router/host`, `@rangojs/router/theme`, `@rangojs/router/vite` — specialized public subpaths
56
+ - `@rangojs/router/rsc`, `@rangojs/router/ssr` — advanced server-only integration subpaths for custom request/HTML pipelines
57
+
58
+ Use only subpaths that are explicitly exported from the package. Avoid deep imports such as `@rangojs/router/cache/cf`.
59
+
60
+ `@rangojs/router` is conditionally resolved. Server-only root APIs such as
61
+ `createRouter()`, `urls()`, `redirect()`, `Prerender()`, and `cookies()` rely on
62
+ the `react-server` export condition and are meant to run in router definitions,
63
+ handlers, and other RSC/server modules. Outside that environment the root entry
64
+ falls back to stub implementations that throw guidance errors.
65
+
66
+ If you hit a root-entrypoint stub error:
67
+
68
+ - hooks and components like `Link`, `Outlet`, `useLoader`, `useNavigation`, and `MetaTags` belong in `@rangojs/router/client`
69
+ - cache APIs like `CFCacheStore` and `createDocumentCacheMiddleware` belong in `@rangojs/router/cache`
70
+ - host-router APIs belong in `@rangojs/router/host`
71
+
48
72
  ## Quick Start
49
73
 
50
74
  ### Vite Config
@@ -62,6 +86,9 @@ export default defineConfig({
62
86
 
63
87
  ### Router
64
88
 
89
+ This file is a server/RSC module and should import router construction APIs from
90
+ `@rangojs/router`.
91
+
65
92
  ```tsx
66
93
  // src/router.tsx
67
94
  import { createRouter, urls } from "@rangojs/router";
@@ -842,16 +869,22 @@ module, use `scopedReverse<typeof localPatterns>(ctx.reverse)` or
842
869
 
843
870
  ## Subpath Exports
844
871
 
845
- | Export | Description |
846
- | ------------------------ | --------------------------------------------------------------------------------- |
847
- | `@rangojs/router` | Core: `createRouter`, `urls`, `createLoader`, `Handler`, `Prerender`, `Meta` |
848
- | `@rangojs/router/client` | Client: `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `MetaTags` |
849
- | `@rangojs/router/cache` | Cache: `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware` |
850
- | `@rangojs/router/theme` | Theme: `useTheme`, `ThemeProvider`, `ThemeScript` |
851
- | `@rangojs/router/host` | Host routing: `createHostRouter`, `defineHosts` |
852
- | `@rangojs/router/vite` | Vite plugin: `rango()` |
853
- | `@rangojs/router/server` | Server utilities |
854
- | `@rangojs/router/build` | Build utilities |
872
+ | Export | Description |
873
+ | ------------------------ | -------------------------------------------------------------------------------------------------------- |
874
+ | `@rangojs/router` | Server/RSC core and shared types: `createRouter`, `urls`, `createLoader`, `Handler`, `Prerender`, `Meta` |
875
+ | `@rangojs/router/client` | Client: `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `MetaTags` |
876
+ | `@rangojs/router/cache` | Cache: `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware` |
877
+ | `@rangojs/router/theme` | Theme: `useTheme`, `ThemeProvider`, `ThemeScript` |
878
+ | `@rangojs/router/host` | Host routing: `createHostRouter`, `defineHosts` |
879
+ | `@rangojs/router/vite` | Vite plugin: `rango()` |
880
+ | `@rangojs/router/rsc` | Advanced server pipeline APIs: `createRSCHandler`, request-context access |
881
+ | `@rangojs/router/ssr` | Advanced SSR bridge APIs: `createSSRHandler` |
882
+ | `@rangojs/router/server` | Internal build/runtime utilities for advanced integrations |
883
+ | `@rangojs/router/build` | Build utilities |
884
+
885
+ The root entrypoint is not a generic client/runtime barrel. If you need hooks
886
+ or components, import from `@rangojs/router/client`; if you need cache or host
887
+ APIs, use their dedicated subpaths.
855
888
 
856
889
  ## Examples
857
890
 
@@ -1745,7 +1745,7 @@ import { resolve } from "node:path";
1745
1745
  // package.json
1746
1746
  var package_default = {
1747
1747
  name: "@rangojs/router",
1748
- version: "0.0.0-experimental.21",
1748
+ version: "0.0.0-experimental.22",
1749
1749
  description: "Django-inspired RSC router with composable URL patterns",
1750
1750
  keywords: [
1751
1751
  "react",
@@ -3733,8 +3733,7 @@ async function discoverRouters(state, rscEnv) {
3733
3733
  let registry = serverMod.RouterRegistry;
3734
3734
  if (!registry || registry.size === 0) {
3735
3735
  try {
3736
- const hostMod = await rscEnv.runner.import("@rangojs/router/host");
3737
- const hostRegistry = hostMod.HostRouterRegistry;
3736
+ const hostRegistry = serverMod.HostRouterRegistry;
3738
3737
  if (hostRegistry && hostRegistry.size > 0) {
3739
3738
  console.log(
3740
3739
  `[rsc-router] Found ${hostRegistry.size} host router(s), resolving lazy handlers...`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.21",
3
+ "version": "0.0.0-experimental.22",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -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/rsc";
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/rsc";
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/cf";
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/rsc";
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/cf";
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/cf";
137
+ import { CFCacheStore } from "@rangojs/router/cache";
138
138
  import { urlpatterns } from "./urls";
139
139
 
140
140
  const router = createRouter<AppBindings>({
@@ -6,7 +6,8 @@ argument-hint: [hook-name]
6
6
 
7
7
  # Client-Side React Hooks
8
8
 
9
- All hooks are imported from `@rangojs/router` or `@rangojs/router/client`.
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() {
@@ -244,7 +245,7 @@ Get all loader data in current context:
244
245
 
245
246
  ```tsx
246
247
  "use client";
247
- import { useLoaderData } from "@rangojs/router";
248
+ import { useLoaderData } from "@rangojs/router/client";
248
249
 
249
250
  function DebugPanel() {
250
251
  const allData = useLoaderData();
@@ -262,7 +263,7 @@ Access accumulated handle data from route segments:
262
263
 
263
264
  ```tsx
264
265
  "use client";
265
- import { useHandle } from "@rangojs/router";
266
+ import { useHandle } from "@rangojs/router/client";
266
267
  import { Breadcrumbs } from "../handles/breadcrumbs";
267
268
 
268
269
  function BreadcrumbNav() {
@@ -324,7 +325,7 @@ Track state of server action invocations:
324
325
 
325
326
  ```tsx
326
327
  "use client";
327
- import { useAction } from "@rangojs/router";
328
+ import { useAction } from "@rangojs/router/client";
328
329
  import { addToCart } from "../actions/cart";
329
330
 
330
331
  function AddToCartButton({ productId }: { productId: string }) {
@@ -359,7 +360,7 @@ Read type-safe state from history:
359
360
 
360
361
  ```tsx
361
362
  "use client";
362
- import { useLocationState, createLocationState } from "@rangojs/router";
363
+ import { useLocationState, createLocationState } from "@rangojs/router/client";
363
364
 
364
365
  // Define typed state (all export patterns supported)
365
366
  // Keys are auto-injected by the Vite plugin -- no manual key needed.
@@ -509,7 +510,7 @@ Manually control client-side navigation cache:
509
510
 
510
511
  ```tsx
511
512
  "use client";
512
- import { useClientCache } from "@rangojs/router";
513
+ import { useClientCache } from "@rangojs/router/client";
513
514
 
514
515
  function SaveButton() {
515
516
  const { clear } = useClientCache();
@@ -537,7 +538,7 @@ function SaveButton() {
537
538
  Render child content in layouts:
538
539
 
539
540
  ```tsx
540
- import { Outlet, ParallelOutlet } from "@rangojs/router";
541
+ import { Outlet, ParallelOutlet } from "@rangojs/router/client";
541
542
 
542
543
  function DashboardLayout({ children }: { children?: React.ReactNode }) {
543
544
  return (
@@ -558,7 +559,7 @@ Access outlet content programmatically:
558
559
 
559
560
  ```tsx
560
561
  "use client";
561
- import { useOutlet } from "@rangojs/router";
562
+ import { useOutlet } from "@rangojs/router/client";
562
563
 
563
564
  function ConditionalLayout() {
564
565
  const outlet = useOutlet();
@@ -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
+ ```
@@ -68,7 +68,7 @@ export const urlpatterns = urls(({ path, loader }) => [
68
68
  ### In Server Components
69
69
 
70
70
  ```typescript
71
- import { useLoader } from "@rangojs/router";
71
+ import { useLoader } from "@rangojs/router/client";
72
72
  import { ProductLoader } from "./loaders/product";
73
73
 
74
74
  async function ProductPage() {
@@ -539,7 +539,7 @@ export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalid
539
539
  ]);
540
540
 
541
541
  // pages/product.tsx
542
- import { useLoader } from "@rangojs/router";
542
+ import { useLoader } from "@rangojs/router/client";
543
543
  import { ProductLoader, CartLoader } from "./loaders/shop";
544
544
 
545
545
  async function ProductPage() {
@@ -521,10 +521,10 @@ export const guidesPatterns = urls(({ path }) => [
521
521
  ]);
522
522
 
523
523
  // urls.tsx
524
- import { urls, include } from "@rangojs/router";
524
+ import { urls } from "@rangojs/router";
525
525
  import { guidesPatterns } from "./pages/guides.js";
526
526
 
527
- export const urlpatterns = urls(({ path }) => [
527
+ export const urlpatterns = urls(({ path, include }) => [
528
528
  path("/", HomePage, { name: "home" }),
529
529
  include("/guides", guidesPatterns, { name: "guides" }),
530
530
  ]);
@@ -32,7 +32,6 @@ Django-inspired RSC router with composable URL patterns, type-safe href, and ser
32
32
  | `/response-routes` | JSON/text/HTML/XML/stream endpoints with `path.json()`, `path.text()` |
33
33
  | `/mime-routes` | Content negotiation — same URL, different response types via Accept header |
34
34
  | `/fonts` | Load web fonts with preload hints |
35
- | `/testing` | Unit test route trees with `buildRouteTree()` |
36
35
 
37
36
  ## Quick Start
38
37
 
@@ -297,10 +297,10 @@ export const shopPatterns = urls(({ path, layout }) => [
297
297
  ]);
298
298
 
299
299
  // src/urls.tsx
300
- import { urls, include } from "@rangojs/router";
300
+ import { urls } from "@rangojs/router";
301
301
  import { shopPatterns } from "./urls/shop";
302
302
 
303
- export const urlpatterns = urls(({ path }) => [
303
+ export const urlpatterns = urls(({ path, include }) => [
304
304
  path("/", HomePage, { name: "home" }),
305
305
  include("/shop", shopPatterns, { name: "shop" }),
306
306
  ]);
@@ -334,7 +334,7 @@ export const ProductLoader = createLoader(async (ctx) => {
334
334
  });
335
335
 
336
336
  // In server component - type is inferred
337
- import { useLoader } from "@rangojs/router";
337
+ import { useLoader } from "@rangojs/router/client";
338
338
 
339
339
  async function ProductPage() {
340
340
  const product = await useLoader(ProductLoader);
package/src/host/index.ts CHANGED
@@ -25,9 +25,6 @@
25
25
  // Core router
26
26
  export { createHostRouter } from "./router.js";
27
27
 
28
- // Host router registry for build-time discovery
29
- export { HostRouterRegistry, type HostRouterRegistryEntry } from "./router.js";
30
-
31
28
  // Utilities
32
29
  export { defineHosts } from "./utils.js";
33
30
 
package/src/index.ts CHANGED
@@ -115,25 +115,32 @@ export type {
115
115
  // Middleware context types
116
116
  export type { MiddlewareContext, CookieOptions } from "./router/middleware.js";
117
117
 
118
+ function serverOnlyStubError(name: string): Error {
119
+ return new Error(
120
+ `${name}() is only available from "@rangojs/router" in a react-server/RSC environment. ` +
121
+ `For client hooks and components, import from "@rangojs/router/client".`,
122
+ );
123
+ }
124
+
118
125
  /**
119
126
  * Error-throwing stub for server-only `urls` function.
120
127
  */
121
128
  export function urls(): never {
122
- throw new Error("urls() is server-only and requires RSC context.");
129
+ throw serverOnlyStubError("urls");
123
130
  }
124
131
 
125
132
  /**
126
133
  * Error-throwing stub for server-only `createRouter` function.
127
134
  */
128
135
  export function createRouter(): never {
129
- throw new Error("createRouter() is server-only and requires RSC context.");
136
+ throw serverOnlyStubError("createRouter");
130
137
  }
131
138
 
132
139
  /**
133
140
  * Error-throwing stub for server-only `redirect` function.
134
141
  */
135
142
  export function redirect(): never {
136
- throw new Error("redirect() is server-only and requires RSC context.");
143
+ throw serverOnlyStubError("redirect");
137
144
  }
138
145
 
139
146
  // Handle API (universal - works on both server and client)
@@ -149,102 +156,94 @@ export { nonce } from "./rsc/nonce.js";
149
156
  * Error-throwing stub for server-only `Prerender` function.
150
157
  */
151
158
  export function Prerender(): never {
152
- throw new Error("Prerender() is server-only and requires RSC context.");
159
+ throw serverOnlyStubError("Prerender");
153
160
  }
154
161
 
155
162
  /**
156
163
  * Error-throwing stub for server-only `Static` function.
157
164
  */
158
165
  export function Static(): never {
159
- throw new Error("Static() is server-only and requires RSC context.");
166
+ throw serverOnlyStubError("Static");
160
167
  }
161
168
 
162
169
  /**
163
170
  * Error-throwing stub for server-only `getRequestContext` function.
164
171
  */
165
172
  export function getRequestContext(): never {
166
- throw new Error(
167
- "getRequestContext() is server-only and requires RSC context.",
168
- );
173
+ throw serverOnlyStubError("getRequestContext");
169
174
  }
170
175
 
171
176
  /**
172
177
  * Error-throwing stub for server-only `cookies` function.
173
178
  */
174
179
  export function cookies(): never {
175
- throw new Error("cookies() is server-only and requires RSC context.");
180
+ throw serverOnlyStubError("cookies");
176
181
  }
177
182
 
178
183
  /**
179
184
  * Error-throwing stub for server-only `headers` function.
180
185
  */
181
186
  export function headers(): never {
182
- throw new Error("headers() is server-only and requires RSC context.");
187
+ throw serverOnlyStubError("headers");
183
188
  }
184
189
 
185
190
  /**
186
191
  * Error-throwing stub for server-only `createReverse` function.
187
192
  */
188
193
  export function createReverse(): never {
189
- throw new Error("createReverse() is server-only and requires RSC context.");
194
+ throw serverOnlyStubError("createReverse");
190
195
  }
191
196
 
192
197
  /**
193
198
  * Error-throwing stub for server-only `enableMatchDebug` function.
194
199
  */
195
200
  export function enableMatchDebug(): never {
196
- throw new Error(
197
- "enableMatchDebug() is server-only and requires RSC context.",
198
- );
201
+ throw serverOnlyStubError("enableMatchDebug");
199
202
  }
200
203
 
201
204
  /**
202
205
  * Error-throwing stub for server-only `getMatchDebugStats` function.
203
206
  */
204
207
  export function getMatchDebugStats(): never {
205
- throw new Error(
206
- "getMatchDebugStats() is server-only and requires RSC context.",
207
- );
208
+ throw serverOnlyStubError("getMatchDebugStats");
208
209
  }
209
210
 
210
211
  // Error-throwing stubs for server-only route helpers
211
212
  export function layout(): never {
212
- throw new Error("layout() is server-only and requires RSC context.");
213
+ throw serverOnlyStubError("layout");
213
214
  }
214
215
  export function cache(): never {
215
- throw new Error("cache() is server-only and requires RSC context.");
216
+ throw serverOnlyStubError("cache");
216
217
  }
217
218
  export function middleware(): never {
218
- throw new Error("middleware() is server-only and requires RSC context.");
219
+ throw serverOnlyStubError("middleware");
219
220
  }
220
221
  export function revalidate(): never {
221
- throw new Error("revalidate() is server-only and requires RSC context.");
222
+ throw serverOnlyStubError("revalidate");
222
223
  }
223
224
  export function loader(): never {
224
- throw new Error("loader() is server-only and requires RSC context.");
225
+ throw serverOnlyStubError("loader");
225
226
  }
226
227
  export function loading(): never {
227
- throw new Error("loading() is server-only and requires RSC context.");
228
+ throw serverOnlyStubError("loading");
228
229
  }
229
230
  export function parallel(): never {
230
- throw new Error("parallel() is server-only and requires RSC context.");
231
+ throw serverOnlyStubError("parallel");
231
232
  }
232
233
  export function intercept(): never {
233
- throw new Error("intercept() is server-only and requires RSC context.");
234
+ throw serverOnlyStubError("intercept");
234
235
  }
235
236
  export function when(): never {
236
- throw new Error("when() is server-only and requires RSC context.");
237
+ throw serverOnlyStubError("when");
237
238
  }
238
239
  export function errorBoundary(): never {
239
- throw new Error("errorBoundary() is server-only and requires RSC context.");
240
+ throw serverOnlyStubError("errorBoundary");
240
241
  }
241
242
  export function notFoundBoundary(): never {
242
- throw new Error(
243
- "notFoundBoundary() is server-only and requires RSC context.",
244
- );
243
+ throw serverOnlyStubError("notFoundBoundary");
245
244
  }
246
245
  export function transition(): never {
247
- throw new Error("transition() is server-only and requires RSC context.");
246
+ throw serverOnlyStubError("transition");
248
247
  }
249
248
 
250
249
  // Request context type (safe for client)
@@ -1,6 +1,3 @@
1
- // Route definition
2
- export { route, type RouteDefinitionResult } from "./route-function.js";
3
-
4
1
  // Type definitions
5
2
  export type { RouteHelpers } from "./helpers-types.js";
6
3
  export type {
@@ -239,7 +239,7 @@ export interface RSCRouterOptions<TEnv = any> {
239
239
  *
240
240
  * @example Static config
241
241
  * ```typescript
242
- * import { MemorySegmentCacheStore } from "rsc-router/rsc";
242
+ * import { MemorySegmentCacheStore } from "@rangojs/router/cache";
243
243
  *
244
244
  * const router = createRouter({
245
245
  * cache: {
package/src/rsc/index.ts CHANGED
@@ -29,28 +29,8 @@ export type {
29
29
  NonceProvider,
30
30
  } from "./types.js";
31
31
 
32
- // Re-export HandleStore types for consumers who need custom handling
33
- export {
34
- createHandleStore,
35
- type HandleStore,
36
- type HandleData,
37
- } from "../server/handle-store.js";
38
-
39
32
  // Re-export request context utilities for server-side access to env/request/params
40
33
  export {
41
34
  getRequestContext,
42
35
  requireRequestContext,
43
- setRequestContextParams,
44
36
  } from "../server/request-context.js";
45
-
46
- // Re-export cache store types and implementations
47
- export type {
48
- SegmentCacheStore,
49
- CachedEntryData,
50
- CachedEntryResult,
51
- SegmentCacheProvider,
52
- SegmentHandleData,
53
- } from "../cache/types.js";
54
-
55
- export { MemorySegmentCacheStore } from "../cache/memory-segment-store.js";
56
- export { CFCacheStore, type CFCacheStoreOptions } from "../cache/cf/index.js";
package/src/server.ts CHANGED
@@ -11,6 +11,12 @@
11
11
  // Router registry (used by Vite plugin for build-time discovery)
12
12
  export { RSC_ROUTER_BRAND, RouterRegistry } from "./router.js";
13
13
 
14
+ // Host router registry (used by Vite plugin for host-router lazy discovery)
15
+ export {
16
+ HostRouterRegistry,
17
+ type HostRouterRegistryEntry,
18
+ } from "./host/router.js";
19
+
14
20
  // Route map builder (Vite plugin injects these via virtual modules)
15
21
  export {
16
22
  registerRouteMap,
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Theme module exports for @rangojs/router/theme
3
3
  *
4
- * This module provides theme management for rsc-router:
4
+ * This module provides the public theme API:
5
5
  * - useTheme: Hook for accessing theme state in client components
6
6
  * - ThemeProvider: Component for manual theme provider setup (typically not needed)
7
+ * - ThemeScript: FOUC-prevention script component for document/head usage
7
8
  * - Types for theme configuration
8
9
  *
9
10
  * @example
@@ -43,15 +44,5 @@ export type {
43
44
  ThemeContextValue,
44
45
  } from "./types.js";
45
46
 
46
- // Constants (for advanced use cases)
47
- export {
48
- THEME_DEFAULTS,
49
- THEME_COOKIE,
50
- resolveThemeConfig,
51
- } from "./constants.js";
52
-
53
- // Script generation (for advanced SSR use cases)
54
- export { generateThemeScript, getNonceAttribute } from "./theme-script.js";
55
-
56
- // Context (for advanced use cases)
57
- export { ThemeContext, useThemeContext } from "./theme-context.js";
47
+ // Constants
48
+ export { THEME_DEFAULTS, THEME_COOKIE } from "./constants.js";
@@ -48,9 +48,8 @@ export async function discoverRouters(
48
48
  // No RSC routers found directly. Check for host routers with lazy handlers
49
49
  // that need to be resolved to trigger sub-app createRouter() calls.
50
50
  try {
51
- const hostMod = await rscEnv.runner.import("@rangojs/router/host");
52
51
  const hostRegistry: Map<string, any> | undefined =
53
- hostMod.HostRouterRegistry;
52
+ serverMod.HostRouterRegistry;
54
53
 
55
54
  if (hostRegistry && hostRegistry.size > 0) {
56
55
  console.log(
@@ -89,7 +88,7 @@ export async function discoverRouters(
89
88
  }
90
89
  }
91
90
  } catch {
92
- // @rangojs/router/host not available or import failed, skip
91
+ // Host-router discovery is best-effort; skip if unavailable
93
92
  }
94
93
 
95
94
  // If still no routers after host router resolution, fail
@@ -1,226 +0,0 @@
1
- ---
2
- name: testing
3
- description: Unit test route trees with buildRouteTree()
4
- argument-hint:
5
- ---
6
-
7
- # Route Tree Unit Testing
8
-
9
- Unit test route definitions by inspecting the route tree, segment IDs, middleware, intercepts, loaders, and pattern matching without running a dev server.
10
-
11
- ## Setup
12
-
13
- The `buildRouteTree` helper lives in `src/__tests__/helpers/route-tree.ts` (not shipped with npm). Import it in your test files:
14
-
15
- ```typescript
16
- import { buildRouteTree } from "./helpers/route-tree.js";
17
- ```
18
-
19
- ## buildRouteTree(urlPatterns)
20
-
21
- Takes a `urls()` result and returns a `RouteTree` with inspection methods:
22
-
23
- ```typescript
24
- import { urls } from "@rangojs/router";
25
- import { buildRouteTree } from "./helpers/route-tree.js";
26
-
27
- const tree = buildRouteTree(
28
- urls(({ path, layout, middleware, loader, intercept, when }) => [
29
- layout(RootLayout, () => [
30
- middleware(authMiddleware),
31
- path("/", HomePage, { name: "home" }),
32
- path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
33
- loader(PostLoader),
34
- ]),
35
- ]),
36
- ]),
37
- );
38
- ```
39
-
40
- ## RouteTree API
41
-
42
- ### Route Patterns
43
-
44
- ```typescript
45
- tree.routes(); // { home: "/", "blog.post": "/blog/:slug" }
46
- tree.routeNames(); // ["home", "blog.post"]
47
- ```
48
-
49
- ### URL Matching
50
-
51
- ```typescript
52
- const m = tree.match("/blog/hello");
53
- m.routeKey; // "blog.post"
54
- m.params; // { slug: "hello" }
55
-
56
- tree.match("/nonexistent"); // null
57
- ```
58
-
59
- ### Segment IDs
60
-
61
- ```typescript
62
- tree.segmentId("home"); // "M0L0L0R0"
63
- tree.segmentIds(); // { home: "M0L0L0R0", "blog.post": "M0L0L0R1" }
64
- tree.segmentPath("blog.post");
65
- // [
66
- // { id: "M0L0", type: "layout" }, // synthetic root
67
- // { id: "M0L0L0", type: "layout" }, // RootLayout
68
- // { id: "M0L0L0R1", type: "route", pattern: "/blog/:slug" },
69
- // ]
70
- ```
71
-
72
- ### Entry Access
73
-
74
- ```typescript
75
- tree.entry("blog.post"); // EntryData
76
- tree.entry("blog.post")!.parent!.type; // "layout"
77
- tree.entryByPattern("/blog/:slug"); // EntryData (lookup by URL pattern)
78
- ```
79
-
80
- ### Middleware
81
-
82
- ```typescript
83
- tree.hasMiddleware("home"); // true (inherited from layout)
84
- tree.middleware("home"); // [authMiddleware] (direct only)
85
- tree.middlewareChain("home");
86
- // [{ segmentId: "M0L0L0", count: 1 }] // all middleware root-to-route
87
- ```
88
-
89
- ### Loaders
90
-
91
- ```typescript
92
- tree.hasLoaders("blog.post"); // true
93
- tree.loaders("blog.post"); // [LoaderEntry { loader, revalidate, cache? }]
94
- ```
95
-
96
- ### Intercepts
97
-
98
- ```typescript
99
- tree.intercepts("home");
100
- // [{ slotName: "@modal", routeName: "card", hasWhen: true, whenCount: 1, hasLoader: false, hasMiddleware: false }]
101
- tree.interceptEntries("home"); // raw InterceptEntry[]
102
- ```
103
-
104
- ### Parallel Slots
105
-
106
- ```typescript
107
- tree.parallelSlots("home"); // EntryData[] of type="parallel"
108
- tree.parallelSlotNames("home"); // ["@sidebar", "@main"]
109
- ```
110
-
111
- ### Boundaries
112
-
113
- ```typescript
114
- tree.hasErrorBoundary("home"); // boolean
115
- tree.hasNotFoundBoundary("home"); // boolean
116
- ```
117
-
118
- ### Cache & Loading
119
-
120
- ```typescript
121
- tree.hasCache("home"); // boolean
122
- tree.hasLoading("home"); // boolean
123
- ```
124
-
125
- ### Debug
126
-
127
- ```typescript
128
- console.log(tree.debug());
129
- // Route Tree:
130
- // home: / [M0L0L0R0] (M0L0 > M0L0L0 > M0L0L0R0) {mw:1}
131
- // blog.post: /blog/:slug [M0L0L0R1] (M0L0 > M0L0L0 > M0L0L0R1) {mw:1, ld:1}
132
- ```
133
-
134
- ## Segment ID Format
135
-
136
- | Prefix | Meaning |
137
- | ------ | ----------------------------- |
138
- | `M0` | Mount index (router instance) |
139
- | `L` | Layout |
140
- | `R` | Route |
141
- | `P` | Parallel slot |
142
- | `D` | Loader (data) |
143
- | `C` | Cache boundary |
144
-
145
- Example: `M0L0L0R1` = mount 0, synthetic root layout, user layout, second route.
146
-
147
- ## Examples
148
-
149
- ### include() with prefix
150
-
151
- ```typescript
152
- const blogPatterns = urls(({ path }) => [
153
- path("/", BlogIndex, { name: "index" }),
154
- path("/:slug", BlogPost, { name: "post" }),
155
- ]);
156
-
157
- const tree = buildRouteTree(
158
- urls(({ path, include }) => [
159
- path("/", HomePage, { name: "home" }),
160
- include("/blog", blogPatterns, { name: "blog" }),
161
- ]),
162
- );
163
-
164
- expect(tree.routes()).toEqual({
165
- home: "/",
166
- "blog.index": "/blog",
167
- "blog.post": "/blog/:slug",
168
- });
169
- ```
170
-
171
- ### Middleware chain
172
-
173
- ```typescript
174
- const authMw = async (ctx, next) => next();
175
- const logMw = async (ctx, next) => next();
176
-
177
- const tree = buildRouteTree(
178
- urls(({ path, layout, middleware }) => [
179
- layout(RootLayout, () => [
180
- middleware(logMw),
181
- layout(AuthLayout, () => [
182
- middleware(authMw),
183
- path("/dashboard", Dashboard, { name: "dashboard" }),
184
- ]),
185
- ]),
186
- ]),
187
- );
188
-
189
- expect(tree.middlewareChain("dashboard")).toEqual([
190
- { segmentId: "M0L0L0", count: 1 }, // logMw on RootLayout
191
- { segmentId: "M0L0L0L0", count: 1 }, // authMw on AuthLayout
192
- ]);
193
- ```
194
-
195
- ### Intercepts with when()
196
-
197
- ```typescript
198
- const tree = buildRouteTree(
199
- urls(({ path, layout, intercept, when }) => [
200
- layout(ShopLayout, () => [
201
- path("/products", ProductList, { name: "products" }),
202
- path("/products/:id", ProductDetail, { name: "product.detail" }),
203
- intercept("@modal", "product.detail", ProductModal, () => [
204
- when((ctx) => ctx.from.pathname.startsWith("/products")),
205
- ]),
206
- ]),
207
- ]),
208
- );
209
-
210
- const intercepts = tree.intercepts("products");
211
- // Note: intercepts are on the parent where intercept() is called
212
- ```
213
-
214
- ### Constrained parameters
215
-
216
- ```typescript
217
- const tree = buildRouteTree(
218
- urls(({ path }) => [
219
- path("/:locale(en|fr)?/about", AboutPage, { name: "about" }),
220
- ]),
221
- );
222
-
223
- expect(tree.match("/about")).not.toBeNull();
224
- expect(tree.match("/fr/about")!.params).toEqual({ locale: "fr" });
225
- expect(tree.match("/de/about")).toBeNull();
226
- ```
@@ -1,119 +0,0 @@
1
- import type {
2
- ResolvedRouteMap,
3
- RouteConfig,
4
- RouteDefinition,
5
- RouteDefinitionOptions,
6
- TrailingSlashMode,
7
- } from "../types.js";
8
-
9
- /**
10
- * Result of route() function with paths and trailing slash config
11
- */
12
- export interface RouteDefinitionResult<T extends RouteDefinition> {
13
- routes: ResolvedRouteMap<T>;
14
- trailingSlash: Record<string, TrailingSlashMode>;
15
- }
16
-
17
- /**
18
- * Check if a value is a RouteConfig object
19
- */
20
- function isRouteConfig(value: unknown): value is RouteConfig {
21
- return (
22
- typeof value === "object" &&
23
- value !== null &&
24
- "path" in value &&
25
- typeof (value as RouteConfig).path === "string"
26
- );
27
- }
28
-
29
- /**
30
- * Define routes with optional trailing slash configuration
31
- *
32
- * @example
33
- * ```typescript
34
- * // Simple string paths
35
- * const routes = route({
36
- * blog: "/blog",
37
- * post: "/blog/:id",
38
- * });
39
- *
40
- * // With trailing slash config
41
- * const routes = route({
42
- * blog: "/blog",
43
- * api: { path: "/api", trailingSlash: "ignore" },
44
- * }, { trailingSlash: "never" }); // global default
45
- * ```
46
- */
47
- export function route<const T extends RouteDefinition>(
48
- input: T,
49
- options?: RouteDefinitionOptions,
50
- ): ResolvedRouteMap<T> & {
51
- __trailingSlash?: Record<string, TrailingSlashMode>;
52
- } {
53
- const trailingSlash: Record<string, TrailingSlashMode> = {};
54
- const routes = flattenRoutes(
55
- input as RouteDefinition,
56
- "",
57
- trailingSlash,
58
- options?.trailingSlash,
59
- );
60
-
61
- // Attach trailing slash config as a non-enumerable property
62
- // This keeps backwards compatibility while passing the config through
63
- const result = routes as ResolvedRouteMap<T> & {
64
- __trailingSlash?: Record<string, TrailingSlashMode>;
65
- };
66
- if (Object.keys(trailingSlash).length > 0) {
67
- Object.defineProperty(result, "__trailingSlash", {
68
- value: trailingSlash,
69
- enumerable: false,
70
- writable: false,
71
- });
72
- }
73
-
74
- return result;
75
- }
76
-
77
- /**
78
- * Flatten nested route definitions
79
- */
80
- function flattenRoutes(
81
- routes: RouteDefinition,
82
- prefix: string,
83
- trailingSlashConfig: Record<string, TrailingSlashMode>,
84
- defaultTrailingSlash?: TrailingSlashMode,
85
- ): Record<string, string> {
86
- const flattened: Record<string, string> = {};
87
-
88
- for (const [key, value] of Object.entries(routes)) {
89
- const fullKey = prefix + key;
90
-
91
- if (typeof value === "string") {
92
- // Direct route pattern - include prefix
93
- flattened[fullKey] = value;
94
- // Apply default trailing slash if set
95
- if (defaultTrailingSlash) {
96
- trailingSlashConfig[fullKey] = defaultTrailingSlash;
97
- }
98
- } else if (isRouteConfig(value)) {
99
- // Route config object with path and optional trailingSlash
100
- flattened[fullKey] = value.path;
101
- // Use route-specific config or fall back to default
102
- const mode = value.trailingSlash ?? defaultTrailingSlash;
103
- if (mode) {
104
- trailingSlashConfig[fullKey] = mode;
105
- }
106
- } else {
107
- // Nested routes - flatten recursively
108
- const nested = flattenRoutes(
109
- value,
110
- `${fullKey}.`,
111
- trailingSlashConfig,
112
- defaultTrailingSlash,
113
- );
114
- Object.assign(flattened, nested);
115
- }
116
- }
117
-
118
- return flattened;
119
- }