@rangojs/router 0.0.0-experimental.0f44aca1
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/AGENTS.md +5 -0
- package/README.md +899 -0
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +5214 -0
- package/package.json +176 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +220 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +645 -0
- package/src/browser/navigation-client.ts +215 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +550 -0
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +360 -0
- package/src/browser/react/NavigationProvider.tsx +386 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +431 -0
- package/src/browser/scroll-restoration.ts +400 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +538 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +469 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +382 -0
- package/src/cache/cf/cf-cache-store.ts +540 -0
- package/src/cache/cf/index.ts +25 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +43 -0
- package/src/cache/memory-segment-store.ts +328 -0
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -0
- package/src/route-map-builder.ts +275 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +267 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +192 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +316 -0
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1239 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +289 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +170 -0
- package/src/router.ts +1002 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +235 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +914 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -0
- package/src/use-loader.tsx +354 -0
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +131 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/plugins/expose-action-id.ts +365 -0
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +254 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: intercept
|
|
3
|
+
description: Define intercept routes for modals, slide-overs, and soft navigation patterns in @rangojs/router
|
|
4
|
+
argument-hint: [@slot-name] [route-to-intercept]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Intercept Routes
|
|
8
|
+
|
|
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
|
+
|
|
11
|
+
Canonical semantics reference:
|
|
12
|
+
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
13
|
+
|
|
14
|
+
## Basic Intercept
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { urls } from "@rangojs/router";
|
|
18
|
+
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
19
|
+
|
|
20
|
+
function ShopLayout() {
|
|
21
|
+
return (
|
|
22
|
+
<div className="shop">
|
|
23
|
+
<Outlet />
|
|
24
|
+
<ParallelOutlet name="@modal" />
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const urlpatterns = urls(({ path, layout, intercept, loader }) => [
|
|
30
|
+
layout(<ShopLayout />, () => [
|
|
31
|
+
// Intercept product detail - shows modal during soft navigation
|
|
32
|
+
intercept(
|
|
33
|
+
"@modal", // Slot name
|
|
34
|
+
"product", // Route name to intercept
|
|
35
|
+
<ProductModal />, // Modal component
|
|
36
|
+
() => [
|
|
37
|
+
loader(ProductLoader),
|
|
38
|
+
loading(<ProductModalSkeleton />),
|
|
39
|
+
]
|
|
40
|
+
),
|
|
41
|
+
|
|
42
|
+
// Normal routes
|
|
43
|
+
path("/shop", ShopIndex, { name: "index" }),
|
|
44
|
+
path("/shop/product/:slug", ProductPage, { name: "product" }),
|
|
45
|
+
]),
|
|
46
|
+
]);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Navigation Behavior
|
|
50
|
+
|
|
51
|
+
| Navigation Type | What Renders |
|
|
52
|
+
| ------------------------------ | ---------------------------------------------------- |
|
|
53
|
+
| Click link `/shop/product/abc` | `<ProductModal />` in `@modal`, background preserved |
|
|
54
|
+
| Direct URL `/shop/product/abc` | Full `<ProductPage />` page |
|
|
55
|
+
| Browser back | Close modal, restore previous state |
|
|
56
|
+
|
|
57
|
+
## Intercept with Layout
|
|
58
|
+
|
|
59
|
+
Wrap intercept content in a modal layout:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
intercept(
|
|
63
|
+
"@modal",
|
|
64
|
+
"product",
|
|
65
|
+
<ProductModalContent />,
|
|
66
|
+
() => [
|
|
67
|
+
layout(<ModalWrapper />), // Wraps the modal content
|
|
68
|
+
loader(ProductLoader),
|
|
69
|
+
loading(<ProductModalSkeleton />),
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Intercept Middleware
|
|
75
|
+
|
|
76
|
+
Intercepts support their own middleware chain via the use callback. The full chain for an intercept request is:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
global mw (router.use) -> route mw (urls middleware()) -> intercept mw -> intercept handler -> intercept loaders
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
intercept(
|
|
84
|
+
"@modal",
|
|
85
|
+
"product",
|
|
86
|
+
<ProductModal />,
|
|
87
|
+
() => [
|
|
88
|
+
middleware(async (ctx, next) => {
|
|
89
|
+
// Runs only for this intercept, after global and route middleware
|
|
90
|
+
ctx.set("interceptSource", "modal");
|
|
91
|
+
await next();
|
|
92
|
+
}),
|
|
93
|
+
loader(ProductLoader),
|
|
94
|
+
]
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The intercept handler can read context variables set by all upstream middleware layers (global, route, and intercept-specific).
|
|
99
|
+
|
|
100
|
+
Handler/layout `ctx.set()` data follows the same rule as elsewhere:
|
|
101
|
+
intercepts see data produced in the current render pass, but partial
|
|
102
|
+
action revalidation only recomputes segments that actually revalidate.
|
|
103
|
+
If an intercept depends on data established by an outer layout/handler,
|
|
104
|
+
revalidate that outer segment too or reload/guard the data inside the
|
|
105
|
+
intercept.
|
|
106
|
+
|
|
107
|
+
### Revalidation Contracts for Intercept Dependencies
|
|
108
|
+
|
|
109
|
+
Use named revalidation contracts on both the outer producer and the intercept
|
|
110
|
+
consumer when they share `ctx.set()` data:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
export const revalidateProductShell = ({ actionId }) =>
|
|
114
|
+
actionId?.includes("src/actions/product.ts#") ?? false;
|
|
115
|
+
|
|
116
|
+
layout(ProductLayout, () => [
|
|
117
|
+
revalidate(revalidateProductShell), // producer reruns
|
|
118
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
119
|
+
revalidate(revalidateProductShell), // consumer reruns
|
|
120
|
+
loader(ProductLoader),
|
|
121
|
+
]),
|
|
122
|
+
]);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Compose multiple contracts if the intercept depends on multiple upstream
|
|
126
|
+
domains.
|
|
127
|
+
|
|
128
|
+
Helper handoff style keeps intercept trees terse:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { revalidate } from "@rangojs/router";
|
|
132
|
+
|
|
133
|
+
export const revalidateProduct = () => [
|
|
134
|
+
revalidate(revalidateProductShell),
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
layout(ProductLayout, () => [
|
|
138
|
+
revalidateProduct(),
|
|
139
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
140
|
+
revalidateProduct(),
|
|
141
|
+
loader(ProductLoader),
|
|
142
|
+
]),
|
|
143
|
+
]);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Conditional Intercept with when()
|
|
147
|
+
|
|
148
|
+
Only intercept based on navigation context:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
intercept(
|
|
152
|
+
"@modal",
|
|
153
|
+
"product",
|
|
154
|
+
<ProductModal />,
|
|
155
|
+
() => [
|
|
156
|
+
// Only intercept when coming from a different section
|
|
157
|
+
when(({ from }) => !from.pathname.startsWith("/shop/product/")),
|
|
158
|
+
loader(ProductLoader),
|
|
159
|
+
]
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Multiple Loaders in Intercept
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
intercept(
|
|
167
|
+
"@modal",
|
|
168
|
+
"product",
|
|
169
|
+
<ProductModal />,
|
|
170
|
+
() => [
|
|
171
|
+
loader(ProductLoader, () => [cache()]),
|
|
172
|
+
loader(ProductCartLoader, () => [revalidate(() => true)]),
|
|
173
|
+
loader(RecommendationsLoader),
|
|
174
|
+
]
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Closing the Modal
|
|
179
|
+
|
|
180
|
+
Use navigation to close:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
"use client";
|
|
184
|
+
import { useRouter } from "@rangojs/router/client";
|
|
185
|
+
|
|
186
|
+
function ModalWrapper({ children }) {
|
|
187
|
+
const router = useRouter();
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div className="modal-overlay" onClick={() => router.back()}>
|
|
191
|
+
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
|
192
|
+
<button onClick={() => router.back()}>Close</button>
|
|
193
|
+
{children}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Interaction with Prerender
|
|
201
|
+
|
|
202
|
+
When the target route of an intercept uses `Prerender`, the intercept handler is
|
|
203
|
+
also resolved at build time and stored alongside the main pre-rendered segments.
|
|
204
|
+
This means intercept navigations to pre-rendered routes are served from the
|
|
205
|
+
prerender store without executing handler code at runtime.
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// The detail route is pre-rendered
|
|
209
|
+
export const ProductDetail = Prerender(
|
|
210
|
+
async () => [{ slug: "shoes" }, { slug: "jacket" }],
|
|
211
|
+
async (ctx) => <ProductPage slug={ctx.params.slug} />,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// urls.tsx
|
|
215
|
+
layout(ShopLayout, () => [
|
|
216
|
+
path("/:slug", ProductDetail, { name: "detail" }, () => [
|
|
217
|
+
loader(ProductLoader),
|
|
218
|
+
]),
|
|
219
|
+
|
|
220
|
+
// This intercept is also pre-rendered at build time
|
|
221
|
+
intercept("@modal", ".detail", <ProductModal />, () => [
|
|
222
|
+
when(({ from }) => from.pathname.startsWith("/shop")),
|
|
223
|
+
loader(ProductLoader),
|
|
224
|
+
]),
|
|
225
|
+
])
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Build-time behavior:
|
|
229
|
+
|
|
230
|
+
- The intercept handler (`<ProductModal />`) is resolved with BuildContext
|
|
231
|
+
- Result is stored under the key `"detail/paramHash/i"` (intercept variant)
|
|
232
|
+
- `when()` conditions are skipped at build time (all intercepts pre-rendered unconditionally)
|
|
233
|
+
- `when()` is still evaluated at runtime by the intercept-resolution middleware
|
|
234
|
+
|
|
235
|
+
Runtime behavior:
|
|
236
|
+
|
|
237
|
+
- Intercept navigation: prerender store serves the `/i` variant (frozen handler + fresh loaders)
|
|
238
|
+
- Direct navigation: prerender store serves the main variant (full page)
|
|
239
|
+
- If no intercept prerender entry exists, falls through to live intercept resolution
|
|
240
|
+
|
|
241
|
+
Loaders inside the intercept always run fresh at request time, same as regular
|
|
242
|
+
pre-rendered routes.
|
|
243
|
+
|
|
244
|
+
During action-driven partial revalidation, this same partial rule applies:
|
|
245
|
+
refreshing the intercept does not implicitly rebuild non-revalidated outer
|
|
246
|
+
segments.
|
|
247
|
+
|
|
248
|
+
## Complete Example
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
// components/ProductModal.tsx
|
|
252
|
+
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
253
|
+
|
|
254
|
+
function ShopLayout() {
|
|
255
|
+
return (
|
|
256
|
+
<div className="shop">
|
|
257
|
+
<ParallelOutlet name="@promoBanner" />
|
|
258
|
+
<main>
|
|
259
|
+
<Outlet />
|
|
260
|
+
</main>
|
|
261
|
+
<ParallelOutlet name="@modal" />
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function ModalWrapper({ children }) {
|
|
267
|
+
return (
|
|
268
|
+
<div className="modal-overlay">
|
|
269
|
+
<div className="modal">{children}</div>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// urls/shop.tsx
|
|
275
|
+
import { urls } from "@rangojs/router";
|
|
276
|
+
|
|
277
|
+
export const shopPatterns = urls(({
|
|
278
|
+
path,
|
|
279
|
+
layout,
|
|
280
|
+
parallel,
|
|
281
|
+
intercept,
|
|
282
|
+
loader,
|
|
283
|
+
loading,
|
|
284
|
+
when,
|
|
285
|
+
}) => [
|
|
286
|
+
layout(<ShopLayout />, () => [
|
|
287
|
+
parallel({
|
|
288
|
+
"@promoBanner": () => <PromoBanner />,
|
|
289
|
+
}),
|
|
290
|
+
|
|
291
|
+
// Intercept product detail into modal
|
|
292
|
+
intercept(
|
|
293
|
+
"@modal",
|
|
294
|
+
"product", // Route name (without prefix)
|
|
295
|
+
<ProductModalContent />,
|
|
296
|
+
() => [
|
|
297
|
+
when(({ from }) => !from.pathname.startsWith("/shop/product/")),
|
|
298
|
+
layout(<ModalWrapper />),
|
|
299
|
+
loading(<ProductModalSkeleton />),
|
|
300
|
+
loader(ProductLoader, () => [cache()]),
|
|
301
|
+
loader(RecommendationsLoader),
|
|
302
|
+
]
|
|
303
|
+
),
|
|
304
|
+
|
|
305
|
+
// Normal routes
|
|
306
|
+
path("/", ShopIndex, { name: "index" }),
|
|
307
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
308
|
+
loader(ProductLoader),
|
|
309
|
+
loading(<ProductPageSkeleton />),
|
|
310
|
+
]),
|
|
311
|
+
]),
|
|
312
|
+
]);
|
|
313
|
+
```
|