@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1fa245e2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{CLAUDE.md → AGENTS.md} +4 -0
- package/README.md +122 -30
- package/dist/bin/rango.js +245 -63
- package/dist/vite/index.js +859 -418
- package/package.json +3 -3
- package/skills/breadcrumbs/SKILL.md +250 -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/hooks/SKILL.md +33 -31
- package/skills/host-router/SKILL.md +218 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +72 -22
- package/skills/middleware/SKILL.md +2 -0
- package/skills/parallel/SKILL.md +126 -0
- package/skills/prerender/SKILL.md +112 -70
- package/skills/rango/SKILL.md +0 -1
- package/skills/route/SKILL.md +34 -4
- package/skills/router-setup/SKILL.md +95 -5
- package/skills/typesafety/SKILL.md +35 -23
- package/src/__internal.ts +92 -0
- package/src/bin/rango.ts +18 -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 +114 -18
- package/src/browser/navigation-client.ts +126 -44
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +80 -15
- package/src/browser/prefetch/cache.ts +166 -27
- package/src/browser/prefetch/fetch.ts +52 -39
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +92 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +70 -14
- package/src/browser/react/NavigationProvider.tsx +40 -4
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +143 -59
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/browser/server-action-bridge.ts +454 -436
- package/src/browser/types.ts +60 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +346 -87
- 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 +453 -11
- 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 +3 -102
- 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 +8 -37
- package/src/index.ts +40 -66
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +22 -1
- package/src/route-definition/dsl-helpers.ts +73 -25
- package/src/route-definition/helpers-types.ts +10 -6
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +11 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +108 -25
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +123 -11
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-api.ts +125 -190
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +88 -16
- 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 +22 -15
- package/src/router/metrics.ts +238 -13
- package/src/router/middleware-types.ts +53 -12
- package/src/router/middleware.ts +172 -85
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +20 -5
- 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 +200 -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 +429 -301
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/trie-matching.ts +20 -2
- package/src/router/types.ts +1 -0
- package/src/router.ts +88 -15
- package/src/rsc/handler.ts +546 -359
- package/src/rsc/index.ts +0 -20
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +25 -8
- package/src/rsc/rsc-rendering.ts +35 -43
- package/src/rsc/server-action.ts +16 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +10 -1
- package/src/search-params.ts +16 -13
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +148 -16
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +182 -34
- 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 +149 -49
- package/src/types/loader-types.ts +36 -9
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +8 -1
- package/src/types/segments.ts +2 -5
- package/src/urls/path-helper-types.ts +9 -2
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +73 -4
- package/src/vite/discovery/bundle-postprocess.ts +61 -89
- package/src/vite/discovery/discover-routers.ts +23 -5
- package/src/vite/discovery/prerender-collection.ts +48 -15
- 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/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/expose-action-id.ts +1 -3
- 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 +174 -211
- package/src/vite/router-discovery.ts +169 -42
- package/src/vite/utils/banner.ts +3 -3
- package/src/vite/utils/prerender-utils.ts +78 -0
- 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/{CLAUDE.md → AGENTS.md}
RENAMED
|
@@ -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
|
|
|
@@ -248,7 +280,8 @@ All handler typing styles are supported, but they solve different problems:
|
|
|
248
280
|
Example of a scoped local name inside a mounted module:
|
|
249
281
|
|
|
250
282
|
```tsx
|
|
251
|
-
import type { Handler
|
|
283
|
+
import type { Handler } from "@rangojs/router";
|
|
284
|
+
import type { ScopedRouteMap } from "@rangojs/router/__internal";
|
|
252
285
|
|
|
253
286
|
type BlogRoutes = ScopedRouteMap<"blog">;
|
|
254
287
|
|
|
@@ -488,7 +521,7 @@ function Nav() {
|
|
|
488
521
|
return (
|
|
489
522
|
<nav>
|
|
490
523
|
<Link to={href("/")}>Home</Link>
|
|
491
|
-
<Link to={href("/blog")} prefetch="
|
|
524
|
+
<Link to={href("/blog")} prefetch="adaptive">
|
|
492
525
|
Blog
|
|
493
526
|
</Link>
|
|
494
527
|
<Link to={href("/about")}>About</Link>
|
|
@@ -683,10 +716,12 @@ export const BlogPost = Prerender(
|
|
|
683
716
|
|
|
684
717
|
### Passthrough for Unknown Params
|
|
685
718
|
|
|
719
|
+
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.
|
|
720
|
+
|
|
686
721
|
```tsx
|
|
687
|
-
import { Prerender } from "@rangojs/router";
|
|
722
|
+
import { Prerender, Passthrough } from "@rangojs/router";
|
|
688
723
|
|
|
689
|
-
export const
|
|
724
|
+
export const ProductPageDef = Prerender(
|
|
690
725
|
async () => {
|
|
691
726
|
const featured = await db.getFeaturedProducts();
|
|
692
727
|
return featured.map((p) => ({ id: p.id }));
|
|
@@ -695,16 +730,22 @@ export const ProductPage = Prerender(
|
|
|
695
730
|
const product = await db.getProduct(ctx.params.id);
|
|
696
731
|
return <Product data={product} />;
|
|
697
732
|
},
|
|
698
|
-
{ passthrough: true },
|
|
699
733
|
);
|
|
700
|
-
```
|
|
701
734
|
|
|
702
|
-
|
|
735
|
+
// In route definition:
|
|
736
|
+
path(
|
|
737
|
+
"/products/:id",
|
|
738
|
+
Passthrough(ProductPageDef, async (ctx) => {
|
|
739
|
+
const product = await ctx.env.DB.getProduct(ctx.params.id);
|
|
740
|
+
return <Product data={product} />;
|
|
741
|
+
}),
|
|
742
|
+
);
|
|
743
|
+
```
|
|
703
744
|
|
|
704
|
-
|
|
745
|
+
Build handlers can also skip individual param sets with `ctx.passthrough()`, deferring them to the live handler:
|
|
705
746
|
|
|
706
747
|
```tsx
|
|
707
|
-
export const
|
|
748
|
+
export const ProductPageDef = Prerender(
|
|
708
749
|
async () => {
|
|
709
750
|
const all = await db.getAllProducts();
|
|
710
751
|
return all.map((p) => ({ id: p.id }));
|
|
@@ -714,10 +755,55 @@ export const ProductPage = Prerender(
|
|
|
714
755
|
if (!product.published) return ctx.passthrough();
|
|
715
756
|
return <Product data={product} />;
|
|
716
757
|
},
|
|
717
|
-
{ passthrough: true },
|
|
718
758
|
);
|
|
719
759
|
```
|
|
720
760
|
|
|
761
|
+
### Build-Time Environment Bindings
|
|
762
|
+
|
|
763
|
+
Prerender handlers can access platform bindings (KV, D1, R2) at build time when `buildEnv` is configured in the Vite plugin:
|
|
764
|
+
|
|
765
|
+
```ts
|
|
766
|
+
// vite.config.ts
|
|
767
|
+
import { rango } from "@rangojs/router/vite";
|
|
768
|
+
|
|
769
|
+
rango({ preset: "cloudflare", buildEnv: "auto" });
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
With `buildEnv: "auto"`, the plugin calls `wrangler.getPlatformProxy()` to provide local bindings. Handlers then access `ctx.env` during build:
|
|
773
|
+
|
|
774
|
+
```tsx
|
|
775
|
+
export const BlogPosts = Prerender<{ slug: string }>(
|
|
776
|
+
async (ctx) => {
|
|
777
|
+
const rows = await ctx.env.DB.prepare("SELECT slug FROM posts").all();
|
|
778
|
+
return rows.map((r) => ({ slug: r.slug }));
|
|
779
|
+
},
|
|
780
|
+
async (ctx) => {
|
|
781
|
+
const post = await ctx.env.DB.prepare("SELECT * FROM posts WHERE slug = ?")
|
|
782
|
+
.bind(ctx.params.slug)
|
|
783
|
+
.first();
|
|
784
|
+
return <BlogPost post={post} />;
|
|
785
|
+
},
|
|
786
|
+
);
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
`buildEnv` also accepts a factory function or plain object:
|
|
790
|
+
|
|
791
|
+
```ts
|
|
792
|
+
// Custom factory
|
|
793
|
+
rango({
|
|
794
|
+
buildEnv: async (ctx) => {
|
|
795
|
+
const { getPlatformProxy } = await import("wrangler");
|
|
796
|
+
const proxy = await getPlatformProxy();
|
|
797
|
+
return { env: proxy.env, dispose: proxy.dispose };
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// Plain object (Node.js)
|
|
802
|
+
rango({ buildEnv: { DATABASE_URL: process.env.DATABASE_URL } });
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
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.
|
|
806
|
+
|
|
721
807
|
## Theme
|
|
722
808
|
|
|
723
809
|
### Router Configuration
|
|
@@ -842,16 +928,22 @@ module, use `scopedReverse<typeof localPatterns>(ctx.reverse)` or
|
|
|
842
928
|
|
|
843
929
|
## Subpath Exports
|
|
844
930
|
|
|
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/
|
|
931
|
+
| Export | Description |
|
|
932
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------- |
|
|
933
|
+
| `@rangojs/router` | Server/RSC core and shared types: `createRouter`, `urls`, `createLoader`, `Handler`, `Prerender`, `Meta` |
|
|
934
|
+
| `@rangojs/router/client` | Client: `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `MetaTags` |
|
|
935
|
+
| `@rangojs/router/cache` | Cache: `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware` |
|
|
936
|
+
| `@rangojs/router/theme` | Theme: `useTheme`, `ThemeProvider`, `ThemeScript` |
|
|
937
|
+
| `@rangojs/router/host` | Host routing: `createHostRouter`, `defineHosts` |
|
|
938
|
+
| `@rangojs/router/vite` | Vite plugin: `rango()` |
|
|
939
|
+
| `@rangojs/router/rsc` | Advanced server pipeline APIs: `createRSCHandler`, request-context access |
|
|
940
|
+
| `@rangojs/router/ssr` | Advanced SSR bridge APIs: `createSSRHandler` |
|
|
941
|
+
| `@rangojs/router/server` | Internal build/runtime utilities for advanced integrations |
|
|
942
|
+
| `@rangojs/router/build` | Build utilities |
|
|
943
|
+
|
|
944
|
+
The root entrypoint is not a generic client/runtime barrel. If you need hooks
|
|
945
|
+
or components, import from `@rangojs/router/client`; if you need cache or host
|
|
946
|
+
APIs, use their dedicated subpaths.
|
|
855
947
|
|
|
856
948
|
## Examples
|
|
857
949
|
|
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;
|
|
@@ -574,8 +577,20 @@ var init_per_module_writer = __esm({
|
|
|
574
577
|
});
|
|
575
578
|
|
|
576
579
|
// src/build/route-types/router-processing.ts
|
|
577
|
-
import {
|
|
578
|
-
|
|
580
|
+
import {
|
|
581
|
+
readFileSync as readFileSync3,
|
|
582
|
+
writeFileSync as writeFileSync2,
|
|
583
|
+
existsSync as existsSync3,
|
|
584
|
+
unlinkSync,
|
|
585
|
+
readdirSync as readdirSync2
|
|
586
|
+
} from "node:fs";
|
|
587
|
+
import {
|
|
588
|
+
join as join2,
|
|
589
|
+
dirname as dirname2,
|
|
590
|
+
resolve as resolve2,
|
|
591
|
+
sep,
|
|
592
|
+
basename as pathBasename
|
|
593
|
+
} from "node:path";
|
|
579
594
|
import ts5 from "typescript";
|
|
580
595
|
function countPublicRouteEntries(source) {
|
|
581
596
|
const matches = source.matchAll(/^\s+(?:"([^"]+)"|([a-zA-Z_$][^:]*)):\s*["{]/gm) ?? [];
|
|
@@ -588,7 +603,78 @@ function countPublicRouteEntries(source) {
|
|
|
588
603
|
}
|
|
589
604
|
return count;
|
|
590
605
|
}
|
|
591
|
-
function
|
|
606
|
+
function isRoutableSourceFile(name) {
|
|
607
|
+
return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !name.includes(".gen.") && !name.includes(".test.") && !name.includes(".spec.");
|
|
608
|
+
}
|
|
609
|
+
function findRouterFilesRecursive(dir, filter, results) {
|
|
610
|
+
let entries;
|
|
611
|
+
try {
|
|
612
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
613
|
+
} catch (err) {
|
|
614
|
+
console.warn(
|
|
615
|
+
`[rsc-router] Failed to scan directory ${dir}: ${err.message}`
|
|
616
|
+
);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const childDirs = [];
|
|
620
|
+
const routerFilesInDir = [];
|
|
621
|
+
for (const entry of entries) {
|
|
622
|
+
const fullPath = join2(dir, entry.name);
|
|
623
|
+
if (entry.isDirectory()) {
|
|
624
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage" || entry.name === "__tests__" || entry.name === "__mocks__" || entry.name.startsWith("."))
|
|
625
|
+
continue;
|
|
626
|
+
childDirs.push(fullPath);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (!isRoutableSourceFile(entry.name)) continue;
|
|
630
|
+
if (filter && !filter(fullPath)) continue;
|
|
631
|
+
try {
|
|
632
|
+
const source = readFileSync3(fullPath, "utf-8");
|
|
633
|
+
if (ROUTER_CALL_PATTERN.test(source)) {
|
|
634
|
+
routerFilesInDir.push(fullPath);
|
|
635
|
+
}
|
|
636
|
+
} catch {
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (routerFilesInDir.length > 0) {
|
|
641
|
+
results.push(...routerFilesInDir);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
for (const childDir of childDirs) {
|
|
645
|
+
findRouterFilesRecursive(childDir, filter, results);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function findNestedRouterConflict(routerFiles) {
|
|
649
|
+
const routerDirs = [
|
|
650
|
+
...new Set(routerFiles.map((filePath) => dirname2(resolve2(filePath))))
|
|
651
|
+
].sort((a, b) => a.length - b.length);
|
|
652
|
+
for (let i = 0; i < routerDirs.length; i++) {
|
|
653
|
+
const ancestorDir = routerDirs[i];
|
|
654
|
+
const prefix = ancestorDir.endsWith(sep) ? ancestorDir : `${ancestorDir}${sep}`;
|
|
655
|
+
for (let j = i + 1; j < routerDirs.length; j++) {
|
|
656
|
+
const nestedDir = routerDirs[j];
|
|
657
|
+
if (!nestedDir.startsWith(prefix)) continue;
|
|
658
|
+
const ancestorFile = routerFiles.find(
|
|
659
|
+
(filePath) => dirname2(resolve2(filePath)) === ancestorDir
|
|
660
|
+
);
|
|
661
|
+
const nestedFile = routerFiles.find(
|
|
662
|
+
(filePath) => dirname2(resolve2(filePath)) === nestedDir
|
|
663
|
+
);
|
|
664
|
+
if (ancestorFile && nestedFile) {
|
|
665
|
+
return { ancestor: ancestorFile, nested: nestedFile };
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
function formatNestedRouterConflictError(conflict, prefix = "[rsc-router]") {
|
|
672
|
+
return `${prefix} Nested router roots are not supported.
|
|
673
|
+
Router root: ${conflict.ancestor}
|
|
674
|
+
Nested router: ${conflict.nested}
|
|
675
|
+
Move the nested router into a sibling directory or configure it as a separate app root.`;
|
|
676
|
+
}
|
|
677
|
+
function extractUrlsFromRouter(code) {
|
|
592
678
|
const sourceFile = ts5.createSourceFile(
|
|
593
679
|
"router.tsx",
|
|
594
680
|
code,
|
|
@@ -602,24 +688,70 @@ function extractUrlsVariableFromRouter(code) {
|
|
|
602
688
|
const callee = node.expression;
|
|
603
689
|
return ts5.isIdentifier(callee) && callee.text === "createRouter";
|
|
604
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
|
+
}
|
|
605
703
|
function visit(node) {
|
|
606
704
|
if (result) return;
|
|
607
|
-
if (ts5.isCallExpression(node) &&
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
result = node.arguments[0].text;
|
|
614
|
-
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) };
|
|
615
711
|
}
|
|
712
|
+
return;
|
|
616
713
|
}
|
|
617
714
|
if (isCreateRouterCall(node)) {
|
|
618
715
|
const callExpr = node;
|
|
619
|
-
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) {
|
|
620
752
|
if (ts5.isObjectLiteralExpression(arg)) {
|
|
621
753
|
for (const prop of arg.properties) {
|
|
622
|
-
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)) {
|
|
623
755
|
result = prop.initializer.text;
|
|
624
756
|
return;
|
|
625
757
|
}
|
|
@@ -632,6 +764,19 @@ function extractUrlsVariableFromRouter(code) {
|
|
|
632
764
|
visit(sourceFile);
|
|
633
765
|
return result;
|
|
634
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
|
+
}
|
|
635
780
|
function buildCombinedRouteMapForRouterFile(routerFilePath) {
|
|
636
781
|
let routerSource;
|
|
637
782
|
try {
|
|
@@ -639,19 +784,40 @@ function buildCombinedRouteMapForRouterFile(routerFilePath) {
|
|
|
639
784
|
} catch {
|
|
640
785
|
return { routes: {}, searchSchemas: {} };
|
|
641
786
|
}
|
|
642
|
-
const
|
|
643
|
-
if (!
|
|
787
|
+
const extraction = extractUrlsFromRouter(routerSource);
|
|
788
|
+
if (!extraction) {
|
|
644
789
|
return { routes: {}, searchSchemas: {} };
|
|
645
790
|
}
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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);
|
|
651
815
|
}
|
|
652
|
-
return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
|
|
653
816
|
}
|
|
654
|
-
|
|
817
|
+
if (basename2) {
|
|
818
|
+
result = applyBasenameToRoutes(result, basename2);
|
|
819
|
+
}
|
|
820
|
+
return result;
|
|
655
821
|
}
|
|
656
822
|
function detectUnresolvableIncludes(routerFilePath) {
|
|
657
823
|
const realPath = resolve2(routerFilePath);
|
|
@@ -661,9 +827,20 @@ function detectUnresolvableIncludes(routerFilePath) {
|
|
|
661
827
|
} catch {
|
|
662
828
|
return [];
|
|
663
829
|
}
|
|
664
|
-
const
|
|
665
|
-
if (!
|
|
666
|
-
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);
|
|
667
844
|
let targetFile;
|
|
668
845
|
let exportedName;
|
|
669
846
|
if (imported) {
|
|
@@ -683,9 +860,8 @@ function detectUnresolvableIncludes(routerFilePath) {
|
|
|
683
860
|
exportedName = imported.exportedName;
|
|
684
861
|
} else {
|
|
685
862
|
targetFile = realPath;
|
|
686
|
-
exportedName =
|
|
863
|
+
exportedName = extraction.name;
|
|
687
864
|
}
|
|
688
|
-
const diagnostics = [];
|
|
689
865
|
buildCombinedRouteMapWithSearch(
|
|
690
866
|
targetFile,
|
|
691
867
|
exportedName,
|
|
@@ -711,19 +887,8 @@ function detectUnresolvableIncludesForUrlsFile(filePath) {
|
|
|
711
887
|
return diagnostics;
|
|
712
888
|
}
|
|
713
889
|
function findRouterFiles(root, filter) {
|
|
714
|
-
const files = findTsFiles(root, filter);
|
|
715
890
|
const result = [];
|
|
716
|
-
|
|
717
|
-
if (filePath.includes(".gen.")) continue;
|
|
718
|
-
try {
|
|
719
|
-
const source = readFileSync3(filePath, "utf-8");
|
|
720
|
-
if (/\bcreateRouter\s*[<(]/.test(source)) {
|
|
721
|
-
result.push(filePath);
|
|
722
|
-
}
|
|
723
|
-
} catch {
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
891
|
+
findRouterFilesRecursive(root, filter, result);
|
|
727
892
|
return result;
|
|
728
893
|
}
|
|
729
894
|
function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
|
|
@@ -739,26 +904,20 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
|
|
|
739
904
|
}
|
|
740
905
|
const routerFilePaths = knownRouterFiles ?? findRouterFiles(root);
|
|
741
906
|
if (routerFilePaths.length === 0) return;
|
|
907
|
+
const nestedRouterConflict = findNestedRouterConflict(routerFilePaths);
|
|
908
|
+
if (nestedRouterConflict) {
|
|
909
|
+
throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
|
|
910
|
+
}
|
|
742
911
|
for (const routerFilePath of routerFilePaths) {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
routerSource
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
const imported = resolveImportedVariable(routerSource, urlsVarName);
|
|
753
|
-
if (imported) {
|
|
754
|
-
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
755
|
-
if (!targetFile) continue;
|
|
756
|
-
result = buildCombinedRouteMapWithSearch(
|
|
757
|
-
targetFile,
|
|
758
|
-
imported.exportedName
|
|
759
|
-
);
|
|
760
|
-
} else {
|
|
761
|
-
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;
|
|
762
921
|
}
|
|
763
922
|
const routerBasename = pathBasename(routerFilePath).replace(
|
|
764
923
|
/\.(tsx?|jsx?)$/,
|
|
@@ -798,14 +957,15 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
|
|
|
798
957
|
}
|
|
799
958
|
}
|
|
800
959
|
}
|
|
960
|
+
var ROUTER_CALL_PATTERN;
|
|
801
961
|
var init_router_processing = __esm({
|
|
802
962
|
"src/build/route-types/router-processing.ts"() {
|
|
803
963
|
"use strict";
|
|
804
964
|
init_codegen();
|
|
805
|
-
init_scan_filter();
|
|
806
965
|
init_include_resolution();
|
|
807
966
|
init_per_module_writer();
|
|
808
967
|
init_route_name();
|
|
968
|
+
ROUTER_CALL_PATTERN = /\bcreateRouter\s*[<(]/;
|
|
809
969
|
}
|
|
810
970
|
});
|
|
811
971
|
|
|
@@ -986,8 +1146,9 @@ function createVersionPlugin() {
|
|
|
986
1146
|
let isDev = false;
|
|
987
1147
|
let server = null;
|
|
988
1148
|
const clientModuleSignatures = /* @__PURE__ */ new Map();
|
|
1149
|
+
let versionCounter = 0;
|
|
989
1150
|
const bumpVersion = (reason) => {
|
|
990
|
-
currentVersion = Date.now().toString(16);
|
|
1151
|
+
currentVersion = Date.now().toString(16) + String(++versionCounter);
|
|
991
1152
|
console.log(`[rsc-router] ${reason}, version updated: ${currentVersion}`);
|
|
992
1153
|
const rscEnv = server?.environments?.rsc;
|
|
993
1154
|
const versionMod = rscEnv?.moduleGraph?.getModuleById(
|
|
@@ -1043,6 +1204,9 @@ function createVersionPlugin() {
|
|
|
1043
1204
|
if (!isDev) return;
|
|
1044
1205
|
const isRscModule = this.environment?.name === "rsc";
|
|
1045
1206
|
if (!isRscModule) return;
|
|
1207
|
+
if (ctx.modules.length === 1 && ctx.modules[0].id === "\0" + VIRTUAL_IDS.version) {
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1046
1210
|
if (isCodeModule(ctx.file)) {
|
|
1047
1211
|
const filePath = normalizeModuleId(ctx.file);
|
|
1048
1212
|
const previousSignature = clientModuleSignatures.get(filePath);
|
|
@@ -1428,6 +1592,15 @@ function runStaticGeneration(args, mode) {
|
|
|
1428
1592
|
formatDiagnostics(uniqueDiagnostics);
|
|
1429
1593
|
console.warn("");
|
|
1430
1594
|
}
|
|
1595
|
+
const nestedRouterConflict = findNestedRouterConflict(routerFiles);
|
|
1596
|
+
if (nestedRouterConflict) {
|
|
1597
|
+
console.error(
|
|
1598
|
+
`
|
|
1599
|
+
${formatNestedRouterConflictError(nestedRouterConflict, "[rango]")}
|
|
1600
|
+
`
|
|
1601
|
+
);
|
|
1602
|
+
process.exit(1);
|
|
1603
|
+
}
|
|
1431
1604
|
for (const urlsFile of urlsFiles) {
|
|
1432
1605
|
writePerModuleRouteTypesForFile(urlsFile);
|
|
1433
1606
|
}
|
|
@@ -1471,6 +1644,15 @@ async function runRuntimeDiscovery(args, configFile) {
|
|
|
1471
1644
|
console.error("[rango] No router files found in the provided paths");
|
|
1472
1645
|
process.exit(1);
|
|
1473
1646
|
}
|
|
1647
|
+
const nestedRouterConflict = findNestedRouterConflict(routerEntries);
|
|
1648
|
+
if (nestedRouterConflict) {
|
|
1649
|
+
console.error(
|
|
1650
|
+
`
|
|
1651
|
+
${formatNestedRouterConflictError(nestedRouterConflict, "[rango]")}
|
|
1652
|
+
`
|
|
1653
|
+
);
|
|
1654
|
+
process.exit(1);
|
|
1655
|
+
}
|
|
1474
1656
|
let discoverAndWriteRouteTypes2;
|
|
1475
1657
|
try {
|
|
1476
1658
|
const mod = await Promise.resolve().then(() => (init_runtime_discovery(), runtime_discovery_exports));
|