@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1b930379

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.
Files changed (84) hide show
  1. package/README.md +46 -12
  2. package/dist/bin/rango.js +109 -15
  3. package/dist/vite/index.js +323 -121
  4. package/package.json +15 -16
  5. package/skills/breadcrumbs/SKILL.md +250 -0
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +33 -31
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/loader/SKILL.md +55 -15
  11. package/skills/prerender/SKILL.md +2 -2
  12. package/skills/rango/SKILL.md +0 -1
  13. package/skills/route/SKILL.md +3 -4
  14. package/skills/router-setup/SKILL.md +8 -3
  15. package/skills/typesafety/SKILL.md +25 -23
  16. package/src/__internal.ts +92 -0
  17. package/src/bin/rango.ts +18 -0
  18. package/src/browser/link-interceptor.ts +4 -0
  19. package/src/browser/navigation-bridge.ts +95 -5
  20. package/src/browser/navigation-client.ts +97 -72
  21. package/src/browser/prefetch/cache.ts +112 -25
  22. package/src/browser/prefetch/fetch.ts +28 -30
  23. package/src/browser/prefetch/policy.ts +6 -0
  24. package/src/browser/react/Link.tsx +19 -7
  25. package/src/browser/rsc-router.tsx +11 -2
  26. package/src/browser/server-action-bridge.ts +448 -432
  27. package/src/browser/types.ts +24 -0
  28. package/src/build/generate-route-types.ts +2 -0
  29. package/src/build/route-trie.ts +19 -3
  30. package/src/build/route-types/router-processing.ts +125 -15
  31. package/src/client.rsc.tsx +2 -1
  32. package/src/client.tsx +1 -46
  33. package/src/handles/breadcrumbs.ts +66 -0
  34. package/src/handles/index.ts +1 -0
  35. package/src/host/index.ts +0 -3
  36. package/src/index.rsc.ts +5 -36
  37. package/src/index.ts +32 -66
  38. package/src/prerender/store.ts +56 -15
  39. package/src/route-definition/index.ts +0 -3
  40. package/src/router/handler-context.ts +30 -3
  41. package/src/router/loader-resolution.ts +1 -1
  42. package/src/router/match-api.ts +1 -1
  43. package/src/router/match-result.ts +0 -9
  44. package/src/router/metrics.ts +233 -13
  45. package/src/router/middleware-types.ts +53 -10
  46. package/src/router/middleware.ts +170 -81
  47. package/src/router/pattern-matching.ts +20 -5
  48. package/src/router/prerender-match.ts +4 -0
  49. package/src/router/revalidation.ts +27 -7
  50. package/src/router/router-interfaces.ts +14 -1
  51. package/src/router/router-options.ts +13 -8
  52. package/src/router/segment-resolution/fresh.ts +18 -0
  53. package/src/router/segment-resolution/helpers.ts +1 -1
  54. package/src/router/segment-resolution/revalidation.ts +22 -9
  55. package/src/router/trie-matching.ts +20 -2
  56. package/src/router.ts +29 -9
  57. package/src/rsc/handler.ts +106 -11
  58. package/src/rsc/index.ts +0 -20
  59. package/src/rsc/progressive-enhancement.ts +21 -8
  60. package/src/rsc/rsc-rendering.ts +30 -43
  61. package/src/rsc/server-action.ts +14 -10
  62. package/src/rsc/ssr-setup.ts +128 -0
  63. package/src/rsc/types.ts +2 -0
  64. package/src/search-params.ts +16 -13
  65. package/src/server/context.ts +8 -2
  66. package/src/server/request-context.ts +38 -16
  67. package/src/server.ts +6 -0
  68. package/src/theme/index.ts +4 -13
  69. package/src/types/handler-context.ts +12 -16
  70. package/src/types/route-config.ts +17 -8
  71. package/src/types/segments.ts +0 -5
  72. package/src/vite/discovery/bundle-postprocess.ts +31 -56
  73. package/src/vite/discovery/discover-routers.ts +18 -4
  74. package/src/vite/discovery/prerender-collection.ts +34 -14
  75. package/src/vite/discovery/state.ts +4 -7
  76. package/src/vite/index.ts +4 -3
  77. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  78. package/src/vite/plugins/refresh-cmd.ts +65 -0
  79. package/src/vite/rango.ts +11 -0
  80. package/src/vite/router-discovery.ts +16 -0
  81. package/src/vite/utils/prerender-utils.ts +60 -0
  82. package/skills/testing/SKILL.md +0 -226
  83. package/src/route-definition/route-function.ts +0 -119
  84. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -11,6 +11,8 @@ import { createServer as createViteServer } from "vite";
