@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.9
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/dist/bin/rango.js +225 -0
- package/dist/vite/index.js +1411 -74
- package/package.json +5 -2
- package/skills/fonts/SKILL.md +165 -0
- package/skills/hooks/SKILL.md +1 -1
- package/skills/links/SKILL.md +21 -21
- package/skills/mime-routes/SKILL.md +124 -0
- package/skills/prerender/SKILL.md +283 -0
- package/skills/rango/SKILL.md +6 -1
- package/skills/response-routes/SKILL.md +358 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/typesafety/SKILL.md +12 -12
- package/src/bin/rango.ts +24 -0
- package/src/browser/navigation-client.ts +3 -0
- package/src/browser/react/Link.tsx +3 -3
- package/src/browser/react/use-handle.ts +3 -9
- package/src/browser/react/use-href.tsx +2 -2
- package/src/browser/rsc-router.tsx +2 -0
- package/src/browser/types.ts +2 -0
- package/src/build/generate-manifest.ts +163 -13
- package/src/build/generate-route-types.ts +752 -0
- package/src/build/index.ts +12 -0
- package/src/build/route-trie.ts +239 -0
- package/src/cache/cache-scope.ts +2 -4
- package/src/client.tsx +25 -3
- package/src/errors.ts +36 -0
- package/src/handle.ts +0 -19
- package/src/href-client.ts +73 -3
- package/src/index.rsc.ts +28 -6
- package/src/index.ts +74 -5
- package/src/loader.rsc.ts +7 -20
- package/src/prerender/param-hash.ts +35 -0
- package/src/prerender/store.ts +40 -0
- package/src/prerender.ts +156 -0
- package/src/{href.ts → reverse.ts} +47 -35
- package/src/route-definition.ts +15 -12
- package/src/route-map-builder.ts +60 -5
- package/src/route-types.ts +16 -4
- package/src/router/handler-context.ts +2 -2
- package/src/router/intercept-resolution.ts +387 -0
- package/src/router/manifest.ts +66 -4
- package/src/router/match-api.ts +621 -0
- package/src/router/match-context.ts +1 -1
- package/src/router/match-middleware/cache-lookup.ts +121 -0
- package/src/router/match-middleware/cache-store.ts +10 -0
- package/src/router/match-middleware/intercept-resolution.ts +13 -0
- package/src/router/match-middleware/segment-resolution.ts +10 -0
- package/src/router/pattern-matching.ts +14 -2
- package/src/router/segment-resolution.ts +1315 -0
- package/src/router/trie-matching.ts +172 -0
- package/src/router/types.ts +68 -1
- package/src/router.gen.ts +6 -0
- package/src/router.ts +586 -2758
- package/src/rsc/handler.ts +426 -62
- package/src/server/context.ts +12 -6
- package/src/server.ts +30 -5
- package/src/types.ts +245 -111
- package/src/urls.gen.ts +8 -0
- package/src/urls.ts +562 -82
- package/src/vite/expose-action-id.ts +1 -1
- package/src/vite/expose-prerender-handler-id.ts +429 -0
- package/src/vite/index.ts +927 -97
- package/src/server/route-manifest-cache.ts +0 -173
|
@@ -0,0 +1,225 @@
|
|
|
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 } from "node:fs";
|
|
8
|
+
import { join, dirname, resolve } from "node:path";
|
|
9
|
+
function extractRoutesFromSource(code) {
|
|
10
|
+
const routes = [];
|
|
11
|
+
const regex = /\bpath(?:\.(?:json|text|html|xml|image|stream|any))?\s*\(/g;
|
|
12
|
+
let match;
|
|
13
|
+
while ((match = regex.exec(code)) !== null) {
|
|
14
|
+
const result = parsePathCall(code, match.index + match[0].length);
|
|
15
|
+
if (result) routes.push(result);
|
|
16
|
+
}
|
|
17
|
+
return routes;
|
|
18
|
+
}
|
|
19
|
+
function generatePerModuleTypesSource(routes) {
|
|
20
|
+
const valid = routes.filter(({ name }) => {
|
|
21
|
+
if (!name || /["'\\`\n\r]/.test(name)) {
|
|
22
|
+
console.warn(`[rsc-router] Skipping route with invalid name: ${JSON.stringify(name)}`);
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
});
|
|
27
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
28
|
+
for (const { name, pattern } of valid) {
|
|
29
|
+
deduped.set(name, pattern);
|
|
30
|
+
}
|
|
31
|
+
const sorted = [...deduped.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
32
|
+
const body = sorted.map(([name, pattern]) => {
|
|
33
|
+
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
|
|
34
|
+
return ` ${key}: "${pattern}",`;
|
|
35
|
+
}).join("\n");
|
|
36
|
+
return `// Auto-generated by @rangojs/router - do not edit
|
|
37
|
+
export const routes = {
|
|
38
|
+
${body}
|
|
39
|
+
} as const;
|
|
40
|
+
export type routes = typeof routes;
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
function isWhitespace(ch) {
|
|
44
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r";
|
|
45
|
+
}
|
|
46
|
+
function readString(code, pos) {
|
|
47
|
+
const quote = code[pos];
|
|
48
|
+
if (quote !== '"' && quote !== "'") return null;
|
|
49
|
+
let value = "";
|
|
50
|
+
pos++;
|
|
51
|
+
while (pos < code.length) {
|
|
52
|
+
if (code[pos] === "\\") {
|
|
53
|
+
pos++;
|
|
54
|
+
if (pos < code.length) {
|
|
55
|
+
value += code[pos];
|
|
56
|
+
pos++;
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (code[pos] === quote) {
|
|
61
|
+
return { value, end: pos + 1 };
|
|
62
|
+
}
|
|
63
|
+
value += code[pos];
|
|
64
|
+
pos++;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function skipStringLiteral(code, pos) {
|
|
69
|
+
const quote = code[pos];
|
|
70
|
+
if (quote === "`") {
|
|
71
|
+
pos++;
|
|
72
|
+
while (pos < code.length) {
|
|
73
|
+
if (code[pos] === "\\") {
|
|
74
|
+
pos += 2;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (code[pos] === "`") return pos + 1;
|
|
78
|
+
if (code[pos] === "$" && pos + 1 < code.length && code[pos + 1] === "{") {
|
|
79
|
+
pos += 2;
|
|
80
|
+
let braceDepth = 1;
|
|
81
|
+
while (pos < code.length && braceDepth > 0) {
|
|
82
|
+
if (code[pos] === "{") braceDepth++;
|
|
83
|
+
else if (code[pos] === "}") braceDepth--;
|
|
84
|
+
else if (code[pos] === "\\") pos++;
|
|
85
|
+
else if (code[pos] === '"' || code[pos] === "'" || code[pos] === "`") {
|
|
86
|
+
pos = skipStringLiteral(code, pos);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (braceDepth > 0) pos++;
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
pos++;
|
|
94
|
+
}
|
|
95
|
+
return pos;
|
|
96
|
+
}
|
|
97
|
+
pos++;
|
|
98
|
+
while (pos < code.length) {
|
|
99
|
+
if (code[pos] === "\\") {
|
|
100
|
+
pos += 2;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (code[pos] === quote) return pos + 1;
|
|
104
|
+
pos++;
|
|
105
|
+
}
|
|
106
|
+
return pos;
|
|
107
|
+
}
|
|
108
|
+
function matchesNameColon(code, pos) {
|
|
109
|
+
if (code.slice(pos, pos + 4) !== "name") return false;
|
|
110
|
+
if (pos > 0 && /\w/.test(code[pos - 1])) return false;
|
|
111
|
+
const afterName = pos + 4;
|
|
112
|
+
if (afterName < code.length && /\w/.test(code[afterName])) return false;
|
|
113
|
+
let checkPos = afterName;
|
|
114
|
+
while (checkPos < code.length && isWhitespace(code[checkPos])) checkPos++;
|
|
115
|
+
return code[checkPos] === ":";
|
|
116
|
+
}
|
|
117
|
+
function extractNameValue(code, pos) {
|
|
118
|
+
pos += 4;
|
|
119
|
+
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
120
|
+
pos++;
|
|
121
|
+
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
122
|
+
return readString(code, pos);
|
|
123
|
+
}
|
|
124
|
+
function parsePathCall(code, pos) {
|
|
125
|
+
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
126
|
+
const patternStr = readString(code, pos);
|
|
127
|
+
if (!patternStr) return null;
|
|
128
|
+
const pattern = patternStr.value;
|
|
129
|
+
pos = patternStr.end;
|
|
130
|
+
let depth = 1;
|
|
131
|
+
let name = null;
|
|
132
|
+
while (pos < code.length && depth > 0) {
|
|
133
|
+
const ch = code[pos];
|
|
134
|
+
if (isWhitespace(ch)) {
|
|
135
|
+
pos++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
|
|
139
|
+
pos += 2;
|
|
140
|
+
while (pos < code.length && code[pos] !== "\n") pos++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
|
|
144
|
+
pos += 2;
|
|
145
|
+
while (pos < code.length - 1 && !(code[pos] === "*" && code[pos + 1] === "/"))
|
|
146
|
+
pos++;
|
|
147
|
+
pos += 2;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (depth === 2 && ch === "n" && matchesNameColon(code, pos)) {
|
|
151
|
+
const nameResult = extractNameValue(code, pos);
|
|
152
|
+
if (nameResult) {
|
|
153
|
+
name = nameResult.value;
|
|
154
|
+
pos = nameResult.end;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (ch === '"' || ch === "`" || ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1]))) {
|
|
159
|
+
pos = skipStringLiteral(code, pos);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
163
|
+
else if (ch === ")" || ch === "}" || ch === "]") depth--;
|
|
164
|
+
pos++;
|
|
165
|
+
}
|
|
166
|
+
if (name === null) return null;
|
|
167
|
+
return { name, pattern };
|
|
168
|
+
}
|
|
169
|
+
function findTsFiles(dir) {
|
|
170
|
+
const results = [];
|
|
171
|
+
let entries;
|
|
172
|
+
try {
|
|
173
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.warn(`[rsc-router] Failed to scan directory ${dir}: ${err.message}`);
|
|
176
|
+
return results;
|
|
177
|
+
}
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
const fullPath = join(dir, entry.name);
|
|
180
|
+
if (entry.isDirectory()) {
|
|
181
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
182
|
+
results.push(...findTsFiles(fullPath));
|
|
183
|
+
} else if ((entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) && !entry.name.includes(".gen.")) {
|
|
184
|
+
results.push(fullPath);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return results;
|
|
188
|
+
}
|
|
189
|
+
function writePerModuleRouteTypesForFile(filePath) {
|
|
190
|
+
try {
|
|
191
|
+
const source = readFileSync(filePath, "utf-8");
|
|
192
|
+
if (!source.includes("urls(")) return;
|
|
193
|
+
const routes = extractRoutesFromSource(source);
|
|
194
|
+
if (routes.length === 0) return;
|
|
195
|
+
const genPath = filePath.replace(/\.(tsx?)$/, ".gen.ts");
|
|
196
|
+
const genSource = generatePerModuleTypesSource(routes);
|
|
197
|
+
const existing = existsSync(genPath) ? readFileSync(genPath, "utf-8") : null;
|
|
198
|
+
if (existing !== genSource) {
|
|
199
|
+
writeFileSync(genPath, genSource);
|
|
200
|
+
console.log(`[rsc-router] Generated route types -> ${genPath}`);
|
|
201
|
+
}
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.warn(`[rsc-router] Failed to generate route types for ${filePath}: ${err.message}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/bin/rango.ts
|
|
208
|
+
var [command, ...args] = process.argv.slice(2);
|
|
209
|
+
if (command === "extract-names") {
|
|
210
|
+
const dir = args[0] ?? "./src";
|
|
211
|
+
const resolvedDir = resolve2(dir);
|
|
212
|
+
console.log(`[rango] Scanning ${resolvedDir} for url modules...`);
|
|
213
|
+
const files = findTsFiles(resolvedDir);
|
|
214
|
+
for (const filePath of files) {
|
|
215
|
+
writePerModuleRouteTypesForFile(filePath);
|
|
216
|
+
}
|
|
217
|
+
console.log(`[rango] Scanned ${files.length} file(s)`);
|
|
218
|
+
process.exit(0);
|
|
219
|
+
} else {
|
|
220
|
+
console.log(`Usage: rango <command>
|
|
221
|
+
|
|
222
|
+
Commands:
|
|
223
|
+
extract-names [dir] Extract route names from url modules (default: ./src)`);
|
|
224
|
+
process.exit(command ? 1 : 0);
|
|
225
|
+
}
|