@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.20dbba0c
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 +4 -0
- package/README.md +172 -50
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +1160 -508
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +17 -16
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +49 -8
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +61 -51
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +91 -17
- package/skills/loader/SKILL.md +107 -24
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +765 -0
- package/skills/parallel/SKILL.md +185 -0
- package/skills/prerender/SKILL.md +112 -70
- package/skills/rango/SKILL.md +24 -23
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +58 -4
- package/skills/router-setup/SKILL.md +95 -5
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +38 -24
- package/src/__internal.ts +92 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +175 -17
- package/src/browser/navigation-client.ts +177 -44
- package/src/browser/navigation-store.ts +68 -9
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +113 -17
- package/src/browser/prefetch/cache.ts +275 -28
- package/src/browser/prefetch/fetch.ts +191 -46
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +98 -14
- package/src/browser/react/NavigationProvider.tsx +89 -14
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +11 -1
- package/src/browser/react/use-router.ts +29 -9
- package/src/browser/rsc-router.tsx +177 -66
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +73 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +67 -25
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +223 -74
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +455 -15
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +2 -1
- package/src/client.tsx +85 -276
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +0 -3
- package/src/index.rsc.ts +9 -36
- package/src/index.ts +79 -70
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +138 -77
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +27 -2
- package/src/route-definition/dsl-helpers.ts +240 -40
- package/src/route-definition/helpers-types.ts +67 -19
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +129 -26
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +10 -7
- package/src/router/loader-resolution.ts +160 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -193
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +94 -17
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +103 -18
- package/src/router/metrics.ts +238 -13
- package/src/router/middleware-types.ts +48 -27
- package/src/router/middleware.ts +201 -86
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +77 -11
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +27 -7
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +50 -5
- package/src/router/router-options.ts +50 -19
- package/src/router/segment-resolution/fresh.ts +215 -19
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +454 -301
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/trie-matching.ts +30 -6
- package/src/router/types.ts +1 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +89 -17
- package/src/rsc/handler.ts +563 -364
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +37 -10
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +47 -44
- package/src/rsc/server-action.ts +24 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +11 -1
- package/src/search-params.ts +16 -13
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +109 -23
- package/src/server/context.ts +174 -19
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +218 -65
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +140 -72
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +2 -5
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +18 -16
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +61 -89
- package/src/vite/discovery/discover-routers.ts +7 -4
- package/src/vite/discovery/prerender-collection.ts +162 -88
- package/src/vite/discovery/state.ts +17 -13
- package/src/vite/index.ts +8 -3
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +190 -217
- package/src/vite/router-discovery.ts +241 -45
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/package-resolution.ts +34 -1
- package/src/vite/utils/prerender-utils.ts +97 -5
- package/src/vite/utils/shared-utils.ts +3 -2
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
package/AGENTS.md
CHANGED
|
@@ -3,3 +3,7 @@
|
|
|
3
3
|
A file-system based React Server Components router.
|
|
4
4
|
|
|
5
5
|
Run `/rango` to understand the API. Detailed guides for each feature are in the `skills/` directory (e.g. `node_modules/@rangojs/router/skills/loader`, `skills/caching`, `skills/middleware`, etc.).
|
|
6
|
+
|
|
7
|
+
## Development rules
|
|
8
|
+
|
|
9
|
+
- Always commit generated files (e.g. `*.gen.ts`) alongside the source changes that produced them.
|
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,26 +86,34 @@ 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
|
-
import { createRouter
|
|
68
|
-
import { Document } from "./document";
|
|
94
|
+
import { createRouter } from "@rangojs/router";
|
|
69
95
|
|
|
70
|
-
const
|
|
71
|
-
path("/",
|
|
72
|
-
path("
|
|
96
|
+
export const router = createRouter().routes(({ path }) => [
|
|
97
|
+
path("/", HomePage, { name: "home" }),
|
|
98
|
+
path("/about", AboutPage, { name: "about" }),
|
|
73
99
|
]);
|
|
74
100
|
|
|
101
|
+
export const reverse = router.reverse;
|
|
102
|
+
// reverse("home") -> "/"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For larger apps, extract route modules with `urls()` and compose with `include()`:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { createRouter, urls } from "@rangojs/router";
|
|
109
|
+
import { blogPatterns } from "./urls/blog";
|
|
110
|
+
|
|
75
111
|
const urlpatterns = urls(({ path, include }) => [
|
|
76
112
|
path("/", HomePage, { name: "home" }),
|
|
77
113
|
include("/blog", blogPatterns, { name: "blog" }),
|
|
78
114
|
]);
|
|
79
115
|
|
|
80
|
-
export const router = createRouter(
|
|
81
|
-
|
|
82
|
-
// Export typed reverse function for URL generation by route name
|
|
83
|
-
export const reverse = router.reverse;
|
|
84
|
-
|
|
116
|
+
export const router = createRouter().routes(urlpatterns);
|
|
85
117
|
// reverse("blog.post", { slug: "hello-world" }) -> "/blog/hello-world"
|
|
86
118
|
```
|
|
87
119
|
|
|
@@ -129,13 +161,18 @@ const urlpatterns = urls(({ path }) => [
|
|
|
129
161
|
]);
|
|
130
162
|
```
|
|
131
163
|
|
|
132
|
-
Use `reverse()` as the default way to link to routes:
|
|
164
|
+
Use `ctx.reverse()` from handler context as the default way to link to routes from server code:
|
|
133
165
|
|
|
134
166
|
```tsx
|
|
135
|
-
|
|
136
|
-
|
|
167
|
+
const ProductPage: Handler<"product"> = (ctx) => {
|
|
168
|
+
const url = ctx.reverse("product", { slug: "widget" }); // "/product/widget"
|
|
169
|
+
const searchUrl = ctx.reverse("search", undefined, { q: "rsc" }); // "/search?q=rsc"
|
|
170
|
+
return <Link to={url}>Widget</Link>;
|
|
171
|
+
};
|
|
137
172
|
```
|
|
138
173
|
|
|
174
|
+
`router.reverse()` (exported from the router module) is the same function without a handler context, useful in scripts or tests. In request code, prefer `ctx.reverse()` — it auto-fills mount params from the current match.
|
|
175
|
+
|
|
139
176
|
### Composable URL Modules
|
|
140
177
|
|
|
141
178
|
Local route names compose cleanly with `include(..., { name })`:
|
|
@@ -248,7 +285,8 @@ All handler typing styles are supported, but they solve different problems:
|
|
|
248
285
|
Example of a scoped local name inside a mounted module:
|
|
249
286
|
|
|
250
287
|
```tsx
|
|
251
|
-
import type { Handler
|
|
288
|
+
import type { Handler } from "@rangojs/router";
|
|
289
|
+
import type { ScopedRouteMap } from "@rangojs/router/__internal";
|
|
252
290
|
|
|
253
291
|
type BlogRoutes = ScopedRouteMap<"blog">;
|
|
254
292
|
|
|
@@ -446,39 +484,64 @@ const urlpatterns = urls(({ path, loader }) => [
|
|
|
446
484
|
|
|
447
485
|
## Navigation & Links
|
|
448
486
|
|
|
449
|
-
### Named Routes with `reverse()` (Server
|
|
487
|
+
### Named Routes with `ctx.reverse()` (Server)
|
|
450
488
|
|
|
451
|
-
In server components, use `reverse()` to generate URLs by route name:
|
|
489
|
+
In server components and handlers, use `ctx.reverse()` to generate URLs by route name. This is the default — it is typed, auto-fills mount params from the current match, and resolves both local (`.name`) and absolute (`name.sub`) names:
|
|
452
490
|
|
|
453
491
|
```tsx
|
|
454
492
|
import { Link } from "@rangojs/router/client";
|
|
493
|
+
import type { Handler } from "@rangojs/router";
|
|
494
|
+
|
|
495
|
+
const BlogPostPage: Handler<"blogPost"> = (ctx) => {
|
|
496
|
+
const backUrl = ctx.reverse("blog");
|
|
497
|
+
return <Link to={backUrl}>Back to blog</Link>;
|
|
498
|
+
};
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
`reverse()` is type-safe — route names and required params are checked at compile time. Included routes use dotted names: `ctx.reverse("api.health")`.
|
|
502
|
+
|
|
503
|
+
For scripts, tests, or other code without a handler context, import the router-level `reverse`:
|
|
504
|
+
|
|
505
|
+
```tsx
|
|
455
506
|
import { reverse } from "./router";
|
|
507
|
+
reverse("blogPost", { slug: "my-post" });
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Client Components
|
|
511
|
+
|
|
512
|
+
**`reverse()` is server-only.** It depends on the route manifest and handler context — neither is available in the browser bundle. Client components receive URLs as props, loader data, or server-action return values:
|
|
456
513
|
|
|
457
|
-
|
|
514
|
+
```tsx
|
|
515
|
+
// server
|
|
516
|
+
function BlogIndex(ctx: HandlerContext) {
|
|
458
517
|
return (
|
|
459
|
-
<
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
</nav>
|
|
518
|
+
<Nav
|
|
519
|
+
home={ctx.reverse("home")}
|
|
520
|
+
post={ctx.reverse("blogPost", { slug: "my-post" })}
|
|
521
|
+
/>
|
|
464
522
|
);
|
|
465
523
|
}
|
|
466
524
|
```
|
|
467
525
|
|
|
468
|
-
`reverse()` is type-safe — route names and required params are checked at compile time. Included routes use dotted names: `reverse("api.health")`.
|
|
469
|
-
|
|
470
|
-
Handlers also have `ctx.reverse()` directly on the context:
|
|
471
|
-
|
|
472
526
|
```tsx
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
};
|
|
527
|
+
"use client";
|
|
528
|
+
import { Link } from "@rangojs/router/client";
|
|
529
|
+
|
|
530
|
+
export function Nav({ home, post }: { home: string; post: string }) {
|
|
531
|
+
return (
|
|
532
|
+
<nav>
|
|
533
|
+
<Link to={home}>Home</Link>
|
|
534
|
+
<Link to={post}>My Post</Link>
|
|
535
|
+
</nav>
|
|
536
|
+
);
|
|
537
|
+
}
|
|
477
538
|
```
|
|
478
539
|
|
|
540
|
+
For client-side navigation to static paths (no named-route lookup), use `href()` — see below. For URLs tied to named routes, always generate on the server and pass the string in.
|
|
541
|
+
|
|
479
542
|
### `href()` for Path Validation (Client Components)
|
|
480
543
|
|
|
481
|
-
In client components, use `href()` for compile-time path validation:
|
|
544
|
+
In client components, use `href()` for compile-time path validation on static path strings:
|
|
482
545
|
|
|
483
546
|
```tsx
|
|
484
547
|
"use client";
|
|
@@ -488,7 +551,7 @@ function Nav() {
|
|
|
488
551
|
return (
|
|
489
552
|
<nav>
|
|
490
553
|
<Link to={href("/")}>Home</Link>
|
|
491
|
-
<Link to={href("/blog")} prefetch="
|
|
554
|
+
<Link to={href("/blog")} prefetch="adaptive">
|
|
492
555
|
Blog
|
|
493
556
|
</Link>
|
|
494
557
|
<Link to={href("/about")}>About</Link>
|
|
@@ -683,10 +746,12 @@ export const BlogPost = Prerender(
|
|
|
683
746
|
|
|
684
747
|
### Passthrough for Unknown Params
|
|
685
748
|
|
|
749
|
+
Wrap a `Prerender` definition with `Passthrough()` to add a live handler for unknown params at runtime. The build handler runs at build time, the live handler runs at request time for params not in the prerender cache.
|
|
750
|
+
|
|
686
751
|
```tsx
|
|
687
|
-
import { Prerender } from "@rangojs/router";
|
|
752
|
+
import { Prerender, Passthrough } from "@rangojs/router";
|
|
688
753
|
|
|
689
|
-
export const
|
|
754
|
+
export const ProductPageDef = Prerender(
|
|
690
755
|
async () => {
|
|
691
756
|
const featured = await db.getFeaturedProducts();
|
|
692
757
|
return featured.map((p) => ({ id: p.id }));
|
|
@@ -695,16 +760,22 @@ export const ProductPage = Prerender(
|
|
|
695
760
|
const product = await db.getProduct(ctx.params.id);
|
|
696
761
|
return <Product data={product} />;
|
|
697
762
|
},
|
|
698
|
-
{ passthrough: true },
|
|
699
763
|
);
|
|
700
|
-
```
|
|
701
764
|
|
|
702
|
-
|
|
765
|
+
// In route definition:
|
|
766
|
+
path(
|
|
767
|
+
"/products/:id",
|
|
768
|
+
Passthrough(ProductPageDef, async (ctx) => {
|
|
769
|
+
const product = await ctx.env.DB.getProduct(ctx.params.id);
|
|
770
|
+
return <Product data={product} />;
|
|
771
|
+
}),
|
|
772
|
+
);
|
|
773
|
+
```
|
|
703
774
|
|
|
704
|
-
|
|
775
|
+
Build handlers can also skip individual param sets with `ctx.passthrough()`, deferring them to the live handler:
|
|
705
776
|
|
|
706
777
|
```tsx
|
|
707
|
-
export const
|
|
778
|
+
export const ProductPageDef = Prerender(
|
|
708
779
|
async () => {
|
|
709
780
|
const all = await db.getAllProducts();
|
|
710
781
|
return all.map((p) => ({ id: p.id }));
|
|
@@ -714,10 +785,55 @@ export const ProductPage = Prerender(
|
|
|
714
785
|
if (!product.published) return ctx.passthrough();
|
|
715
786
|
return <Product data={product} />;
|
|
716
787
|
},
|
|
717
|
-
{ passthrough: true },
|
|
718
788
|
);
|
|
719
789
|
```
|
|
720
790
|
|
|
791
|
+
### Build-Time Environment Bindings
|
|
792
|
+
|
|
793
|
+
Prerender handlers can access platform bindings (KV, D1, R2) at build time when `buildEnv` is configured in the Vite plugin:
|
|
794
|
+
|
|
795
|
+
```ts
|
|
796
|
+
// vite.config.ts
|
|
797
|
+
import { rango } from "@rangojs/router/vite";
|
|
798
|
+
|
|
799
|
+
rango({ preset: "cloudflare", buildEnv: "auto" });
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
With `buildEnv: "auto"`, the plugin calls `wrangler.getPlatformProxy()` to provide local bindings. Handlers then access `ctx.env` during build:
|
|
803
|
+
|
|
804
|
+
```tsx
|
|
805
|
+
export const BlogPosts = Prerender<{ slug: string }>(
|
|
806
|
+
async (ctx) => {
|
|
807
|
+
const rows = await ctx.env.DB.prepare("SELECT slug FROM posts").all();
|
|
808
|
+
return rows.map((r) => ({ slug: r.slug }));
|
|
809
|
+
},
|
|
810
|
+
async (ctx) => {
|
|
811
|
+
const post = await ctx.env.DB.prepare("SELECT * FROM posts WHERE slug = ?")
|
|
812
|
+
.bind(ctx.params.slug)
|
|
813
|
+
.first();
|
|
814
|
+
return <BlogPost post={post} />;
|
|
815
|
+
},
|
|
816
|
+
);
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
`buildEnv` also accepts a factory function or plain object:
|
|
820
|
+
|
|
821
|
+
```ts
|
|
822
|
+
// Custom factory
|
|
823
|
+
rango({
|
|
824
|
+
buildEnv: async (ctx) => {
|
|
825
|
+
const { getPlatformProxy } = await import("wrangler");
|
|
826
|
+
const proxy = await getPlatformProxy();
|
|
827
|
+
return { env: proxy.env, dispose: proxy.dispose };
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// Plain object (Node.js)
|
|
832
|
+
rango({ buildEnv: { DATABASE_URL: process.env.DATABASE_URL } });
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
Build-time env applies to both production builds and dev on-demand prerender. Without `buildEnv`, accessing `ctx.env` in a Prerender handler throws with a clear error.
|
|
836
|
+
|
|
721
837
|
## Theme
|
|
722
838
|
|
|
723
839
|
### Router Configuration
|
|
@@ -842,16 +958,22 @@ module, use `scopedReverse<typeof localPatterns>(ctx.reverse)` or
|
|
|
842
958
|
|
|
843
959
|
## Subpath Exports
|
|
844
960
|
|
|
845
|
-
| Export | Description
|
|
846
|
-
| ------------------------ |
|
|
847
|
-
| `@rangojs/router` |
|
|
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`
|
|
854
|
-
| `@rangojs/router/
|
|
961
|
+
| Export | Description |
|
|
962
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------- |
|
|
963
|
+
| `@rangojs/router` | Server/RSC core and shared types: `createRouter`, `urls`, `createLoader`, `Handler`, `Prerender`, `Meta` |
|
|
964
|
+
| `@rangojs/router/client` | Client: `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `MetaTags` |
|
|
965
|
+
| `@rangojs/router/cache` | Cache: `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware` |
|
|
966
|
+
| `@rangojs/router/theme` | Theme: `useTheme`, `ThemeProvider`, `ThemeScript` |
|
|
967
|
+
| `@rangojs/router/host` | Host routing: `createHostRouter`, `defineHosts` |
|
|
968
|
+
| `@rangojs/router/vite` | Vite plugin: `rango()` |
|
|
969
|
+
| `@rangojs/router/rsc` | Advanced server pipeline APIs: `createRSCHandler`, request-context access |
|
|
970
|
+
| `@rangojs/router/ssr` | Advanced SSR bridge APIs: `createSSRHandler` |
|
|
971
|
+
| `@rangojs/router/server` | Internal build/runtime utilities for advanced integrations |
|
|
972
|
+
| `@rangojs/router/build` | Build utilities |
|
|
973
|
+
|
|
974
|
+
The root entrypoint is not a generic client/runtime barrel. If you need hooks
|
|
975
|
+
or components, import from `@rangojs/router/client`; if you need cache or host
|
|
976
|
+
APIs, use their dedicated subpaths.
|
|
855
977
|
|
|
856
978
|
## Examples
|
|
857
979
|
|
package/dist/bin/rango.js
CHANGED
|
@@ -218,7 +218,8 @@ function findTsFiles(dir, filter) {
|
|
|
218
218
|
for (const entry of entries) {
|
|
219
219
|
const fullPath = join(dir, entry.name);
|
|
220
220
|
if (entry.isDirectory()) {
|
|
221
|
-
if (entry.name === "node_modules" || entry.name.startsWith("."))
|
|
221
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".") || entry.name === "dist" || entry.name === "build" || entry.name === "coverage")
|
|
222
|
+
continue;
|
|
222
223
|
results.push(...findTsFiles(fullPath, filter));
|
|
223
224
|
} else if ((entry.name.endsWith(".ts") || entry.name.endsWith(".tsx") || entry.name.endsWith(".js") || entry.name.endsWith(".jsx")) && !entry.name.includes(".gen.")) {
|
|
224
225
|
if (filter && !filter(fullPath)) continue;
|
|
@@ -450,7 +451,7 @@ function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSche
|
|
|
450
451
|
}
|
|
451
452
|
return routeMap;
|
|
452
453
|
}
|
|
453
|
-
function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut) {
|
|
454
|
+
function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut, inlineBlock) {
|
|
454
455
|
visited = visited ?? /* @__PURE__ */ new Set();
|
|
455
456
|
const realPath = resolve(filePath);
|
|
456
457
|
const key = variableName ? `${realPath}:${variableName}` : realPath;
|
|
@@ -466,7 +467,9 @@ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagno
|
|
|
466
467
|
return { routes: {}, searchSchemas: {} };
|
|
467
468
|
}
|
|
468
469
|
let block;
|
|
469
|
-
if (
|
|
470
|
+
if (inlineBlock) {
|
|
471
|
+
block = inlineBlock;
|
|
472
|
+
} else if (variableName) {
|
|
470
473
|
const extracted = extractUrlsBlockForVariable(source, variableName);
|
|
471
474
|
if (!extracted) return { routes: {}, searchSchemas: {} };
|
|
472
475
|
block = extracted;
|
|
@@ -601,7 +604,7 @@ function countPublicRouteEntries(source) {
|
|
|
601
604
|
return count;
|
|
602
605
|
}
|
|
603
606
|
function isRoutableSourceFile(name) {
|
|
604
|
-
return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !name.includes(".gen.");
|
|
607
|
+
return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !name.includes(".gen.") && !name.includes(".test.") && !name.includes(".spec.");
|
|
605
608
|
}
|
|
606
609
|
function findRouterFilesRecursive(dir, filter, results) {
|
|
607
610
|
let entries;
|
|
@@ -618,7 +621,8 @@ function findRouterFilesRecursive(dir, filter, results) {
|
|
|
618
621
|
for (const entry of entries) {
|
|
619
622
|
const fullPath = join2(dir, entry.name);
|
|
620
623
|
if (entry.isDirectory()) {
|
|
621
|
-
if (entry.name === "node_modules" || entry.name.startsWith("."))
|
|
624
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage" || entry.name === "__tests__" || entry.name === "__mocks__" || entry.name.startsWith("."))
|
|
625
|
+
continue;
|
|
622
626
|
childDirs.push(fullPath);
|
|
623
627
|
continue;
|
|
624
628
|
}
|
|
@@ -670,7 +674,7 @@ Router root: ${conflict.ancestor}
|
|
|
670
674
|
Nested router: ${conflict.nested}
|
|
671
675
|
Move the nested router into a sibling directory or configure it as a separate app root.`;
|
|
672
676
|
}
|
|
673
|
-
function
|
|
677
|
+
function extractUrlsFromRouter(code) {
|
|
674
678
|
const sourceFile = ts5.createSourceFile(
|
|
675
679
|
"router.tsx",
|
|
676
680
|
code,
|
|
@@ -684,24 +688,70 @@ function extractUrlsVariableFromRouter(code) {
|
|
|
684
688
|
const callee = node.expression;
|
|
685
689
|
return ts5.isIdentifier(callee) && callee.text === "createRouter";
|
|
686
690
|
}
|
|
691
|
+
function isInlineBuilder(node) {
|
|
692
|
+
return ts5.isArrowFunction(node) || ts5.isFunctionExpression(node);
|
|
693
|
+
}
|
|
694
|
+
function isRoutesOnCreateRouter(node) {
|
|
695
|
+
if (!ts5.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "routes")
|
|
696
|
+
return false;
|
|
697
|
+
let inner = node.expression.expression;
|
|
698
|
+
while (ts5.isCallExpression(inner) && ts5.isPropertyAccessExpression(inner.expression)) {
|
|
699
|
+
inner = inner.expression.expression;
|
|
700
|
+
}
|
|
701
|
+
return isCreateRouterCall(inner);
|
|
702
|
+
}
|
|
687
703
|
function visit(node) {
|
|
688
704
|
if (result) return;
|
|
689
|
-
if (ts5.isCallExpression(node) &&
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
result = node.arguments[0].text;
|
|
696
|
-
return;
|
|
705
|
+
if (ts5.isCallExpression(node) && node.arguments.length >= 1 && isRoutesOnCreateRouter(node)) {
|
|
706
|
+
const arg = node.arguments[0];
|
|
707
|
+
if (ts5.isIdentifier(arg)) {
|
|
708
|
+
result = { kind: "variable", name: arg.text };
|
|
709
|
+
} else if (isInlineBuilder(arg)) {
|
|
710
|
+
result = { kind: "inline", block: arg.getText(sourceFile) };
|
|
697
711
|
}
|
|
712
|
+
return;
|
|
698
713
|
}
|
|
699
714
|
if (isCreateRouterCall(node)) {
|
|
700
715
|
const callExpr = node;
|
|
701
|
-
for (const
|
|
716
|
+
for (const callArg of callExpr.arguments) {
|
|
717
|
+
if (ts5.isObjectLiteralExpression(callArg)) {
|
|
718
|
+
for (const prop of callArg.properties) {
|
|
719
|
+
if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "urls") {
|
|
720
|
+
if (ts5.isIdentifier(prop.initializer)) {
|
|
721
|
+
result = { kind: "variable", name: prop.initializer.text };
|
|
722
|
+
} else if (isInlineBuilder(prop.initializer)) {
|
|
723
|
+
result = {
|
|
724
|
+
kind: "inline",
|
|
725
|
+
block: prop.initializer.getText(sourceFile)
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
ts5.forEachChild(node, visit);
|
|
735
|
+
}
|
|
736
|
+
visit(sourceFile);
|
|
737
|
+
return result;
|
|
738
|
+
}
|
|
739
|
+
function extractBasenameFromRouter(code) {
|
|
740
|
+
const sourceFile = ts5.createSourceFile(
|
|
741
|
+
"router.tsx",
|
|
742
|
+
code,
|
|
743
|
+
ts5.ScriptTarget.Latest,
|
|
744
|
+
true,
|
|
745
|
+
ts5.ScriptKind.TSX
|
|
746
|
+
);
|
|
747
|
+
let result;
|
|
748
|
+
function visit(node) {
|
|
749
|
+
if (result !== void 0) return;
|
|
750
|
+
if (ts5.isCallExpression(node) && ts5.isIdentifier(node.expression) && node.expression.text === "createRouter") {
|
|
751
|
+
for (const arg of node.arguments) {
|
|
702
752
|
if (ts5.isObjectLiteralExpression(arg)) {
|
|
703
753
|
for (const prop of arg.properties) {
|
|
704
|
-
if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "
|
|
754
|
+
if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "basename" && ts5.isStringLiteral(prop.initializer)) {
|
|
705
755
|
result = prop.initializer.text;
|
|
706
756
|
return;
|
|
707
757
|
}
|
|
@@ -714,6 +764,19 @@ function extractUrlsVariableFromRouter(code) {
|
|
|
714
764
|
visit(sourceFile);
|
|
715
765
|
return result;
|
|
716
766
|
}
|
|
767
|
+
function applyBasenameToRoutes(result, basename2) {
|
|
768
|
+
const prefixed = {};
|
|
769
|
+
for (const [name, pattern] of Object.entries(result.routes)) {
|
|
770
|
+
if (pattern === "/") {
|
|
771
|
+
prefixed[name] = basename2;
|
|
772
|
+
} else if (basename2.endsWith("/") && pattern.startsWith("/")) {
|
|
773
|
+
prefixed[name] = basename2 + pattern.slice(1);
|
|
774
|
+
} else {
|
|
775
|
+
prefixed[name] = basename2 + pattern;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return { routes: prefixed, searchSchemas: result.searchSchemas };
|
|
779
|
+
}
|
|
717
780
|
function buildCombinedRouteMapForRouterFile(routerFilePath) {
|
|
718
781
|
let routerSource;
|
|
719
782
|
try {
|
|
@@ -721,19 +784,40 @@ function buildCombinedRouteMapForRouterFile(routerFilePath) {
|
|
|
721
784
|
} catch {
|
|
722
785
|
return { routes: {}, searchSchemas: {} };
|
|
723
786
|
}
|
|
724
|
-
const
|
|
725
|
-
if (!
|
|
787
|
+
const extraction = extractUrlsFromRouter(routerSource);
|
|
788
|
+
if (!extraction) {
|
|
726
789
|
return { routes: {}, searchSchemas: {} };
|
|
727
790
|
}
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
791
|
+
const rawBasename = extractBasenameFromRouter(routerSource);
|
|
792
|
+
const basename2 = rawBasename ? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "") : void 0;
|
|
793
|
+
let result;
|
|
794
|
+
if (extraction.kind === "inline") {
|
|
795
|
+
result = buildCombinedRouteMapWithSearch(
|
|
796
|
+
routerFilePath,
|
|
797
|
+
void 0,
|
|
798
|
+
void 0,
|
|
799
|
+
void 0,
|
|
800
|
+
extraction.block
|
|
801
|
+
);
|
|
802
|
+
} else {
|
|
803
|
+
const imported = resolveImportedVariable(routerSource, extraction.name);
|
|
804
|
+
if (imported) {
|
|
805
|
+
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
806
|
+
if (!targetFile) {
|
|
807
|
+
return { routes: {}, searchSchemas: {} };
|
|
808
|
+
}
|
|
809
|
+
result = buildCombinedRouteMapWithSearch(
|
|
810
|
+
targetFile,
|
|
811
|
+
imported.exportedName
|
|
812
|
+
);
|
|
813
|
+
} else {
|
|
814
|
+
result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
|
|
733
815
|
}
|
|
734
|
-
return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
|
|
735
816
|
}
|
|
736
|
-
|
|
817
|
+
if (basename2) {
|
|
818
|
+
result = applyBasenameToRoutes(result, basename2);
|
|
819
|
+
}
|
|
820
|
+
return result;
|
|
737
821
|
}
|
|
738
822
|
function detectUnresolvableIncludes(routerFilePath) {
|
|
739
823
|
const realPath = resolve2(routerFilePath);
|
|
@@ -743,9 +827,20 @@ function detectUnresolvableIncludes(routerFilePath) {
|
|
|
743
827
|
} catch {
|
|
744
828
|
return [];
|
|
745
829
|
}
|
|
746
|
-
const
|
|
747
|
-
if (!
|
|
748
|
-
const
|
|
830
|
+
const extraction = extractUrlsFromRouter(source);
|
|
831
|
+
if (!extraction) return [];
|
|
832
|
+
const diagnostics = [];
|
|
833
|
+
if (extraction.kind === "inline") {
|
|
834
|
+
buildCombinedRouteMapWithSearch(
|
|
835
|
+
realPath,
|
|
836
|
+
void 0,
|
|
837
|
+
/* @__PURE__ */ new Set(),
|
|
838
|
+
diagnostics,
|
|
839
|
+
extraction.block
|
|
840
|
+
);
|
|
841
|
+
return diagnostics;
|
|
842
|
+
}
|
|
843
|
+
const imported = resolveImportedVariable(source, extraction.name);
|
|
749
844
|
let targetFile;
|
|
750
845
|
let exportedName;
|
|
751
846
|
if (imported) {
|
|
@@ -765,9 +860,8 @@ function detectUnresolvableIncludes(routerFilePath) {
|
|
|
765
860
|
exportedName = imported.exportedName;
|
|
766
861
|
} else {
|
|
767
862
|
targetFile = realPath;
|
|
768
|
-
exportedName =
|
|
863
|
+
exportedName = extraction.name;
|
|
769
864
|
}
|
|
770
|
-
const diagnostics = [];
|
|
771
865
|
buildCombinedRouteMapWithSearch(
|
|
772
866
|
targetFile,
|
|
773
867
|
exportedName,
|
|
@@ -815,25 +909,15 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
|
|
|
815
909
|
throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
|
|
816
910
|
}
|
|
817
911
|
for (const routerFilePath of routerFilePaths) {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
routerSource
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const imported = resolveImportedVariable(routerSource, urlsVarName);
|
|
828
|
-
if (imported) {
|
|
829
|
-
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
830
|
-
if (!targetFile) continue;
|
|
831
|
-
result = buildCombinedRouteMapWithSearch(
|
|
832
|
-
targetFile,
|
|
833
|
-
imported.exportedName
|
|
834
|
-
);
|
|
835
|
-
} else {
|
|
836
|
-
result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
|
|
912
|
+
const result = buildCombinedRouteMapForRouterFile(routerFilePath);
|
|
913
|
+
if (Object.keys(result.routes).length === 0 && Object.keys(result.searchSchemas).length === 0) {
|
|
914
|
+
let routerSource;
|
|
915
|
+
try {
|
|
916
|
+
routerSource = readFileSync3(routerFilePath, "utf-8");
|
|
917
|
+
} catch {
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
if (!extractUrlsFromRouter(routerSource)) continue;
|
|
837
921
|
}
|
|
838
922
|
const routerBasename = pathBasename(routerFilePath).replace(
|
|
839
923
|
/\.(tsx?|jsx?)$/,
|
|
@@ -1062,8 +1146,9 @@ function createVersionPlugin() {
|
|
|
1062
1146
|
let isDev = false;
|
|
1063
1147
|
let server = null;
|
|
1064
1148
|
const clientModuleSignatures = /* @__PURE__ */ new Map();
|
|
1149
|
+
let versionCounter = 0;
|
|
1065
1150
|
const bumpVersion = (reason) => {
|
|
1066
|
-
currentVersion = Date.now().toString(16);
|
|
1151
|
+
currentVersion = Date.now().toString(16) + String(++versionCounter);
|
|
1067
1152
|
console.log(`[rsc-router] ${reason}, version updated: ${currentVersion}`);
|
|
1068
1153
|
const rscEnv = server?.environments?.rsc;
|
|
1069
1154
|
const versionMod = rscEnv?.moduleGraph?.getModuleById(
|
|
@@ -1119,6 +1204,9 @@ function createVersionPlugin() {
|
|
|
1119
1204
|
if (!isDev) return;
|
|
1120
1205
|
const isRscModule = this.environment?.name === "rsc";
|
|
1121
1206
|
if (!isRscModule) return;
|
|
1207
|
+
if (ctx.modules.length === 1 && ctx.modules[0].id === "\0" + VIRTUAL_IDS.version) {
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1122
1210
|
if (isCodeModule(ctx.file)) {
|
|
1123
1211
|
const filePath = normalizeModuleId(ctx.file);
|
|
1124
1212
|
const previousSignature = clientModuleSignatures.get(filePath);
|