@rangojs/router 0.0.0-experimental.ce770c9a → 0.0.0-experimental.d20dd405
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 +188 -35
- package/dist/bin/rango.js +130 -47
- package/dist/vite/index.js +1884 -537
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +7 -5
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +8 -0
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +54 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +45 -0
- package/skills/layout/SKILL.md +24 -0
- package/skills/links/SKILL.md +237 -17
- package/skills/loader/SKILL.md +123 -46
- package/skills/middleware/SKILL.md +36 -3
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/parallel/SKILL.md +135 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/rango/SKILL.md +26 -22
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +79 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +19 -1
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/__internal.ts +1 -1
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +44 -4
- package/src/browser/navigation-bridge.ts +156 -10
- package/src/browser/navigation-client.ts +128 -53
- package/src/browser/navigation-store.ts +68 -9
- package/src/browser/partial-update.ts +94 -15
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +156 -18
- package/src/browser/prefetch/queue.ts +92 -29
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +72 -8
- package/src/browser/react/NavigationProvider.tsx +107 -47
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- 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 +17 -4
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +29 -9
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/rsc-router.tsx +60 -9
- package/src/browser/scroll-restoration.ts +38 -33
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +46 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +211 -72
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +46 -5
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +89 -231
- package/src/context-var.ts +72 -2
- package/src/handle.ts +40 -0
- package/src/href-client.ts +4 -1
- package/src/index.rsc.ts +6 -1
- package/src/index.ts +49 -6
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +62 -15
- package/src/route-definition/dsl-helpers.ts +210 -35
- package/src/route-definition/helpers-types.ts +73 -20
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +9 -1
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/handler-context.ts +77 -38
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/lazy-includes.ts +6 -6
- package/src/router/loader-resolution.ts +159 -21
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +128 -192
- package/src/router/match-handlers.ts +1 -0
- package/src/router/match-middleware/background-revalidation.ts +12 -1
- package/src/router/match-middleware/cache-lookup.ts +70 -10
- package/src/router/match-middleware/cache-store.ts +21 -4
- package/src/router/match-middleware/segment-resolution.ts +53 -0
- package/src/router/match-result.ts +112 -9
- package/src/router/middleware-types.ts +20 -33
- package/src/router/middleware.ts +56 -12
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +15 -1
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +111 -21
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/revalidation.ts +254 -130
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +1 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +55 -7
- package/src/rsc/handler.ts +478 -374
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +18 -2
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +20 -1
- package/src/rsc/server-action.ts +12 -0
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +15 -1
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +71 -70
- package/src/server/context.ts +76 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +185 -57
- package/src/ssr/index.tsx +8 -1
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +145 -68
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +12 -1
- package/src/types/segments.ts +18 -1
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +47 -12
- 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/debug.ts +184 -0
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +36 -4
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +175 -74
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +13 -4
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +60 -5
- package/src/vite/plugins/cjs-to-esm.ts +5 -0
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +16 -4
- 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 +52 -28
- 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-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +563 -316
- package/src/vite/plugins/performance-tracks.ts +96 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/use-cache-transform.ts +56 -43
- package/src/vite/plugins/version-injector.ts +37 -11
- package/src/vite/rango.ts +63 -11
- package/src/vite/router-discovery.ts +732 -86
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +38 -5
- package/src/vite/utils/shared-utils.ts +3 -2
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Named-route RSC router with structural composability and type-safe partial rende
|
|
|
10
10
|
- **Structural composability** — Attach routes, loaders, middleware, handles, caching, prerendering, and static generation without hiding the route tree
|
|
11
11
|
- **Composable URL patterns** — Django-style `urls()` DSL with `path`, `layout`, `include`
|
|
12
12
|
- **Data loaders** — `createLoader()` with automatic streaming and Suspense integration
|
|
13
|
+
- **Server actions** — `"use server"` mutations with `useActionState`, `useOptimistic`, and per-segment + per-loader `revalidate()` rules
|
|
13
14
|
- **Live data layer** — Pre-render or cache the UI shell while loaders stay live by default at request time
|
|
14
15
|
- **Layouts & nesting** — Nested layouts with `<Outlet />` and parallel routes
|
|
15
16
|
- **Segment-level caching** — `cache()` DSL with TTL/SWR and pluggable cache stores
|
|
@@ -91,24 +92,29 @@ This file is a server/RSC module and should import router construction APIs from
|
|
|
91
92
|
|
|
92
93
|
```tsx
|
|
93
94
|
// src/router.tsx
|
|
94
|
-
import { createRouter
|
|
95
|
-
import { Document } from "./document";
|
|
95
|
+
import { createRouter } from "@rangojs/router";
|
|
96
96
|
|
|
97
|
-
const
|
|
98
|
-
path("/",
|
|
99
|
-
path("
|
|
97
|
+
export const router = createRouter().routes(({ path }) => [
|
|
98
|
+
path("/", HomePage, { name: "home" }),
|
|
99
|
+
path("/about", AboutPage, { name: "about" }),
|
|
100
100
|
]);
|
|
101
101
|
|
|
102
|
+
export const reverse = router.reverse;
|
|
103
|
+
// reverse("home") -> "/"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For larger apps, extract route modules with `urls()` and compose with `include()`:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { createRouter, urls } from "@rangojs/router";
|
|
110
|
+
import { blogPatterns } from "./urls/blog";
|
|
111
|
+
|
|
102
112
|
const urlpatterns = urls(({ path, include }) => [
|
|
103
113
|
path("/", HomePage, { name: "home" }),
|
|
104
114
|
include("/blog", blogPatterns, { name: "blog" }),
|
|
105
115
|
]);
|
|
106
116
|
|
|
107
|
-
export const router = createRouter(
|
|
108
|
-
|
|
109
|
-
// Export typed reverse function for URL generation by route name
|
|
110
|
-
export const reverse = router.reverse;
|
|
111
|
-
|
|
117
|
+
export const router = createRouter().routes(urlpatterns);
|
|
112
118
|
// reverse("blog.post", { slug: "hello-world" }) -> "/blog/hello-world"
|
|
113
119
|
```
|
|
114
120
|
|
|
@@ -156,13 +162,18 @@ const urlpatterns = urls(({ path }) => [
|
|
|
156
162
|
]);
|
|
157
163
|
```
|
|
158
164
|
|
|
159
|
-
Use `reverse()` as the default way to link to routes:
|
|
165
|
+
Use `ctx.reverse()` from handler context as the default way to link to routes from server code:
|
|
160
166
|
|
|
161
167
|
```tsx
|
|
162
|
-
|
|
163
|
-
|
|
168
|
+
const ProductPage: Handler<"product"> = (ctx) => {
|
|
169
|
+
const url = ctx.reverse("product", { slug: "widget" }); // "/product/widget"
|
|
170
|
+
const searchUrl = ctx.reverse("search", undefined, { q: "rsc" }); // "/search?q=rsc"
|
|
171
|
+
return <Link to={url}>Widget</Link>;
|
|
172
|
+
};
|
|
164
173
|
```
|
|
165
174
|
|
|
175
|
+
`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.
|
|
176
|
+
|
|
166
177
|
### Composable URL Modules
|
|
167
178
|
|
|
168
179
|
Local route names compose cleanly with `include(..., { name })`:
|
|
@@ -472,41 +483,130 @@ const urlpatterns = urls(({ path, loader }) => [
|
|
|
472
483
|
]);
|
|
473
484
|
```
|
|
474
485
|
|
|
475
|
-
##
|
|
486
|
+
## Server Actions
|
|
487
|
+
|
|
488
|
+
Server actions are React's RSC mutation primitive. Define them with the
|
|
489
|
+
`"use server"` directive — Rango uses standard React 19 hooks
|
|
490
|
+
(`useActionState`, `useFormStatus`, `useOptimistic`) with no framework wrapper.
|
|
476
491
|
|
|
477
|
-
|
|
492
|
+
```tsx
|
|
493
|
+
// app/actions/cart.ts
|
|
494
|
+
"use server";
|
|
495
|
+
|
|
496
|
+
import { getRequestContext } from "@rangojs/router";
|
|
478
497
|
|
|
479
|
-
|
|
498
|
+
export async function addToCart(productId: string): Promise<void> {
|
|
499
|
+
const ctx = getRequestContext();
|
|
500
|
+
const userId = ctx.get("user").id;
|
|
501
|
+
await db.cart.insert({ userId, productId });
|
|
502
|
+
}
|
|
503
|
+
```
|
|
480
504
|
|
|
481
505
|
```tsx
|
|
482
|
-
|
|
483
|
-
|
|
506
|
+
// Client form with progressive enhancement + pending state
|
|
507
|
+
"use client";
|
|
508
|
+
import { useActionState } from "react";
|
|
509
|
+
import { saveProfile } from "../actions/profile";
|
|
484
510
|
|
|
485
|
-
function
|
|
511
|
+
export function ProfileForm() {
|
|
512
|
+
const [state, action, pending] = useActionState(saveProfile, null);
|
|
486
513
|
return (
|
|
487
|
-
<
|
|
488
|
-
<
|
|
489
|
-
<
|
|
490
|
-
<
|
|
491
|
-
</
|
|
514
|
+
<form action={action}>
|
|
515
|
+
<input name="name" defaultValue={state?.values?.name} />
|
|
516
|
+
{state?.errors?.name && <p role="alert">{state.errors.name}</p>}
|
|
517
|
+
<button disabled={pending}>{pending ? "Saving…" : "Save"}</button>
|
|
518
|
+
</form>
|
|
492
519
|
);
|
|
493
520
|
}
|
|
494
521
|
```
|
|
495
522
|
|
|
496
|
-
|
|
523
|
+
After an action runs, matched route segments (path/layout/parallel/intercept)
|
|
524
|
+
and loaders can re-render/re-resolve so the UI reflects the new state.
|
|
525
|
+
Attach a `revalidate(({ actionId }) => ...)` rule on any segment or loader
|
|
526
|
+
that owns data the action touched:
|
|
527
|
+
|
|
528
|
+
```tsx
|
|
529
|
+
urls(({ path, loader, revalidate }) => [
|
|
530
|
+
// Segment-level: re-render the cart page handler after cart actions.
|
|
531
|
+
// Nest loaders that belong to this route inside the same path() so the
|
|
532
|
+
// segment owns its data dependencies.
|
|
533
|
+
path("/cart", CartPage, { name: "cart" }, () => [
|
|
534
|
+
revalidate(
|
|
535
|
+
({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
|
|
536
|
+
),
|
|
537
|
+
loader(CartLoader, () => [
|
|
538
|
+
revalidate(
|
|
539
|
+
({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
|
|
540
|
+
),
|
|
541
|
+
]),
|
|
542
|
+
]),
|
|
543
|
+
]);
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
For the full guide — validation with Zod, error handling, file uploads,
|
|
547
|
+
`useOptimistic`, redirects, and progressive enhancement — see the
|
|
548
|
+
`/server-actions` skill.
|
|
549
|
+
|
|
550
|
+
## Navigation & Links
|
|
551
|
+
|
|
552
|
+
### Named Routes with `ctx.reverse()` (Server)
|
|
497
553
|
|
|
498
|
-
|
|
554
|
+
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:
|
|
499
555
|
|
|
500
556
|
```tsx
|
|
557
|
+
import { Link } from "@rangojs/router/client";
|
|
558
|
+
import type { Handler } from "@rangojs/router";
|
|
559
|
+
|
|
501
560
|
const BlogPostPage: Handler<"blogPost"> = (ctx) => {
|
|
502
561
|
const backUrl = ctx.reverse("blog");
|
|
503
562
|
return <Link to={backUrl}>Back to blog</Link>;
|
|
504
563
|
};
|
|
505
564
|
```
|
|
506
565
|
|
|
566
|
+
`reverse()` is type-safe — route names and required params are checked at compile time. Included routes use dotted names: `ctx.reverse("api.health")`.
|
|
567
|
+
|
|
568
|
+
For scripts, tests, or other code without a handler context, import the router-level `reverse`:
|
|
569
|
+
|
|
570
|
+
```tsx
|
|
571
|
+
import { reverse } from "./router";
|
|
572
|
+
reverse("blogPost", { slug: "my-post" });
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Client Components
|
|
576
|
+
|
|
577
|
+
**`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:
|
|
578
|
+
|
|
579
|
+
```tsx
|
|
580
|
+
// server
|
|
581
|
+
function BlogIndex(ctx: HandlerContext) {
|
|
582
|
+
return (
|
|
583
|
+
<Nav
|
|
584
|
+
home={ctx.reverse("home")}
|
|
585
|
+
post={ctx.reverse("blogPost", { slug: "my-post" })}
|
|
586
|
+
/>
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
```tsx
|
|
592
|
+
"use client";
|
|
593
|
+
import { Link } from "@rangojs/router/client";
|
|
594
|
+
|
|
595
|
+
export function Nav({ home, post }: { home: string; post: string }) {
|
|
596
|
+
return (
|
|
597
|
+
<nav>
|
|
598
|
+
<Link to={home}>Home</Link>
|
|
599
|
+
<Link to={post}>My Post</Link>
|
|
600
|
+
</nav>
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
For client-side navigation to static paths (no named-route lookup), use `href()` — see below. For URLs tied to named routes, you have two options: import the per-module generated `routes` map and use `useReverse(routes)` for in-module names (see [`/links` skill](./skills/links/SKILL.md)), or generate the URL on the server and pass the string in for cross-module URLs.
|
|
606
|
+
|
|
507
607
|
### `href()` for Path Validation (Client Components)
|
|
508
608
|
|
|
509
|
-
In client components, use `href()` for compile-time path validation:
|
|
609
|
+
In client components, use `href()` for compile-time path validation on static path strings:
|
|
510
610
|
|
|
511
611
|
```tsx
|
|
512
612
|
"use client";
|
|
@@ -711,10 +811,12 @@ export const BlogPost = Prerender(
|
|
|
711
811
|
|
|
712
812
|
### Passthrough for Unknown Params
|
|
713
813
|
|
|
814
|
+
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.
|
|
815
|
+
|
|
714
816
|
```tsx
|
|
715
|
-
import { Prerender } from "@rangojs/router";
|
|
817
|
+
import { Prerender, Passthrough } from "@rangojs/router";
|
|
716
818
|
|
|
717
|
-
export const
|
|
819
|
+
export const ProductPageDef = Prerender(
|
|
718
820
|
async () => {
|
|
719
821
|
const featured = await db.getFeaturedProducts();
|
|
720
822
|
return featured.map((p) => ({ id: p.id }));
|
|
@@ -723,16 +825,22 @@ export const ProductPage = Prerender(
|
|
|
723
825
|
const product = await db.getProduct(ctx.params.id);
|
|
724
826
|
return <Product data={product} />;
|
|
725
827
|
},
|
|
726
|
-
{ passthrough: true },
|
|
727
828
|
);
|
|
728
|
-
```
|
|
729
829
|
|
|
730
|
-
|
|
830
|
+
// In route definition:
|
|
831
|
+
path(
|
|
832
|
+
"/products/:id",
|
|
833
|
+
Passthrough(ProductPageDef, async (ctx) => {
|
|
834
|
+
const product = await ctx.env.DB.getProduct(ctx.params.id);
|
|
835
|
+
return <Product data={product} />;
|
|
836
|
+
}),
|
|
837
|
+
);
|
|
838
|
+
```
|
|
731
839
|
|
|
732
|
-
|
|
840
|
+
Build handlers can also skip individual param sets with `ctx.passthrough()`, deferring them to the live handler:
|
|
733
841
|
|
|
734
842
|
```tsx
|
|
735
|
-
export const
|
|
843
|
+
export const ProductPageDef = Prerender(
|
|
736
844
|
async () => {
|
|
737
845
|
const all = await db.getAllProducts();
|
|
738
846
|
return all.map((p) => ({ id: p.id }));
|
|
@@ -742,10 +850,55 @@ export const ProductPage = Prerender(
|
|
|
742
850
|
if (!product.published) return ctx.passthrough();
|
|
743
851
|
return <Product data={product} />;
|
|
744
852
|
},
|
|
745
|
-
{ passthrough: true },
|
|
746
853
|
);
|
|
747
854
|
```
|
|
748
855
|
|
|
856
|
+
### Build-Time Environment Bindings
|
|
857
|
+
|
|
858
|
+
Prerender handlers can access platform bindings (KV, D1, R2) at build time when `buildEnv` is configured in the Vite plugin:
|
|
859
|
+
|
|
860
|
+
```ts
|
|
861
|
+
// vite.config.ts
|
|
862
|
+
import { rango } from "@rangojs/router/vite";
|
|
863
|
+
|
|
864
|
+
rango({ preset: "cloudflare", buildEnv: "auto" });
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
With `buildEnv: "auto"`, the plugin calls `wrangler.getPlatformProxy()` to provide local bindings. Handlers then access `ctx.env` during build:
|
|
868
|
+
|
|
869
|
+
```tsx
|
|
870
|
+
export const BlogPosts = Prerender<{ slug: string }>(
|
|
871
|
+
async (ctx) => {
|
|
872
|
+
const rows = await ctx.env.DB.prepare("SELECT slug FROM posts").all();
|
|
873
|
+
return rows.map((r) => ({ slug: r.slug }));
|
|
874
|
+
},
|
|
875
|
+
async (ctx) => {
|
|
876
|
+
const post = await ctx.env.DB.prepare("SELECT * FROM posts WHERE slug = ?")
|
|
877
|
+
.bind(ctx.params.slug)
|
|
878
|
+
.first();
|
|
879
|
+
return <BlogPost post={post} />;
|
|
880
|
+
},
|
|
881
|
+
);
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
`buildEnv` also accepts a factory function or plain object:
|
|
885
|
+
|
|
886
|
+
```ts
|
|
887
|
+
// Custom factory
|
|
888
|
+
rango({
|
|
889
|
+
buildEnv: async (ctx) => {
|
|
890
|
+
const { getPlatformProxy } = await import("wrangler");
|
|
891
|
+
const proxy = await getPlatformProxy();
|
|
892
|
+
return { env: proxy.env, dispose: proxy.dispose };
|
|
893
|
+
},
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
// Plain object (Node.js)
|
|
897
|
+
rango({ buildEnv: { DATABASE_URL: process.env.DATABASE_URL } });
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
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.
|
|
901
|
+
|
|
749
902
|
## Theme
|
|
750
903
|
|
|
751
904
|
### Router Configuration
|
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;
|
|
@@ -671,7 +674,7 @@ Router root: ${conflict.ancestor}
|
|
|
671
674
|
Nested router: ${conflict.nested}
|
|
672
675
|
Move the nested router into a sibling directory or configure it as a separate app root.`;
|
|
673
676
|
}
|
|
674
|
-
function
|
|
677
|
+
function extractUrlsFromRouter(code) {
|
|
675
678
|
const sourceFile = ts5.createSourceFile(
|
|
676
679
|
"router.tsx",
|
|
677
680
|
code,
|
|
@@ -685,24 +688,70 @@ function extractUrlsVariableFromRouter(code) {
|
|
|
685
688
|
const callee = node.expression;
|
|
686
689
|
return ts5.isIdentifier(callee) && callee.text === "createRouter";
|
|
687
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
|
+
}
|
|
688
703
|
function visit(node) {
|
|
689
704
|
if (result) return;
|
|
690
|
-
if (ts5.isCallExpression(node) &&
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
result = node.arguments[0].text;
|
|
697
|
-
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) };
|
|
698
711
|
}
|
|
712
|
+
return;
|
|
699
713
|
}
|
|
700
714
|
if (isCreateRouterCall(node)) {
|
|
701
715
|
const callExpr = node;
|
|
702
|
-
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) {
|
|
703
752
|
if (ts5.isObjectLiteralExpression(arg)) {
|
|
704
753
|
for (const prop of arg.properties) {
|
|
705
|
-
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)) {
|
|
706
755
|
result = prop.initializer.text;
|
|
707
756
|
return;
|
|
708
757
|
}
|
|
@@ -715,6 +764,19 @@ function extractUrlsVariableFromRouter(code) {
|
|
|
715
764
|
visit(sourceFile);
|
|
716
765
|
return result;
|
|
717
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
|
+
}
|
|
718
780
|
function buildCombinedRouteMapForRouterFile(routerFilePath) {
|
|
719
781
|
let routerSource;
|
|
720
782
|
try {
|
|
@@ -722,19 +784,40 @@ function buildCombinedRouteMapForRouterFile(routerFilePath) {
|
|
|
722
784
|
} catch {
|
|
723
785
|
return { routes: {}, searchSchemas: {} };
|
|
724
786
|
}
|
|
725
|
-
const
|
|
726
|
-
if (!
|
|
787
|
+
const extraction = extractUrlsFromRouter(routerSource);
|
|
788
|
+
if (!extraction) {
|
|
727
789
|
return { routes: {}, searchSchemas: {} };
|
|
728
790
|
}
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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);
|
|
734
815
|
}
|
|
735
|
-
return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
|
|
736
816
|
}
|
|
737
|
-
|
|
817
|
+
if (basename2) {
|
|
818
|
+
result = applyBasenameToRoutes(result, basename2);
|
|
819
|
+
}
|
|
820
|
+
return result;
|
|
738
821
|
}
|
|
739
822
|
function detectUnresolvableIncludes(routerFilePath) {
|
|
740
823
|
const realPath = resolve2(routerFilePath);
|
|
@@ -744,9 +827,20 @@ function detectUnresolvableIncludes(routerFilePath) {
|
|
|
744
827
|
} catch {
|
|
745
828
|
return [];
|
|
746
829
|
}
|
|
747
|
-
const
|
|
748
|
-
if (!
|
|
749
|
-
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);
|
|
750
844
|
let targetFile;
|
|
751
845
|
let exportedName;
|
|
752
846
|
if (imported) {
|
|
@@ -766,9 +860,8 @@ function detectUnresolvableIncludes(routerFilePath) {
|
|
|
766
860
|
exportedName = imported.exportedName;
|
|
767
861
|
} else {
|
|
768
862
|
targetFile = realPath;
|
|
769
|
-
exportedName =
|
|
863
|
+
exportedName = extraction.name;
|
|
770
864
|
}
|
|
771
|
-
const diagnostics = [];
|
|
772
865
|
buildCombinedRouteMapWithSearch(
|
|
773
866
|
targetFile,
|
|
774
867
|
exportedName,
|
|
@@ -816,25 +909,15 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
|
|
|
816
909
|
throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
|
|
817
910
|
}
|
|
818
911
|
for (const routerFilePath of routerFilePaths) {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
routerSource
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const imported = resolveImportedVariable(routerSource, urlsVarName);
|
|
829
|
-
if (imported) {
|
|
830
|
-
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
831
|
-
if (!targetFile) continue;
|
|
832
|
-
result = buildCombinedRouteMapWithSearch(
|
|
833
|
-
targetFile,
|
|
834
|
-
imported.exportedName
|
|
835
|
-
);
|
|
836
|
-
} else {
|
|
837
|
-
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;
|
|
838
921
|
}
|
|
839
922
|
const routerBasename = pathBasename(routerFilePath).replace(
|
|
840
923
|
/\.(tsx?|jsx?)$/,
|