@rangojs/router 0.0.0-experimental.10

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 (172) hide show
  1. package/CLAUDE.md +43 -0
  2. package/README.md +19 -0
  3. package/dist/bin/rango.js +227 -0
  4. package/dist/vite/index.js +3039 -0
  5. package/package.json +171 -0
  6. package/skills/caching/SKILL.md +191 -0
  7. package/skills/debug-manifest/SKILL.md +108 -0
  8. package/skills/document-cache/SKILL.md +180 -0
  9. package/skills/fonts/SKILL.md +165 -0
  10. package/skills/hooks/SKILL.md +442 -0
  11. package/skills/intercept/SKILL.md +190 -0
  12. package/skills/layout/SKILL.md +213 -0
  13. package/skills/links/SKILL.md +180 -0
  14. package/skills/loader/SKILL.md +246 -0
  15. package/skills/middleware/SKILL.md +202 -0
  16. package/skills/mime-routes/SKILL.md +124 -0
  17. package/skills/parallel/SKILL.md +228 -0
  18. package/skills/prerender/SKILL.md +283 -0
  19. package/skills/rango/SKILL.md +54 -0
  20. package/skills/response-routes/SKILL.md +358 -0
  21. package/skills/route/SKILL.md +173 -0
  22. package/skills/router-setup/SKILL.md +346 -0
  23. package/skills/tailwind/SKILL.md +129 -0
  24. package/skills/theme/SKILL.md +78 -0
  25. package/skills/typesafety/SKILL.md +394 -0
  26. package/src/__internal.ts +175 -0
  27. package/src/bin/rango.ts +24 -0
  28. package/src/browser/event-controller.ts +876 -0
  29. package/src/browser/index.ts +18 -0
  30. package/src/browser/link-interceptor.ts +121 -0
  31. package/src/browser/lru-cache.ts +69 -0
  32. package/src/browser/merge-segment-loaders.ts +126 -0
  33. package/src/browser/navigation-bridge.ts +913 -0
  34. package/src/browser/navigation-client.ts +165 -0
  35. package/src/browser/navigation-store.ts +823 -0
  36. package/src/browser/partial-update.ts +600 -0
  37. package/src/browser/react/Link.tsx +248 -0
  38. package/src/browser/react/NavigationProvider.tsx +346 -0
  39. package/src/browser/react/ScrollRestoration.tsx +94 -0
  40. package/src/browser/react/context.ts +53 -0
  41. package/src/browser/react/index.ts +52 -0
  42. package/src/browser/react/location-state-shared.ts +120 -0
  43. package/src/browser/react/location-state.ts +62 -0
  44. package/src/browser/react/mount-context.ts +32 -0
  45. package/src/browser/react/use-action.ts +240 -0
  46. package/src/browser/react/use-client-cache.ts +56 -0
  47. package/src/browser/react/use-handle.ts +203 -0
  48. package/src/browser/react/use-href.tsx +40 -0
  49. package/src/browser/react/use-link-status.ts +134 -0
  50. package/src/browser/react/use-mount.ts +31 -0
  51. package/src/browser/react/use-navigation.ts +140 -0
  52. package/src/browser/react/use-segments.ts +188 -0
  53. package/src/browser/request-controller.ts +164 -0
  54. package/src/browser/rsc-router.tsx +352 -0
  55. package/src/browser/scroll-restoration.ts +324 -0
  56. package/src/browser/segment-structure-assert.ts +67 -0
  57. package/src/browser/server-action-bridge.ts +762 -0
  58. package/src/browser/shallow.ts +35 -0
  59. package/src/browser/types.ts +478 -0
  60. package/src/build/generate-manifest.ts +377 -0
  61. package/src/build/generate-route-types.ts +828 -0
  62. package/src/build/index.ts +36 -0
  63. package/src/build/route-trie.ts +239 -0
  64. package/src/cache/cache-scope.ts +563 -0
  65. package/src/cache/cf/cf-cache-store.ts +428 -0
  66. package/src/cache/cf/index.ts +19 -0
  67. package/src/cache/document-cache.ts +340 -0
  68. package/src/cache/index.ts +58 -0
  69. package/src/cache/memory-segment-store.ts +150 -0
  70. package/src/cache/memory-store.ts +253 -0
  71. package/src/cache/types.ts +392 -0
  72. package/src/client.rsc.tsx +83 -0
  73. package/src/client.tsx +643 -0
  74. package/src/component-utils.ts +76 -0
  75. package/src/components/DefaultDocument.tsx +23 -0
  76. package/src/debug.ts +233 -0
  77. package/src/default-error-boundary.tsx +88 -0
  78. package/src/deps/browser.ts +8 -0
  79. package/src/deps/html-stream-client.ts +2 -0
  80. package/src/deps/html-stream-server.ts +2 -0
  81. package/src/deps/rsc.ts +10 -0
  82. package/src/deps/ssr.ts +2 -0
  83. package/src/errors.ts +295 -0
  84. package/src/handle.ts +130 -0
  85. package/src/handles/MetaTags.tsx +193 -0
  86. package/src/handles/index.ts +6 -0
  87. package/src/handles/meta.ts +247 -0
  88. package/src/host/cookie-handler.ts +159 -0
  89. package/src/host/errors.ts +97 -0
  90. package/src/host/index.ts +56 -0
  91. package/src/host/pattern-matcher.ts +214 -0
  92. package/src/host/router.ts +330 -0
  93. package/src/host/testing.ts +79 -0
  94. package/src/host/types.ts +138 -0
  95. package/src/host/utils.ts +25 -0
  96. package/src/href-client.ts +202 -0
  97. package/src/href-context.ts +33 -0
  98. package/src/index.rsc.ts +121 -0
  99. package/src/index.ts +165 -0
  100. package/src/loader.rsc.ts +207 -0
  101. package/src/loader.ts +47 -0
  102. package/src/network-error-thrower.tsx +21 -0
  103. package/src/outlet-context.ts +15 -0
  104. package/src/prerender/param-hash.ts +35 -0
  105. package/src/prerender/store.ts +40 -0
  106. package/src/prerender.ts +156 -0
  107. package/src/reverse.ts +267 -0
  108. package/src/root-error-boundary.tsx +277 -0
  109. package/src/route-content-wrapper.tsx +193 -0
  110. package/src/route-definition.ts +1431 -0
  111. package/src/route-map-builder.ts +242 -0
  112. package/src/route-types.ts +220 -0
  113. package/src/router/error-handling.ts +287 -0
  114. package/src/router/handler-context.ts +158 -0
  115. package/src/router/intercept-resolution.ts +387 -0
  116. package/src/router/loader-resolution.ts +327 -0
  117. package/src/router/manifest.ts +216 -0
  118. package/src/router/match-api.ts +621 -0
  119. package/src/router/match-context.ts +264 -0
  120. package/src/router/match-middleware/background-revalidation.ts +236 -0
  121. package/src/router/match-middleware/cache-lookup.ts +382 -0
  122. package/src/router/match-middleware/cache-store.ts +276 -0
  123. package/src/router/match-middleware/index.ts +81 -0
  124. package/src/router/match-middleware/intercept-resolution.ts +281 -0
  125. package/src/router/match-middleware/segment-resolution.ts +184 -0
  126. package/src/router/match-pipelines.ts +214 -0
  127. package/src/router/match-result.ts +213 -0
  128. package/src/router/metrics.ts +62 -0
  129. package/src/router/middleware.ts +791 -0
  130. package/src/router/pattern-matching.ts +407 -0
  131. package/src/router/revalidation.ts +190 -0
  132. package/src/router/router-context.ts +301 -0
  133. package/src/router/segment-resolution.ts +1315 -0
  134. package/src/router/trie-matching.ts +172 -0
  135. package/src/router/types.ts +163 -0
  136. package/src/router.gen.ts +6 -0
  137. package/src/router.ts +2423 -0
  138. package/src/rsc/handler.ts +1443 -0
  139. package/src/rsc/helpers.ts +64 -0
  140. package/src/rsc/index.ts +56 -0
  141. package/src/rsc/nonce.ts +18 -0
  142. package/src/rsc/types.ts +236 -0
  143. package/src/segment-system.tsx +442 -0
  144. package/src/server/context.ts +466 -0
  145. package/src/server/handle-store.ts +229 -0
  146. package/src/server/loader-registry.ts +174 -0
  147. package/src/server/request-context.ts +554 -0
  148. package/src/server/root-layout.tsx +10 -0
  149. package/src/server/tsconfig.json +14 -0
  150. package/src/server.ts +171 -0
  151. package/src/ssr/index.tsx +296 -0
  152. package/src/theme/ThemeProvider.tsx +291 -0
  153. package/src/theme/ThemeScript.tsx +61 -0
  154. package/src/theme/constants.ts +59 -0
  155. package/src/theme/index.ts +58 -0
  156. package/src/theme/theme-context.ts +70 -0
  157. package/src/theme/theme-script.ts +152 -0
  158. package/src/theme/types.ts +182 -0
  159. package/src/theme/use-theme.ts +44 -0
  160. package/src/types.ts +1757 -0
  161. package/src/urls.gen.ts +8 -0
  162. package/src/urls.ts +1282 -0
  163. package/src/use-loader.tsx +346 -0
  164. package/src/vite/expose-action-id.ts +344 -0
  165. package/src/vite/expose-handle-id.ts +209 -0
  166. package/src/vite/expose-loader-id.ts +426 -0
  167. package/src/vite/expose-location-state-id.ts +177 -0
  168. package/src/vite/expose-prerender-handler-id.ts +429 -0
  169. package/src/vite/index.ts +2068 -0
  170. package/src/vite/package-resolution.ts +125 -0
  171. package/src/vite/version.d.ts +12 -0
  172. package/src/vite/virtual-entries.ts +114 -0