11
11
  import { resolve } from "node:path";
12
12
  import { readFileSync } from "node:fs";
13
13
  import {
14
+ formatNestedRouterConflictError,
15
+ findNestedRouterConflict,
14
16
  findRouterFiles,
15
17
  createScanFilter,
16
18
  } from "../build/generate-route-types.js";
@@ -40,6 +42,7 @@ import {
40
42
  generatePerRouterModule,
41
43
  } from "./discovery/virtual-module-codegen.js";
42
44
  import { postprocessBundle } from "./discovery/bundle-postprocess.js";
45
+ import { resetStagedBuildAssets } from "./utils/prerender-utils.js";
43
46
 
44
47
  export { VIRTUAL_ROUTES_MANIFEST_ID };
45
48
 
@@ -559,6 +562,16 @@ export function createRouterDiscoveryPlugin(
559
562
  if (!hasUrls && !hasCreateRouter) return;
560
563
  // Invalidate cache when a router file changes (new router added/removed)
561
564
  if (hasCreateRouter) {
565
+ const nestedRouterConflict = findNestedRouterConflict([
566
+ ...(s.cachedRouterFiles ?? []),
567
+ resolve(filePath),
568
+ ]);
569
+ if (nestedRouterConflict) {
570
+ server.config.logger.error(
571
+ formatNestedRouterConflictError(nestedRouterConflict),
572
+ );
573
+ return;
574
+ }
562
575
  s.cachedRouterFiles = undefined;
563
576
  }
564
577
  scheduleRouteRegeneration();
@@ -592,6 +605,9 @@ export function createRouterDiscoveryPlugin(
592
605
  if (!s.isBuildMode) return;
593
606
  // Only run once across environment builds
594
607
  if (s.mergedRouteManifest !== null) return;
608
+ resetStagedBuildAssets(s.projectRoot);
609
+ s.prerenderManifestEntries = null;
610
+ s.staticManifestEntries = null;
595
611
 
596
612
  let tempServer: any = null;
597
613
  // Signal to user-space code (e.g. reverse.ts) that build-time discovery
@@ -1,3 +1,14 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ copyFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ rmSync,
7
+ statSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { resolve } from "node:path";
11
+
1
12
  /**
2
13
  * Escape special RegExp characters in a string for safe interpolation
3
14
  * into new RegExp() patterns.
@@ -127,3 +138,52 @@ export function notifyOnError(
127
138
  break; // Only notify the first router with onError
128
139
  }
129
140
  }
141
+
142
+ function getStagedAssetDir(projectRoot: string): string {
143
+ return resolve(projectRoot, "node_modules/.rangojs-router-build/rsc-assets");
144
+ }
145
+
146
+ export function resetStagedBuildAssets(projectRoot: string): void {
147
+ rmSync(getStagedAssetDir(projectRoot), { recursive: true, force: true });
148
+ }
149
+
150
+ export function stageBuildAssetModule(
151
+ projectRoot: string,
152
+ prefix: "__pr" | "__st",
153
+ exportValue: string,
154
+ ): string {
155
+ const stagedDir = getStagedAssetDir(projectRoot);
156
+ mkdirSync(stagedDir, { recursive: true });
157
+
158
+ const contentHash = createHash("sha256")
159
+ .update(exportValue)
160
+ .digest("hex")
161
+ .slice(0, 8);
162
+ const fileName = `${prefix}-${contentHash}.js`;
163
+ const filePath = resolve(stagedDir, fileName);
164
+
165
+ if (!existsSync(filePath)) {
166
+ writeFileSync(filePath, `export default ${exportValue};\n`);
167
+ }
168
+
169
+ return fileName;
170
+ }
171
+
172
+ export function copyStagedBuildAssets(
173
+ projectRoot: string,
174
+ fileNames: Iterable<string>,
175
+ ): number {
176
+ const stagedDir = getStagedAssetDir(projectRoot);
177
+ const distAssetsDir = resolve(projectRoot, "dist/rsc/assets");
178
+ mkdirSync(distAssetsDir, { recursive: true });
179
+
180
+ let totalBytes = 0;
181
+ for (const fileName of new Set(fileNames)) {
182
+ const stagedPath = resolve(stagedDir, fileName);
183
+ const distPath = resolve(distAssetsDir, fileName);
184
+ copyFileSync(stagedPath, distPath);
185
+ totalBytes += statSync(stagedPath).size;
186
+ }
187
+
188
+ return totalBytes;
189
+ }
@@ -1,226 +0,0 @@
1
- ---
2
- name: testing
3
- description: Unit test route trees with buildRouteTree()
4
- argument-hint:
5
- ---
6
-
7
- # Route Tree Unit Testing
8
-
9
- Unit test route definitions by inspecting the route tree, segment IDs, middleware, intercepts, loaders, and pattern matching without running a dev server.
10
-
11
- ## Setup
12
-
13
- The `buildRouteTree` helper lives in `src/__tests__/helpers/route-tree.ts` (not shipped with npm). Import it in your test files:
14
-
15
- ```typescript
16
- import { buildRouteTree } from "./helpers/route-tree.js";
17
- ```
18
-
19
- ## buildRouteTree(urlPatterns)
20
-
21
- Takes a `urls()` result and returns a `RouteTree` with inspection methods:
22
-
23
- ```typescript
24
- import { urls } from "@rangojs/router";
25
- import { buildRouteTree } from "./helpers/route-tree.js";
26
-
27
- const tree = buildRouteTree(
28
- urls(({ path, layout, middleware, loader, intercept, when }) => [
29
- layout(RootLayout, () => [
30
- middleware(authMiddleware),
31
- path("/", HomePage, { name: "home" }),
32
- path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
33
- loader(PostLoader),
34
- ]),
35
- ]),
36
- ]),
37
- );
38
- ```
39
-
40
- ## RouteTree API
41
-
42
- ### Route Patterns
43
-
44
- ```typescript
45
- tree.routes(); // { home: "/", "blog.post": "/blog/:slug" }
46
- tree.routeNames(); // ["home", "blog.post"]
47
- ```
48
-
49
- ### URL Matching
50
-
51
- ```typescript
52
- const m = tree.match("/blog/hello");
53
- m.routeKey; // "blog.post"
54
- m.params; // { slug: "hello" }
55
-
56
- tree.match("/nonexistent"); // null
57
- ```
58
-
59
- ### Segment IDs
60
-
61
- ```typescript
62
- tree.segmentId("home"); // "M0L0L0R0"
63
- tree.segmentIds(); // { home: "M0L0L0R0", "blog.post": "M0L0L0R1" }
64
- tree.segmentPath("blog.post");
65
- // [
66
- // { id: "M0L0", type: "layout" }, // synthetic root
67
- // { id: "M0L0L0", type: "layout" }, // RootLayout
68
- // { id: "M0L0L0R1", type: "route", pattern: "/blog/:slug" },
69
- // ]
70
- ```
71
-
72
- ### Entry Access
73
-
74
- ```typescript
75
- tree.entry("blog.post"); // EntryData
76
- tree.entry("blog.post")!.parent!.type; // "layout"
77
- tree.entryByPattern("/blog/:slug"); // EntryData (lookup by URL pattern)
78
- ```
79
-
80
- ### Middleware
81
-
82
- ```typescript
83
- tree.hasMiddleware("home"); // true (inherited from layout)
84
- tree.middleware("home"); // [authMiddleware] (direct only)
85
- tree.middlewareChain("home");
86
- // [{ segmentId: "M0L0L0", count: 1 }] // all middleware root-to-route
87
- ```
88
-
89
- ### Loaders
90
-
91
- ```typescript
92
- tree.hasLoaders("blog.post"); // true
93
- tree.loaders("blog.post"); // [LoaderEntry { loader, revalidate, cache? }]
94
- ```
95
-
96
- ### Intercepts
97
-
98
- ```typescript
99
- tree.intercepts("home");
100
- // [{ slotName: "@modal", routeName: "card", hasWhen: true, whenCount: 1, hasLoader: false, hasMiddleware: false }]
101
- tree.interceptEntries("home"); // raw InterceptEntry[]
102
- ```
103
-
104
- ### Parallel Slots
105
-
106
- ```typescript
107
- tree.parallelSlots("home"); // EntryData[] of type="parallel"
108
- tree.parallelSlotNames("home"); // ["@sidebar", "@main"]
109
- ```
110
-
111
- ### Boundaries
112
-
113
- ```typescript
114
- tree.hasErrorBoundary("home"); // boolean
115
- tree.hasNotFoundBoundary("home"); // boolean
116
- ```
117
-
118
- ### Cache & Loading
119
-
120
- ```typescript
121
- tree.hasCache("home"); // boolean
122
- tree.hasLoading("home"); // boolean
123
- ```
124
-
125
- ### Debug
126
-
127
- ```typescript
128
- console.log(tree.debug());
129
- // Route Tree:
130
- // home: / [M0L0L0R0] (M0L0 > M0L0L0 > M0L0L0R0) {mw:1}
131
- // blog.post: /blog/:slug [M0L0L0R1] (M0L0 > M0L0L0 > M0L0L0R1) {mw:1, ld:1}
132
- ```
133
-
134
- ## Segment ID Format
135
-
136
- | Prefix | Meaning |
137
- | ------ | ----------------------------- |
138
- | `M0` | Mount index (router instance) |
139
- | `L` | Layout |
140
- | `R` | Route |
141
- | `P` | Parallel slot |
142
- | `D` | Loader (data) |
143
- | `C` | Cache boundary |
144
-
145
- Example: `M0L0L0R1` = mount 0, synthetic root layout, user layout, second route.
146
-
147
- ## Examples
148
-
149
- ### include() with prefix
150
-
151
- ```typescript
152
- const blogPatterns = urls(({ path }) => [
153
- path("/", BlogIndex, { name: "index" }),
154
- path("/:slug", BlogPost, { name: "post" }),
155
- ]);
156
-
157
- const tree = buildRouteTree(
158
- urls(({ path, include }) => [
159
- path("/", HomePage, { name: "home" }),
160
- include("/blog", blogPatterns, { name: "blog" }),
161
- ]),
162
- );
163
-
164
- expect(tree.routes()).toEqual({
165
- home: "/",
166
- "blog.index": "/blog",
167
- "blog.post": "/blog/:slug",
168
- });
169
- ```
170
-
171
- ### Middleware chain
172
-
173
- ```typescript
174
- const authMw = async (ctx, next) => next();
175
- const logMw = async (ctx, next) => next();
176
-
177
- const tree = buildRouteTree(
178
- urls(({ path, layout, middleware }) => [
179
- layout(RootLayout, () => [
180
- middleware(logMw),
181
- layout(AuthLayout, () => [
182
- middleware(authMw),
183
- path("/dashboard", Dashboard, { name: "dashboard" }),
184
- ]),
185
- ]),
186
- ]),
187
- );
188
-
189
- expect(tree.middlewareChain("dashboard")).toEqual([
190
- { segmentId: "M0L0L0", count: 1 }, // logMw on RootLayout
191
- { segmentId: "M0L0L0L0", count: 1 }, // authMw on AuthLayout
192
- ]);
193
- ```
194
-
195
- ### Intercepts with when()
196
-
197
- ```typescript
198
- const tree = buildRouteTree(
199
- urls(({ path, layout, intercept, when }) => [
200
- layout(ShopLayout, () => [
201
- path("/products", ProductList, { name: "products" }),
202
- path("/products/:id", ProductDetail, { name: "product.detail" }),
203
- intercept("@modal", "product.detail", ProductModal, () => [
204
- when((ctx) => ctx.from.pathname.startsWith("/products")),
205
- ]),
206
- ]),
207
- ]),
208
- );
209
-
210
- const intercepts = tree.intercepts("products");
211
- // Note: intercepts are on the parent where intercept() is called
212
- ```
213
-
214
- ### Constrained parameters
215
-
216
- ```typescript
217
- const tree = buildRouteTree(
218
- urls(({ path }) => [
219
- path("/:locale(en|fr)?/about", AboutPage, { name: "about" }),
220
- ]),
221
- );
222
-
223
- expect(tree.match("/about")).not.toBeNull();
224
- expect(tree.match("/fr/about")!.params).toEqual({ locale: "fr" });
225
- expect(tree.match("/de/about")).toBeNull();
226
- ```
@@ -1,119 +0,0 @@
1
- import type {
2
- ResolvedRouteMap,
3
- RouteConfig,
4
- RouteDefinition,
5
- RouteDefinitionOptions,
6
- TrailingSlashMode,
7
- } from "../types.js";
8
-
9
- /**
10
- * Result of route() function with paths and trailing slash config
11
- */
12
- export interface RouteDefinitionResult<T extends RouteDefinition> {
13
- routes: ResolvedRouteMap<T>;
14
- trailingSlash: Record<string, TrailingSlashMode>;
15
- }
16
-
17
- /**
18
- * Check if a value is a RouteConfig object
19
- */
20
- function isRouteConfig(value: unknown): value is RouteConfig {
21
- return (
22
- typeof value === "object" &&
23
- value !== null &&
24
- "path" in value &&
25
- typeof (value as RouteConfig).path === "string"
26
- );
27
- }
28
-
29
- /**
30
- * Define routes with optional trailing slash configuration
31
- *
32
- * @example
33
- * ```typescript
34
- * // Simple string paths
35
- * const routes = route({
36
- * blog: "/blog",
37
- * post: "/blog/:id",
38
- * });
39
- *
40
- * // With trailing slash config
41
- * const routes = route({
42
- * blog: "/blog",
43
- * api: { path: "/api", trailingSlash: "ignore" },
44
- * }, { trailingSlash: "never" }); // global default
45
- * ```
46
- */
47
- export function route<const T extends RouteDefinition>(
48
- input: T,
49
- options?: RouteDefinitionOptions,
50
- ): ResolvedRouteMap<T> & {
51
- __trailingSlash?: Record<string, TrailingSlashMode>;
52
- } {
53
- const trailingSlash: Record<string, TrailingSlashMode> = {};
54
- const routes = flattenRoutes(
55
- input as RouteDefinition,
56
- "",
57
- trailingSlash,
58
- options?.trailingSlash,
59
- );
60
-
61
- // Attach trailing slash config as a non-enumerable property
62
- // This keeps backwards compatibility while passing the config through
63
- const result = routes as ResolvedRouteMap<T> & {
64
- __trailingSlash?: Record<string, TrailingSlashMode>;
65
- };
66
- if (Object.keys(trailingSlash).length > 0) {
67
- Object.defineProperty(result, "__trailingSlash", {
68
- value: trailingSlash,
69
- enumerable: false,
70
- writable: false,
71
- });
72
- }
73
-
74
- return result;
75
- }
76
-
77
- /**
78
- * Flatten nested route definitions
79
- */
80
- function flattenRoutes(
81
- routes: RouteDefinition,
82
- prefix: string,
83
- trailingSlashConfig: Record<string, TrailingSlashMode>,
84
- defaultTrailingSlash?: TrailingSlashMode,
85
- ): Record<string, string> {
86
- const flattened: Record<string, string> = {};
87
-
88
- for (const [key, value] of Object.entries(routes)) {
89
- const fullKey = prefix + key;
90
-
91
- if (typeof value === "string") {
92
- // Direct route pattern - include prefix
93
- flattened[fullKey] = value;
94
- // Apply default trailing slash if set
95
- if (defaultTrailingSlash) {
96
- trailingSlashConfig[fullKey] = defaultTrailingSlash;
97
- }
98
- } else if (isRouteConfig(value)) {
99
- // Route config object with path and optional trailingSlash
100
- flattened[fullKey] = value.path;
101
- // Use route-specific config or fall back to default
102
- const mode = value.trailingSlash ?? defaultTrailingSlash;
103
- if (mode) {
104
- trailingSlashConfig[fullKey] = mode;
105
- }
106
- } else {
107
- // Nested routes - flatten recursively
108
- const nested = flattenRoutes(
109
- value,
110
- `${fullKey}.`,
111
- trailingSlashConfig,
112
- defaultTrailingSlash,
113
- );
114
- Object.assign(flattened, nested);
115
- }
116
- }
117
-
118
- return flattened;
119
- }
File without changes