package/CLAUDE.md ADDED
@@ -0,0 +1,43 @@
1
+ # @rangojs/router
2
+
3
+ Run `/rango` first to understand the API. Skills are in `node_modules/@rangojs/router/skills/`.
4
+
5
+ ## Tree-Structure-Critical Files (DO NOT MODIFY without understanding)
6
+
7
+ The following files control the React tree structure. Changing the tree structure
8
+ (element types, nesting depth, or keys at any position) between SSR, navigation,
9
+ and action renders will cause React to remount components, destroying client state
10
+ like `useActionState`, refs, and local state. This is extremely hard to debug.
11
+
12
+ **Protected files:**
13
+
14
+ - `src/segment-system.tsx` - `renderSegments()` builds the React tree from segments.
15
+ The `loading` property determines tree structure:
16
+ - `undefined` / `null` -> OutletProvider directly (no boundary)
17
+ - `false` -> LoaderBoundary + OutletProvider (boundary, no RouteContentWrapper)
18
+ - truthy (ReactNode) -> LoaderBoundary + OutletProvider + RouteContentWrapper
19
+
20
+ - `src/route-content-wrapper.tsx` - `LoaderBoundary` and `RouteContentWrapper`.
21
+ These add structural depth (Suspense boundaries) to the React tree.
22
+
23
+ - `src/browser/server-action-bridge.ts` - Merges server action segments with
24
+ cached segments. Must preserve cached `loading` values to prevent tree drift.
25
+
26
+ - `src/browser/partial-update.ts` - Merges navigation segments with cached segments.
27
+
28
+ **Rules:**
29
+
30
+ 1. Never change the conditional logic in `renderSegments()` that decides between
31
+ LoaderBoundary/RouteContentWrapper/OutletProvider without verifying all three
32
+ render paths (SSR, navigation, action) produce identical tree structures.
33
+
34
+ 2. Never add or remove wrapper elements (Suspense, div, Fragment) around segment
35
+ content without checking that the same wrappers exist in ALL render paths.
36
+
37
+ 3. When merging segments (action bridge, partial update), always preserve the
38
+ cached `loading` value if it differs from the server value. The server may
39
+ return different `loading` values based on `isSSR` context.
40
+
41
+ 4. Run `pnpm --filter @rangojs/router exec playwright test loader-behavior` after
42
+ any changes to these files. The skipSSR action tests specifically catch tree
43
+ structure regressions.
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # @rangojs/router
2
+
3
+ > **Warning:** This package is experimental and under active development. APIs may change without notice.
4
+
5
+ Type-safe RSC router with partial rendering support for Vite.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @rangojs/router@experimental
11
+ ```
12
+
13
+ ## Status
14
+
15
+ This package is in early experimental stages. It is not recommended for production use.
16
+
17
+ ## License
18
+
19
+ MIT
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin/rango.ts
4
+ import { resolve as resolve2 } from "node:path";
5
+
6
+ // src/build/generate-route-types.ts
7
+ import { readFileSync, writeFileSync, existsSync, readdirSync, unlinkSync } from "node:fs";
8
+ import { join, dirname, resolve, relative, basename as pathBasename } from "node:path";
9
+ import picomatch from "picomatch";
10
+ function extractRoutesFromSource(code) {
11
+ const routes = [];
12
+ const regex = /\bpath(?:\.(?:json|text|html|xml|image|stream|any))?\s*\(/g;
13
+ let match;
14
+ while ((match = regex.exec(code)) !== null) {
15
+ const result = parsePathCall(code, match.index + match[0].length);
16
+ if (result) routes.push(result);
17
+ }
18
+ return routes;
19
+ }
20
+ function generatePerModuleTypesSource(routes) {
21
+ const valid = routes.filter(({ name }) => {
22
+ if (!name || /["'\\`\n\r]/.test(name)) {
23
+ console.warn(`[rsc-router] Skipping route with invalid name: ${JSON.stringify(name)}`);
24
+ return false;
25
+ }
26
+ return true;
27
+ });
28
+ const deduped = /* @__PURE__ */ new Map();
29
+ for (const { name, pattern } of valid) {
30
+ deduped.set(name, pattern);
31
+ }
32
+ const sorted = [...deduped.entries()].sort(([a], [b]) => a.localeCompare(b));
33
+ const body = sorted.map(([name, pattern]) => {
34
+ const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
35
+ return ` ${key}: "${pattern}",`;
36
+ }).join("\n");
37
+ return `// Auto-generated by @rangojs/router - do not edit
38
+ export const routes = {
39
+ ${body}
40
+ } as const;
41
+ export type routes = typeof routes;
42
+ `;
43
+ }
44
+ function isWhitespace(ch) {
45
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r";
46
+ }
47
+ function readString(code, pos) {
48
+ const quote = code[pos];
49
+ if (quote !== '"' && quote !== "'") return null;
50
+ let value = "";
51
+ pos++;
52
+ while (pos < code.length) {
53
+ if (code[pos] === "\\") {
54
+ pos++;
55
+ if (pos < code.length) {
56
+ value += code[pos];
57
+ pos++;
58
+ }
59
+ continue;
60
+ }
61
+ if (code[pos] === quote) {
62
+ return { value, end: pos + 1 };
63
+ }
64
+ value += code[pos];
65
+ pos++;
66
+ }
67
+ return null;
68
+ }
69
+ function skipStringLiteral(code, pos) {
70
+ const quote = code[pos];
71
+ if (quote === "`") {
72
+ pos++;
73
+ while (pos < code.length) {
74
+ if (code[pos] === "\\") {
75
+ pos += 2;
76
+ continue;
77
+ }
78
+ if (code[pos] === "`") return pos + 1;
79
+ if (code[pos] === "$" && pos + 1 < code.length && code[pos + 1] === "{") {
80
+ pos += 2;
81
+ let braceDepth = 1;
82
+ while (pos < code.length && braceDepth > 0) {
83
+ if (code[pos] === "{") braceDepth++;
84
+ else if (code[pos] === "}") braceDepth--;
85
+ else if (code[pos] === "\\") pos++;
86
+ else if (code[pos] === '"' || code[pos] === "'" || code[pos] === "`") {
87
+ pos = skipStringLiteral(code, pos);
88
+ continue;
89
+ }
90
+ if (braceDepth > 0) pos++;
91
+ }
92
+ continue;
93
+ }
94
+ pos++;
95
+ }
96
+ return pos;
97
+ }
98
+ pos++;
99
+ while (pos < code.length) {
100
+ if (code[pos] === "\\") {
101
+ pos += 2;
102
+ continue;
103
+ }
104
+ if (code[pos] === quote) return pos + 1;
105
+ pos++;
106
+ }
107
+ return pos;
108
+ }
109
+ function matchesNameColon(code, pos) {
110
+ if (code.slice(pos, pos + 4) !== "name") return false;
111
+ if (pos > 0 && /\w/.test(code[pos - 1])) return false;
112
+ const afterName = pos + 4;
113
+ if (afterName < code.length && /\w/.test(code[afterName])) return false;
114
+ let checkPos = afterName;
115
+ while (checkPos < code.length && isWhitespace(code[checkPos])) checkPos++;
116
+ return code[checkPos] === ":";
117
+ }
118
+ function extractNameValue(code, pos) {
119
+ pos += 4;
120
+ while (pos < code.length && isWhitespace(code[pos])) pos++;
121
+ pos++;
122
+ while (pos < code.length && isWhitespace(code[pos])) pos++;
123
+ return readString(code, pos);
124
+ }
125
+ function parsePathCall(code, pos) {
126
+ while (pos < code.length && isWhitespace(code[pos])) pos++;
127
+ const patternStr = readString(code, pos);
128
+ if (!patternStr) return null;
129
+ const pattern = patternStr.value;
130
+ pos = patternStr.end;
131
+ let depth = 1;
132
+ let name = null;
133
+ while (pos < code.length && depth > 0) {
134
+ const ch = code[pos];
135
+ if (isWhitespace(ch)) {
136
+ pos++;
137
+ continue;
138
+ }
139
+ if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
140
+ pos += 2;
141
+ while (pos < code.length && code[pos] !== "\n") pos++;
142
+ continue;
143
+ }
144
+ if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
145
+ pos += 2;
146
+ while (pos < code.length - 1 && !(code[pos] === "*" && code[pos + 1] === "/"))
147
+ pos++;
148
+ pos += 2;
149
+ continue;
150
+ }
151
+ if (depth === 2 && ch === "n" && matchesNameColon(code, pos)) {
152
+ const nameResult = extractNameValue(code, pos);
153
+ if (nameResult) {
154
+ name = nameResult.value;
155
+ pos = nameResult.end;
156
+ continue;
157
+ }
158
+ }
159
+ if (ch === '"' || ch === "`" || ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1]))) {
160
+ pos = skipStringLiteral(code, pos);
161
+ continue;
162
+ }
163
+ if (ch === "(" || ch === "{" || ch === "[") depth++;
164
+ else if (ch === ")" || ch === "}" || ch === "]") depth--;
165
+ pos++;
166
+ }
167
+ if (name === null) return null;
168
+ return { name, pattern };
169
+ }
170
+ function findTsFiles(dir, filter) {
171
+ const results = [];
172
+ let entries;
173
+ try {
174
+ entries = readdirSync(dir, { withFileTypes: true });
175
+ } catch (err) {
176
+ console.warn(`[rsc-router] Failed to scan directory ${dir}: ${err.message}`);
177
+ return results;
178
+ }
179
+ for (const entry of entries) {
180
+ const fullPath = join(dir, entry.name);
181
+ if (entry.isDirectory()) {
182
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
183
+ results.push(...findTsFiles(fullPath, filter));
184
+ } else if ((entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) && !entry.name.includes(".gen.")) {
185
+ if (filter && !filter(fullPath)) continue;
186
+ results.push(fullPath);
187
+ }
188
+ }
189
+ return results;
190
+ }
191
+ function writePerModuleRouteTypesForFile(filePath) {
192
+ try {
193
+ const source = readFileSync(filePath, "utf-8");
194
+ if (!source.includes("urls(")) return;
195
+ const routes = extractRoutesFromSource(source);
196
+ if (routes.length === 0) return;
197
+ const genPath = filePath.replace(/\.(tsx?)$/, ".gen.ts");
198
+ const genSource = generatePerModuleTypesSource(routes);
199
+ const existing = existsSync(genPath) ? readFileSync(genPath, "utf-8") : null;
200
+ if (existing !== genSource) {
201
+ writeFileSync(genPath, genSource);
202
+ console.log(`[rsc-router] Generated route types -> ${genPath}`);
203
+ }
204
+ } catch (err) {
205
+ console.warn(`[rsc-router] Failed to generate route types for ${filePath}: ${err.message}`);
206
+ }
207
+ }
208
+
209
+ // src/bin/rango.ts
210
+ var [command, ...args] = process.argv.slice(2);
211
+ if (command === "extract-names") {
212
+ const dir = args[0] ?? "./src";
213
+ const resolvedDir = resolve2(dir);
214
+ console.log(`[rango] Scanning ${resolvedDir} for url modules...`);
215
+ const files = findTsFiles(resolvedDir);
216
+ for (const filePath of files) {
217
+ writePerModuleRouteTypesForFile(filePath);
218
+ }
219
+ console.log(`[rango] Scanned ${files.length} file(s)`);
220
+ process.exit(0);
221
+ } else {
222
+ console.log(`Usage: rango <command>
223
+
224
+ Commands:
225
+ extract-names [dir] Extract route names from url modules (default: ./src)`);
226
+ process.exit(command ? 1 : 0);
227
+ }