@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.
- package/CLAUDE.md +43 -0
- package/README.md +19 -0
- package/dist/bin/rango.js +227 -0
- package/dist/vite/index.js +3039 -0
- package/package.json +171 -0
- package/skills/caching/SKILL.md +191 -0
- package/skills/debug-manifest/SKILL.md +108 -0
- package/skills/document-cache/SKILL.md +180 -0
- package/skills/fonts/SKILL.md +165 -0
- package/skills/hooks/SKILL.md +442 -0
- package/skills/intercept/SKILL.md +190 -0
- package/skills/layout/SKILL.md +213 -0
- package/skills/links/SKILL.md +180 -0
- package/skills/loader/SKILL.md +246 -0
- package/skills/middleware/SKILL.md +202 -0
- package/skills/mime-routes/SKILL.md +124 -0
- package/skills/parallel/SKILL.md +228 -0
- package/skills/prerender/SKILL.md +283 -0
- package/skills/rango/SKILL.md +54 -0
- package/skills/response-routes/SKILL.md +358 -0
- package/skills/route/SKILL.md +173 -0
- package/skills/router-setup/SKILL.md +346 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +78 -0
- package/skills/typesafety/SKILL.md +394 -0
- package/src/__internal.ts +175 -0
- package/src/bin/rango.ts +24 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +913 -0
- package/src/browser/navigation-client.ts +165 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +600 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +346 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/mount-context.ts +32 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +203 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +140 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +164 -0
- package/src/browser/rsc-router.tsx +352 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/segment-structure-assert.ts +67 -0
- package/src/browser/server-action-bridge.ts +762 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +478 -0
- package/src/build/generate-manifest.ts +377 -0
- package/src/build/generate-route-types.ts +828 -0
- package/src/build/index.ts +36 -0
- package/src/build/route-trie.ts +239 -0
- package/src/cache/cache-scope.ts +563 -0
- package/src/cache/cf/cf-cache-store.ts +428 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/document-cache.ts +340 -0
- package/src/cache/index.ts +58 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +392 -0
- package/src/client.rsc.tsx +83 -0
- package/src/client.tsx +643 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +23 -0
- package/src/debug.ts +233 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +295 -0
- package/src/handle.ts +130 -0
- package/src/handles/MetaTags.tsx +193 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/host/cookie-handler.ts +159 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +56 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +330 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +138 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +202 -0
- package/src/href-context.ts +33 -0
- package/src/index.rsc.ts +121 -0
- package/src/index.ts +165 -0
- package/src/loader.rsc.ts +207 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/prerender/param-hash.ts +35 -0
- package/src/prerender/store.ts +40 -0
- package/src/prerender.ts +156 -0
- package/src/reverse.ts +267 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +193 -0
- package/src/route-definition.ts +1431 -0
- package/src/route-map-builder.ts +242 -0
- package/src/route-types.ts +220 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +158 -0
- package/src/router/intercept-resolution.ts +387 -0
- package/src/router/loader-resolution.ts +327 -0
- package/src/router/manifest.ts +216 -0
- package/src/router/match-api.ts +621 -0
- package/src/router/match-context.ts +264 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +382 -0
- package/src/router/match-middleware/cache-store.ts +276 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +281 -0
- package/src/router/match-middleware/segment-resolution.ts +184 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +213 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.ts +791 -0
- package/src/router/pattern-matching.ts +407 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +301 -0
- package/src/router/segment-resolution.ts +1315 -0
- package/src/router/trie-matching.ts +172 -0
- package/src/router/types.ts +163 -0
- package/src/router.gen.ts +6 -0
- package/src/router.ts +2423 -0
- package/src/rsc/handler.ts +1443 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +236 -0
- package/src/segment-system.tsx +442 -0
- package/src/server/context.ts +466 -0
- package/src/server/handle-store.ts +229 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +171 -0
- package/src/ssr/index.tsx +296 -0
- package/src/theme/ThemeProvider.tsx +291 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +59 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/theme-context.ts +70 -0
- package/src/theme/theme-script.ts +152 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types.ts +1757 -0
- package/src/urls.gen.ts +8 -0
- package/src/urls.ts +1282 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +426 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/expose-prerender-handler-id.ts +429 -0
- package/src/vite/index.ts +2068 -0
- package/src/vite/package-resolution.ts +125 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +114 -0
|
@@ -0,0 +1,3039 @@
|
|
|
1
|
+
// src/vite/index.ts
|
|
2
|
+
import { createServer as createViteServer } from "vite";
|
|
3
|
+
import * as Vite from "vite";
|
|
4
|
+
import { resolve as resolve3, join as join2, dirname as dirname2, basename } from "node:path";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { mkdirSync, writeFileSync as writeFileSync2, readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "node:fs";
|
|
8
|
+
|
|
9
|
+
// src/build/generate-route-types.ts
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, unlinkSync } from "node:fs";
|
|
11
|
+
import { join, dirname, resolve, relative, basename as pathBasename } from "node:path";
|
|
12
|
+
import picomatch from "picomatch";
|
|
13
|
+
function extractRoutesFromSource(code) {
|
|
14
|
+
const routes = [];
|
|
15
|
+
const regex = /\bpath(?:\.(?:json|text|html|xml|image|stream|any))?\s*\(/g;
|
|
16
|
+
let match;
|
|
17
|
+
while ((match = regex.exec(code)) !== null) {
|
|
18
|
+
const result = parsePathCall(code, match.index + match[0].length);
|
|
19
|
+
if (result) routes.push(result);
|
|
20
|
+
}
|
|
21
|
+
return routes;
|
|
22
|
+
}
|
|
23
|
+
function generatePerModuleTypesSource(routes) {
|
|
24
|
+
const valid = routes.filter(({ name }) => {
|
|
25
|
+
if (!name || /["'\\`\n\r]/.test(name)) {
|
|
26
|
+
console.warn(`[rsc-router] Skipping route with invalid name: ${JSON.stringify(name)}`);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
});
|
|
31
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
32
|
+
for (const { name, pattern } of valid) {
|
|
33
|
+
deduped.set(name, pattern);
|
|
34
|
+
}
|
|
35
|
+
const sorted = [...deduped.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
36
|
+
const body = sorted.map(([name, pattern]) => {
|
|
37
|
+
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
|
|
38
|
+
return ` ${key}: "${pattern}",`;
|
|
39
|
+
}).join("\n");
|
|
40
|
+
return `// Auto-generated by @rangojs/router - do not edit
|
|
41
|
+
export const routes = {
|
|
42
|
+
${body}
|
|
43
|
+
} as const;
|
|
44
|
+
export type routes = typeof routes;
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
function isWhitespace(ch) {
|
|
48
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r";
|
|
49
|
+
}
|
|
50
|
+
function readString(code, pos) {
|
|
51
|
+
const quote = code[pos];
|
|
52
|
+
if (quote !== '"' && quote !== "'") return null;
|
|
53
|
+
let value = "";
|
|
54
|
+
pos++;
|
|
55
|
+
while (pos < code.length) {
|
|
56
|
+
if (code[pos] === "\\") {
|
|
57
|
+
pos++;
|
|
58
|
+
if (pos < code.length) {
|
|
59
|
+
value += code[pos];
|
|
60
|
+
pos++;
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (code[pos] === quote) {
|
|
65
|
+
return { value, end: pos + 1 };
|
|
66
|
+
}
|
|
67
|
+
value += code[pos];
|
|
68
|
+
pos++;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function skipStringLiteral(code, pos) {
|
|
73
|
+
const quote = code[pos];
|
|
74
|
+
if (quote === "`") {
|
|
75
|
+
pos++;
|
|
76
|
+
while (pos < code.length) {
|
|
77
|
+
if (code[pos] === "\\") {
|
|
78
|
+
pos += 2;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (code[pos] === "`") return pos + 1;
|
|
82
|
+
if (code[pos] === "$" && pos + 1 < code.length && code[pos + 1] === "{") {
|
|
83
|
+
pos += 2;
|
|
84
|
+
let braceDepth = 1;
|
|
85
|
+
while (pos < code.length && braceDepth > 0) {
|
|
86
|
+
if (code[pos] === "{") braceDepth++;
|
|
87
|
+
else if (code[pos] === "}") braceDepth--;
|
|
88
|
+
else if (code[pos] === "\\") pos++;
|
|
89
|
+
else if (code[pos] === '"' || code[pos] === "'" || code[pos] === "`") {
|
|
90
|
+
pos = skipStringLiteral(code, pos);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (braceDepth > 0) pos++;
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
pos++;
|
|
98
|
+
}
|
|
99
|
+
return pos;
|
|
100
|
+
}
|
|
101
|
+
pos++;
|
|
102
|
+
while (pos < code.length) {
|
|
103
|
+
if (code[pos] === "\\") {
|
|
104
|
+
pos += 2;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (code[pos] === quote) return pos + 1;
|
|
108
|
+
pos++;
|
|
109
|
+
}
|
|
110
|
+
return pos;
|
|
111
|
+
}
|
|
112
|
+
function matchesNameColon(code, pos) {
|
|
113
|
+
if (code.slice(pos, pos + 4) !== "name") return false;
|
|
114
|
+
if (pos > 0 && /\w/.test(code[pos - 1])) return false;
|
|
115
|
+
const afterName = pos + 4;
|
|
116
|
+
if (afterName < code.length && /\w/.test(code[afterName])) return false;
|
|
117
|
+
let checkPos = afterName;
|
|
118
|
+
while (checkPos < code.length && isWhitespace(code[checkPos])) checkPos++;
|
|
119
|
+
return code[checkPos] === ":";
|
|
120
|
+
}
|
|
121
|
+
function extractNameValue(code, pos) {
|
|
122
|
+
pos += 4;
|
|
123
|
+
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
124
|
+
pos++;
|
|
125
|
+
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
126
|
+
return readString(code, pos);
|
|
127
|
+
}
|
|
128
|
+
function parsePathCall(code, pos) {
|
|
129
|
+
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
130
|
+
const patternStr = readString(code, pos);
|
|
131
|
+
if (!patternStr) return null;
|
|
132
|
+
const pattern = patternStr.value;
|
|
133
|
+
pos = patternStr.end;
|
|
134
|
+
let depth = 1;
|
|
135
|
+
let name = null;
|
|
136
|
+
while (pos < code.length && depth > 0) {
|
|
137
|
+
const ch = code[pos];
|
|
138
|
+
if (isWhitespace(ch)) {
|
|
139
|
+
pos++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
|
|
143
|
+
pos += 2;
|
|
144
|
+
while (pos < code.length && code[pos] !== "\n") pos++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
|
|
148
|
+
pos += 2;
|
|
149
|
+
while (pos < code.length - 1 && !(code[pos] === "*" && code[pos + 1] === "/"))
|
|
150
|
+
pos++;
|
|
151
|
+
pos += 2;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (depth === 2 && ch === "n" && matchesNameColon(code, pos)) {
|
|
155
|
+
const nameResult = extractNameValue(code, pos);
|
|
156
|
+
if (nameResult) {
|
|
157
|
+
name = nameResult.value;
|
|
158
|
+
pos = nameResult.end;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (ch === '"' || ch === "`" || ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1]))) {
|
|
163
|
+
pos = skipStringLiteral(code, pos);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
167
|
+
else if (ch === ")" || ch === "}" || ch === "]") depth--;
|
|
168
|
+
pos++;
|
|
169
|
+
}
|
|
170
|
+
if (name === null) return null;
|
|
171
|
+
return { name, pattern };
|
|
172
|
+
}
|
|
173
|
+
function generateRouteTypesSource(routeManifest) {
|
|
174
|
+
const entries = Object.entries(routeManifest).sort(
|
|
175
|
+
([a], [b]) => a.localeCompare(b)
|
|
176
|
+
);
|
|
177
|
+
const interfaceBody = entries.map(([name, pattern]) => ` "${name}": "${pattern}";`).join("\n");
|
|
178
|
+
return `// Auto-generated by @rangojs/router - do not edit
|
|
179
|
+
export {};
|
|
180
|
+
|
|
181
|
+
declare global {
|
|
182
|
+
namespace RSCRouter {
|
|
183
|
+
interface GeneratedRouteMap {
|
|
184
|
+
${interfaceBody}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
var DEFAULT_EXCLUDE_PATTERNS = [
|
|
191
|
+
"**/__tests__/**",
|
|
192
|
+
"**/__mocks__/**",
|
|
193
|
+
"**/dist/**",
|
|
194
|
+
"**/coverage/**",
|
|
195
|
+
"**/*.test.{ts,tsx}",
|
|
196
|
+
"**/*.spec.{ts,tsx}"
|
|
197
|
+
];
|
|
198
|
+
function createScanFilter(root, opts) {
|
|
199
|
+
const { include, exclude } = opts;
|
|
200
|
+
const hasInclude = include && include.length > 0;
|
|
201
|
+
const hasCustomExclude = exclude !== void 0;
|
|
202
|
+
if (!hasInclude && !hasCustomExclude) return void 0;
|
|
203
|
+
const effectiveExclude = exclude ?? DEFAULT_EXCLUDE_PATTERNS;
|
|
204
|
+
const includeMatcher = hasInclude ? picomatch(include) : null;
|
|
205
|
+
const excludeMatcher = effectiveExclude.length > 0 ? picomatch(effectiveExclude) : null;
|
|
206
|
+
return (absolutePath) => {
|
|
207
|
+
const rel = relative(root, absolutePath);
|
|
208
|
+
if (excludeMatcher && excludeMatcher(rel)) return false;
|
|
209
|
+
if (includeMatcher) return includeMatcher(rel);
|
|
210
|
+
return true;
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function findTsFiles(dir, filter) {
|
|
214
|
+
const results = [];
|
|
215
|
+
let entries;
|
|
216
|
+
try {
|
|
217
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.warn(`[rsc-router] Failed to scan directory ${dir}: ${err.message}`);
|
|
220
|
+
return results;
|
|
221
|
+
}
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
const fullPath = join(dir, entry.name);
|
|
224
|
+
if (entry.isDirectory()) {
|
|
225
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
226
|
+
results.push(...findTsFiles(fullPath, filter));
|
|
227
|
+
} else if ((entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) && !entry.name.includes(".gen.")) {
|
|
228
|
+
if (filter && !filter(fullPath)) continue;
|
|
229
|
+
results.push(fullPath);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return results;
|
|
233
|
+
}
|
|
234
|
+
function writePerModuleRouteTypes(root, filter) {
|
|
235
|
+
const files = findTsFiles(root, filter);
|
|
236
|
+
for (const filePath of files) {
|
|
237
|
+
writePerModuleRouteTypesForFile(filePath);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function writePerModuleRouteTypesForFile(filePath) {
|
|
241
|
+
try {
|
|
242
|
+
const source = readFileSync(filePath, "utf-8");
|
|
243
|
+
if (!source.includes("urls(")) return;
|
|
244
|
+
const routes = extractRoutesFromSource(source);
|
|
245
|
+
if (routes.length === 0) return;
|
|
246
|
+
const genPath = filePath.replace(/\.(tsx?)$/, ".gen.ts");
|
|
247
|
+
const genSource = generatePerModuleTypesSource(routes);
|
|
248
|
+
const existing = existsSync(genPath) ? readFileSync(genPath, "utf-8") : null;
|
|
249
|
+
if (existing !== genSource) {
|
|
250
|
+
writeFileSync(genPath, genSource);
|
|
251
|
+
console.log(`[rsc-router] Generated route types -> ${genPath}`);
|
|
252
|
+
}
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.warn(`[rsc-router] Failed to generate route types for ${filePath}: ${err.message}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function extractIncludesFromSource(code) {
|
|
258
|
+
const results = [];
|
|
259
|
+
const regex = /\binclude\s*\(/g;
|
|
260
|
+
let match;
|
|
261
|
+
while ((match = regex.exec(code)) !== null) {
|
|
262
|
+
const result = parseIncludeCall(code, match.index + match[0].length);
|
|
263
|
+
if (result) results.push(result);
|
|
264
|
+
}
|
|
265
|
+
return results;
|
|
266
|
+
}
|
|
267
|
+
function parseIncludeCall(code, pos) {
|
|
268
|
+
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
269
|
+
const prefixStr = readString(code, pos);
|
|
270
|
+
if (!prefixStr) return null;
|
|
271
|
+
const pathPrefix = prefixStr.value;
|
|
272
|
+
pos = prefixStr.end;
|
|
273
|
+
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
274
|
+
if (pos >= code.length || code[pos] !== ",") return null;
|
|
275
|
+
pos++;
|
|
276
|
+
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
277
|
+
const varStart = pos;
|
|
278
|
+
while (pos < code.length && /[\w$]/.test(code[pos])) pos++;
|
|
279
|
+
if (pos === varStart) return null;
|
|
280
|
+
const variableName = code.slice(varStart, pos);
|
|
281
|
+
let namePrefix = null;
|
|
282
|
+
let depth = 1;
|
|
283
|
+
while (pos < code.length && depth > 0) {
|
|
284
|
+
const ch = code[pos];
|
|
285
|
+
if (isWhitespace(ch)) {
|
|
286
|
+
pos++;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
|
|
290
|
+
pos += 2;
|
|
291
|
+
while (pos < code.length && code[pos] !== "\n") pos++;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
|
|
295
|
+
pos += 2;
|
|
296
|
+
while (pos < code.length - 1 && !(code[pos] === "*" && code[pos + 1] === "/"))
|
|
297
|
+
pos++;
|
|
298
|
+
pos += 2;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (depth === 2 && ch === "n" && matchesNameColon(code, pos)) {
|
|
302
|
+
const nameResult = extractNameValue(code, pos);
|
|
303
|
+
if (nameResult) {
|
|
304
|
+
namePrefix = nameResult.value;
|
|
305
|
+
pos = nameResult.end;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (ch === '"' || ch === "`" || ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1]))) {
|
|
310
|
+
pos = skipStringLiteral(code, pos);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
314
|
+
else if (ch === ")" || ch === "}" || ch === "]") depth--;
|
|
315
|
+
pos++;
|
|
316
|
+
}
|
|
317
|
+
return { pathPrefix, variableName, namePrefix };
|
|
318
|
+
}
|
|
319
|
+
function resolveImportedVariable(code, localName) {
|
|
320
|
+
const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
|
|
321
|
+
let match;
|
|
322
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
323
|
+
const imports = match[1];
|
|
324
|
+
const specifier = match[2];
|
|
325
|
+
const parts = imports.split(",").map((s) => s.trim()).filter(Boolean);
|
|
326
|
+
for (const part of parts) {
|
|
327
|
+
const asMatch = part.match(/^(\w+)\s+as\s+(\w+)$/);
|
|
328
|
+
if (asMatch && asMatch[2] === localName)
|
|
329
|
+
return { specifier, exportedName: asMatch[1] };
|
|
330
|
+
if (part === localName) return { specifier, exportedName: localName };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
function resolveImportPath(importSpec, fromFile) {
|
|
336
|
+
if (!importSpec.startsWith(".")) return null;
|
|
337
|
+
const dir = dirname(fromFile);
|
|
338
|
+
let base = importSpec;
|
|
339
|
+
if (base.endsWith(".js")) base = base.slice(0, -3);
|
|
340
|
+
else if (base.endsWith(".mjs")) base = base.slice(0, -4);
|
|
341
|
+
const candidates = [
|
|
342
|
+
resolve(dir, base + ".ts"),
|
|
343
|
+
resolve(dir, base + ".tsx"),
|
|
344
|
+
resolve(dir, base + "/index.ts"),
|
|
345
|
+
resolve(dir, base + "/index.tsx")
|
|
346
|
+
];
|
|
347
|
+
for (const candidate of candidates) {
|
|
348
|
+
if (existsSync(candidate)) return candidate;
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
function escapeRegExp(s) {
|
|
353
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
354
|
+
}
|
|
355
|
+
function extractUrlsBlockForVariable(code, varName) {
|
|
356
|
+
const pattern = new RegExp(
|
|
357
|
+
`(?:export\\s+)?(?:const|let|var)\\s+${escapeRegExp(varName)}\\s*=\\s*urls\\s*\\(`
|
|
358
|
+
);
|
|
359
|
+
const match = pattern.exec(code);
|
|
360
|
+
if (!match) return null;
|
|
361
|
+
const openParen = match.index + match[0].length - 1;
|
|
362
|
+
let depth = 1;
|
|
363
|
+
let pos = openParen + 1;
|
|
364
|
+
while (pos < code.length && depth > 0) {
|
|
365
|
+
const ch = code[pos];
|
|
366
|
+
if (ch === '"' || ch === "`" || ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1]))) {
|
|
367
|
+
pos = skipStringLiteral(code, pos);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
|
|
371
|
+
pos += 2;
|
|
372
|
+
while (pos < code.length && code[pos] !== "\n") pos++;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
|
|
376
|
+
pos += 2;
|
|
377
|
+
while (pos < code.length - 1 && !(code[pos] === "*" && code[pos + 1] === "/"))
|
|
378
|
+
pos++;
|
|
379
|
+
pos += 2;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
383
|
+
else if (ch === ")" || ch === "}" || ch === "]") depth--;
|
|
384
|
+
pos++;
|
|
385
|
+
}
|
|
386
|
+
return code.slice(openParen, pos);
|
|
387
|
+
}
|
|
388
|
+
function buildCombinedRouteMap(filePath, variableName, visited) {
|
|
389
|
+
visited = visited ?? /* @__PURE__ */ new Set();
|
|
390
|
+
const realPath = resolve(filePath);
|
|
391
|
+
const key = variableName ? `${realPath}:${variableName}` : realPath;
|
|
392
|
+
if (visited.has(key)) return {};
|
|
393
|
+
visited.add(key);
|
|
394
|
+
let source;
|
|
395
|
+
try {
|
|
396
|
+
source = readFileSync(realPath, "utf-8");
|
|
397
|
+
} catch {
|
|
398
|
+
return {};
|
|
399
|
+
}
|
|
400
|
+
let block;
|
|
401
|
+
if (variableName) {
|
|
402
|
+
const extracted = extractUrlsBlockForVariable(source, variableName);
|
|
403
|
+
if (!extracted) return {};
|
|
404
|
+
block = extracted;
|
|
405
|
+
} else {
|
|
406
|
+
block = source;
|
|
407
|
+
}
|
|
408
|
+
return buildRouteMapFromBlock(block, source, realPath, visited);
|
|
409
|
+
}
|
|
410
|
+
function buildRouteMapFromBlock(block, fullSource, filePath, visited) {
|
|
411
|
+
const routeMap = {};
|
|
412
|
+
const localRoutes = extractRoutesFromSource(block);
|
|
413
|
+
for (const { name, pattern } of localRoutes) {
|
|
414
|
+
routeMap[name] = pattern;
|
|
415
|
+
}
|
|
416
|
+
const includes = extractIncludesFromSource(block);
|
|
417
|
+
for (const { pathPrefix, variableName, namePrefix } of includes) {
|
|
418
|
+
let childRoutes;
|
|
419
|
+
const imported = resolveImportedVariable(fullSource, variableName);
|
|
420
|
+
if (imported) {
|
|
421
|
+
const targetFile = resolveImportPath(imported.specifier, filePath);
|
|
422
|
+
if (!targetFile) continue;
|
|
423
|
+
childRoutes = buildCombinedRouteMap(
|
|
424
|
+
targetFile,
|
|
425
|
+
imported.exportedName,
|
|
426
|
+
visited
|
|
427
|
+
);
|
|
428
|
+
} else {
|
|
429
|
+
childRoutes = buildCombinedRouteMap(filePath, variableName, visited);
|
|
430
|
+
}
|
|
431
|
+
for (const [name, pattern] of Object.entries(childRoutes)) {
|
|
432
|
+
const prefixedName = namePrefix ? `${namePrefix}.${name}` : name;
|
|
433
|
+
const prefixedPattern = pattern === "/" ? pathPrefix || "/" : pathPrefix + pattern;
|
|
434
|
+
routeMap[prefixedName] = prefixedPattern;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return routeMap;
|
|
438
|
+
}
|
|
439
|
+
function extractUrlsVariableFromRouter(code) {
|
|
440
|
+
const routesCallMatch = code.match(/\.routes\s*\(\s*([a-zA-Z_$][\w$]*)\s*\)/);
|
|
441
|
+
if (routesCallMatch) return routesCallMatch[1];
|
|
442
|
+
const urlsOptionMatch = code.match(/urls\s*:\s*([a-zA-Z_$][\w$]*)/);
|
|
443
|
+
if (urlsOptionMatch) return urlsOptionMatch[1];
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
function findRouterFiles(root, filter) {
|
|
447
|
+
const files = findTsFiles(root, filter);
|
|
448
|
+
const result = [];
|
|
449
|
+
for (const filePath of files) {
|
|
450
|
+
if (filePath.includes(".gen.")) continue;
|
|
451
|
+
try {
|
|
452
|
+
const source = readFileSync(filePath, "utf-8");
|
|
453
|
+
if (/\bcreateRouter\s*[<(]/.test(source)) {
|
|
454
|
+
result.push(filePath);
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
function writeCombinedRouteTypes(root, knownRouterFiles) {
|
|
463
|
+
try {
|
|
464
|
+
const oldCombinedPath = join(root, "src", "named-routes.gen.ts");
|
|
465
|
+
if (existsSync(oldCombinedPath)) {
|
|
466
|
+
unlinkSync(oldCombinedPath);
|
|
467
|
+
console.log(`[rsc-router] Removed stale combined route types: ${oldCombinedPath}`);
|
|
468
|
+
}
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
const routerFilePaths = knownRouterFiles ?? findRouterFiles(root);
|
|
472
|
+
if (routerFilePaths.length === 0) return;
|
|
473
|
+
for (const routerFilePath of routerFilePaths) {
|
|
474
|
+
let routerSource;
|
|
475
|
+
try {
|
|
476
|
+
routerSource = readFileSync(routerFilePath, "utf-8");
|
|
477
|
+
} catch {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const urlsVarName = extractUrlsVariableFromRouter(routerSource);
|
|
481
|
+
if (!urlsVarName) continue;
|
|
482
|
+
let routeMap;
|
|
483
|
+
const imported = resolveImportedVariable(routerSource, urlsVarName);
|
|
484
|
+
if (imported) {
|
|
485
|
+
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
486
|
+
if (!targetFile) continue;
|
|
487
|
+
routeMap = buildCombinedRouteMap(targetFile, imported.exportedName);
|
|
488
|
+
} else {
|
|
489
|
+
routeMap = buildCombinedRouteMap(routerFilePath, urlsVarName);
|
|
490
|
+
}
|
|
491
|
+
if (Object.keys(routeMap).length === 0) continue;
|
|
492
|
+
const routerBasename = pathBasename(routerFilePath).replace(/\.(tsx?|jsx?)$/, "");
|
|
493
|
+
const outPath = join(dirname(routerFilePath), `${routerBasename}.named-routes.gen.ts`);
|
|
494
|
+
const source = generateRouteTypesSource(routeMap);
|
|
495
|
+
const existing = existsSync(outPath) ? readFileSync(outPath, "utf-8") : null;
|
|
496
|
+
if (existing !== source) {
|
|
497
|
+
writeFileSync(outPath, source);
|
|
498
|
+
console.log(`[rsc-router] Generated route types (${Object.keys(routeMap).length} routes) -> ${outPath}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/vite/expose-action-id.ts
|
|
504
|
+
import MagicString from "magic-string";
|
|
505
|
+
import path from "node:path";
|
|
506
|
+
import fs from "node:fs";
|
|
507
|
+
function getRscPluginApi(config) {
|
|
508
|
+
let plugin = config.plugins.find((p) => p.name === "rsc:minimal");
|
|
509
|
+
if (!plugin) {
|
|
510
|
+
plugin = config.plugins.find(
|
|
511
|
+
(p) => p.api?.manager?.serverReferenceMetaMap !== void 0
|
|
512
|
+
);
|
|
513
|
+
if (plugin) {
|
|
514
|
+
console.warn(
|
|
515
|
+
`[rsc-router:expose-action-id] RSC plugin found by API structure (name: "${plugin.name}"). Consider updating the name lookup if the plugin was renamed.`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return plugin?.api;
|
|
520
|
+
}
|
|
521
|
+
function normalizePath(p) {
|
|
522
|
+
return p.split(path.sep).join("/");
|
|
523
|
+
}
|
|
524
|
+
function isUseServerModule(filePath) {
|
|
525
|
+
try {
|
|
526
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
527
|
+
const trimmed = content.replace(/^\s*\/\/[^\n]*\n/gm, "").replace(/^\s*\/\*[\s\S]*?\*\/\s*/gm, "").trimStart();
|
|
528
|
+
return trimmed.startsWith('"use server"') || trimmed.startsWith("'use server'");
|
|
529
|
+
} catch {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function transformServerReferences(code, sourceId, hashToFileMap) {
|
|
534
|
+
if (!code.includes("createServerReference(")) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
const pattern = /((?:\$\$\w+\.)?createServerReference)\(("[^"]+#[^"]+")([^)]*)\)/g;
|
|
538
|
+
const s = new MagicString(code);
|
|
539
|
+
let hasChanges = false;
|
|
540
|
+
let match;
|
|
541
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
542
|
+
hasChanges = true;
|
|
543
|
+
const [fullMatch, fnCall, idArg, rest] = match;
|
|
544
|
+
const start = match.index;
|
|
545
|
+
const end = start + fullMatch.length;
|
|
546
|
+
let finalIdArg = idArg;
|
|
547
|
+
if (hashToFileMap) {
|
|
548
|
+
const idValue = idArg.slice(1, -1);
|
|
549
|
+
const hashMatch = idValue.match(/^([^#]+)#(.+)$/);
|
|
550
|
+
if (hashMatch) {
|
|
551
|
+
const [, hash, actionName] = hashMatch;
|
|
552
|
+
const filePath = hashToFileMap.get(hash);
|
|
553
|
+
if (filePath) {
|
|
554
|
+
finalIdArg = `"${filePath}#${actionName}"`;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const replacement = `(function(fn) { fn.$$id = ${finalIdArg}; return fn; })(${fnCall}(${idArg}${rest}))`;
|
|
559
|
+
s.overwrite(start, end, replacement);
|
|
560
|
+
}
|
|
561
|
+
if (!hasChanges) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
code: s.toString(),
|
|
566
|
+
map: s.generateMap({ source: sourceId, includeContent: true })
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function transformRegisterServerReference(code, sourceId, hashToFileMap) {
|
|
570
|
+
if (!hashToFileMap || !code.includes("registerServerReference(")) {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
const pattern = /registerServerReference\(([^,]+),\s*"([^"]+)",\s*"([^"]+)"\)/g;
|
|
574
|
+
const s = new MagicString(code);
|
|
575
|
+
let hasChanges = false;
|
|
576
|
+
let match;
|
|
577
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
578
|
+
const [fullMatch, fnArg, hash, exportName] = match;
|
|
579
|
+
const start = match.index;
|
|
580
|
+
const end = start + fullMatch.length;
|
|
581
|
+
const filePath = hashToFileMap.get(hash);
|
|
582
|
+
if (filePath) {
|
|
583
|
+
hasChanges = true;
|
|
584
|
+
const filePathId = `${filePath}#${exportName}`;
|
|
585
|
+
const replacement = `(function(fn) { fn.$id = "${filePathId}"; return fn; })(registerServerReference(${fnArg}, "${hash}", "${exportName}"))`;
|
|
586
|
+
s.overwrite(start, end, replacement);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (!hasChanges) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
code: s.toString(),
|
|
594
|
+
map: s.generateMap({ source: sourceId, includeContent: true })
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function exposeActionId() {
|
|
598
|
+
let config;
|
|
599
|
+
let isBuild = false;
|
|
600
|
+
let hashToFileMap;
|
|
601
|
+
let rscPluginApi;
|
|
602
|
+
return {
|
|
603
|
+
name: "@rangojs/router:expose-action-id",
|
|
604
|
+
// Run after all other plugins (including RSC plugin's transforms)
|
|
605
|
+
enforce: "post",
|
|
606
|
+
configResolved(resolvedConfig) {
|
|
607
|
+
config = resolvedConfig;
|
|
608
|
+
isBuild = config.command === "build";
|
|
609
|
+
rscPluginApi = getRscPluginApi(config);
|
|
610
|
+
},
|
|
611
|
+
buildStart() {
|
|
612
|
+
if (!rscPluginApi) {
|
|
613
|
+
rscPluginApi = getRscPluginApi(config);
|
|
614
|
+
}
|
|
615
|
+
if (!rscPluginApi) {
|
|
616
|
+
throw new Error(
|
|
617
|
+
"[rsc-router] Could not find @vitejs/plugin-rsc. @rangojs/router requires the Vite RSC plugin.\nThe RSC plugin should be included automatically. If you disabled it with\nrango({ rsc: false }), add rsc() before rango() in your config."
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (!isBuild) return;
|
|
621
|
+
hashToFileMap = /* @__PURE__ */ new Map();
|
|
622
|
+
const { serverReferenceMetaMap } = rscPluginApi.manager;
|
|
623
|
+
for (const [absolutePath, meta] of Object.entries(
|
|
624
|
+
serverReferenceMetaMap
|
|
625
|
+
)) {
|
|
626
|
+
if (!isUseServerModule(absolutePath)) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const relativePath = normalizePath(
|
|
630
|
+
path.relative(config.root, absolutePath)
|
|
631
|
+
);
|
|
632
|
+
hashToFileMap.set(meta.referenceKey, relativePath);
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
// Dev mode only: transform hook runs after RSC plugin creates server references
|
|
636
|
+
// In dev mode, IDs already contain file paths, not hashes
|
|
637
|
+
transform(code, id) {
|
|
638
|
+
if (isBuild) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (!code.includes("createServerReference(")) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (id.includes("/node_modules/")) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
return transformServerReferences(code, id);
|
|
648
|
+
},
|
|
649
|
+
// Build mode: renderChunk runs after all transforms and bundling complete
|
|
650
|
+
renderChunk(code, chunk) {
|
|
651
|
+
const isRscEnv = this.environment?.name === "rsc";
|
|
652
|
+
const effectiveMap = isRscEnv ? hashToFileMap : void 0;
|
|
653
|
+
const result = transformServerReferences(
|
|
654
|
+
code,
|
|
655
|
+
chunk.fileName,
|
|
656
|
+
effectiveMap
|
|
657
|
+
);
|
|
658
|
+
if (isRscEnv && hashToFileMap) {
|
|
659
|
+
const codeToTransform = result ? result.code : code;
|
|
660
|
+
const registerResult = transformRegisterServerReference(
|
|
661
|
+
codeToTransform,
|
|
662
|
+
chunk.fileName,
|
|
663
|
+
hashToFileMap
|
|
664
|
+
);
|
|
665
|
+
if (registerResult) {
|
|
666
|
+
return { code: registerResult.code, map: registerResult.map };
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (result) {
|
|
670
|
+
return { code: result.code, map: result.map };
|
|
671
|
+
}
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/vite/expose-loader-id.ts
|
|
678
|
+
import MagicString2 from "magic-string";
|
|
679
|
+
import path2 from "node:path";
|
|
680
|
+
import crypto from "node:crypto";
|
|
681
|
+
function normalizePath2(p) {
|
|
682
|
+
return p.split(path2.sep).join("/");
|
|
683
|
+
}
|
|
684
|
+
function hashLoaderId(filePath, exportName) {
|
|
685
|
+
const input = `${filePath}#${exportName}`;
|
|
686
|
+
const hash = crypto.createHash("sha256").update(input).digest("hex");
|
|
687
|
+
return `${hash.slice(0, 8)}#${exportName}`;
|
|
688
|
+
}
|
|
689
|
+
function hasCreateLoaderImport(code) {
|
|
690
|
+
const pattern = /import\s*\{[^}]*\bcreateLoader\b[^}]*\}\s*from\s*["']@rangojs\/router(?:\/server)?["']/;
|
|
691
|
+
return pattern.test(code);
|
|
692
|
+
}
|
|
693
|
+
function countCreateLoaderArgs(code, startPos, endPos) {
|
|
694
|
+
let depth = 0;
|
|
695
|
+
let argCount = 0;
|
|
696
|
+
let hasContent = false;
|
|
697
|
+
for (let i = startPos; i < endPos; i++) {
|
|
698
|
+
const char = code[i];
|
|
699
|
+
if (char === "(" || char === "[" || char === "{") {
|
|
700
|
+
depth++;
|
|
701
|
+
hasContent = true;
|
|
702
|
+
} else if (char === ")" || char === "]" || char === "}") {
|
|
703
|
+
depth--;
|
|
704
|
+
} else if (char === "," && depth === 0) {
|
|
705
|
+
argCount++;
|
|
706
|
+
} else if (!/\s/.test(char)) {
|
|
707
|
+
hasContent = true;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return hasContent ? argCount + 1 : 0;
|
|
711
|
+
}
|
|
712
|
+
function generateClientLoaderStubs(code, filePath, isBuild) {
|
|
713
|
+
const loaderPattern = /export\s+const\s+(\w+)\s*=\s*createLoader\s*\(/g;
|
|
714
|
+
const loaders = [];
|
|
715
|
+
let match;
|
|
716
|
+
while ((match = loaderPattern.exec(code)) !== null) {
|
|
717
|
+
loaders.push(match[1]);
|
|
718
|
+
}
|
|
719
|
+
if (loaders.length === 0) {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
const allExports = /export\s+(const|let|var|function|class|default)\s+(\w+)/g;
|
|
723
|
+
let exportMatch;
|
|
724
|
+
const nonLoaderExports = [];
|
|
725
|
+
while ((exportMatch = allExports.exec(code)) !== null) {
|
|
726
|
+
const name = exportMatch[2];
|
|
727
|
+
if (!loaders.includes(name)) {
|
|
728
|
+
nonLoaderExports.push(name);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (nonLoaderExports.length > 0) {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
const stubs = loaders.map((name) => {
|
|
735
|
+
const loaderId = isBuild ? hashLoaderId(filePath, name) : `${filePath}#${name}`;
|
|
736
|
+
return `export const ${name} = { __brand: "loader", $$id: "${loaderId}" };`;
|
|
737
|
+
});
|
|
738
|
+
return {
|
|
739
|
+
code: stubs.join("\n") + "\n"
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
function transformLoaderExports(code, filePath, sourceId, isBuild = false) {
|
|
743
|
+
if (!code.includes("createLoader")) {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
if (!hasCreateLoaderImport(code)) {
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
const pattern = /export\s+const\s+(\w+)\s*=\s*createLoader\s*\(/g;
|
|
750
|
+
const s = new MagicString2(code);
|
|
751
|
+
let hasChanges = false;
|
|
752
|
+
let match;
|
|
753
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
754
|
+
const exportName = match[1];
|
|
755
|
+
const matchEnd = match.index + match[0].length;
|
|
756
|
+
let parenDepth = 1;
|
|
757
|
+
let i = matchEnd;
|
|
758
|
+
while (i < code.length && parenDepth > 0) {
|
|
759
|
+
if (code[i] === "(") parenDepth++;
|
|
760
|
+
if (code[i] === ")") parenDepth--;
|
|
761
|
+
i++;
|
|
762
|
+
}
|
|
763
|
+
const closeParenPos = i - 1;
|
|
764
|
+
const argCount = countCreateLoaderArgs(code, matchEnd, closeParenPos);
|
|
765
|
+
let statementEnd = i;
|
|
766
|
+
while (statementEnd < code.length && /\s/.test(code[statementEnd])) {
|
|
767
|
+
statementEnd++;
|
|
768
|
+
}
|
|
769
|
+
if (code[statementEnd] === ";") {
|
|
770
|
+
statementEnd++;
|
|
771
|
+
}
|
|
772
|
+
const loaderId = isBuild ? hashLoaderId(filePath, exportName) : `${filePath}#${exportName}`;
|
|
773
|
+
const paramInjection = argCount === 1 ? `, undefined, "${loaderId}"` : `, "${loaderId}"`;
|
|
774
|
+
s.appendLeft(closeParenPos, paramInjection);
|
|
775
|
+
const propInjection = `
|
|
776
|
+
${exportName}.$$id = "${loaderId}";`;
|
|
777
|
+
s.appendRight(statementEnd, propInjection);
|
|
778
|
+
hasChanges = true;
|
|
779
|
+
}
|
|
780
|
+
if (!hasChanges) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
return {
|
|
784
|
+
code: s.toString(),
|
|
785
|
+
map: s.generateMap({ source: sourceId, includeContent: true })
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
var VIRTUAL_LOADER_MANIFEST = "virtual:rsc-router/loader-manifest";
|
|
789
|
+
var RESOLVED_VIRTUAL_LOADER_MANIFEST = "\0" + VIRTUAL_LOADER_MANIFEST;
|
|
790
|
+
function exposeLoaderId() {
|
|
791
|
+
let config;
|
|
792
|
+
let isBuild = false;
|
|
793
|
+
const loaderRegistry = /* @__PURE__ */ new Map();
|
|
794
|
+
const pendingLoaderScans = /* @__PURE__ */ new Map();
|
|
795
|
+
return {
|
|
796
|
+
name: "@rangojs/router:expose-loader-id",
|
|
797
|
+
enforce: "post",
|
|
798
|
+
configResolved(resolvedConfig) {
|
|
799
|
+
config = resolvedConfig;
|
|
800
|
+
isBuild = config.command === "build";
|
|
801
|
+
},
|
|
802
|
+
async buildStart() {
|
|
803
|
+
if (!isBuild) return;
|
|
804
|
+
const fs2 = await import("node:fs/promises");
|
|
805
|
+
async function scanDir(dir) {
|
|
806
|
+
const results = [];
|
|
807
|
+
try {
|
|
808
|
+
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
809
|
+
for (const entry of entries) {
|
|
810
|
+
const fullPath = path2.join(dir, entry.name);
|
|
811
|
+
if (entry.isDirectory()) {
|
|
812
|
+
if (entry.name !== "node_modules") {
|
|
813
|
+
results.push(...await scanDir(fullPath));
|
|
814
|
+
}
|
|
815
|
+
} else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|
|
816
|
+
results.push(fullPath);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
return results;
|
|
822
|
+
}
|
|
823
|
+
try {
|
|
824
|
+
const srcDir = path2.join(config.root, "src");
|
|
825
|
+
const files = await scanDir(srcDir);
|
|
826
|
+
for (const filePath of files) {
|
|
827
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
828
|
+
if (!content.includes("createLoader")) continue;
|
|
829
|
+
if (!hasCreateLoaderImport(content)) continue;
|
|
830
|
+
const pattern = /export\s+const\s+(\w+)\s*=\s*createLoader\s*\(/g;
|
|
831
|
+
const relativePath = normalizePath2(
|
|
832
|
+
path2.relative(config.root, filePath)
|
|
833
|
+
);
|
|
834
|
+
let match;
|
|
835
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
836
|
+
const exportName = match[1];
|
|
837
|
+
const hashedId = hashLoaderId(relativePath, exportName);
|
|
838
|
+
loaderRegistry.set(hashedId, {
|
|
839
|
+
filePath: relativePath,
|
|
840
|
+
exportName
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
} catch (error) {
|
|
845
|
+
console.warn("[exposeLoaderId] Pre-scan failed:", error);
|
|
846
|
+
}
|
|
847
|
+
},
|
|
848
|
+
resolveId(id) {
|
|
849
|
+
if (id === VIRTUAL_LOADER_MANIFEST) {
|
|
850
|
+
return RESOLVED_VIRTUAL_LOADER_MANIFEST;
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
load(id) {
|
|
854
|
+
if (id === RESOLVED_VIRTUAL_LOADER_MANIFEST) {
|
|
855
|
+
if (!isBuild) {
|
|
856
|
+
return `import { setLoaderImports } from "@rangojs/router/server";
|
|
857
|
+
|
|
858
|
+
// Dev mode: empty map, loaders are resolved dynamically via path parsing
|
|
859
|
+
setLoaderImports({});
|
|
860
|
+
`;
|
|
861
|
+
}
|
|
862
|
+
const lazyImports = [];
|
|
863
|
+
for (const [hashedId, { filePath, exportName }] of loaderRegistry) {
|
|
864
|
+
lazyImports.push(
|
|
865
|
+
` "${hashedId}": () => import("/${filePath}").then(m => m.${exportName})`
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
if (lazyImports.length === 0) {
|
|
869
|
+
return `import { setLoaderImports } from "@rangojs/router/server";
|
|
870
|
+
|
|
871
|
+
// No fetchable loaders discovered during build
|
|
872
|
+
setLoaderImports({});
|
|
873
|
+
`;
|
|
874
|
+
}
|
|
875
|
+
const code = `import { setLoaderImports } from "@rangojs/router/server";
|
|
876
|
+
|
|
877
|
+
// Lazy import map - loaders are loaded on-demand when first requested
|
|
878
|
+
setLoaderImports({
|
|
879
|
+
${lazyImports.join(",\n")}
|
|
880
|
+
});
|
|
881
|
+
`;
|
|
882
|
+
return code;
|
|
883
|
+
}
|
|
884
|
+
},
|
|
885
|
+
transform(code, id) {
|
|
886
|
+
if (id.includes("/node_modules/")) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (!code.includes("createLoader")) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (!hasCreateLoaderImport(code)) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const envName = this.environment?.name;
|
|
896
|
+
const isRscEnv = envName === "rsc";
|
|
897
|
+
const relativePath = normalizePath2(path2.relative(config.root, id));
|
|
898
|
+
if (isRscEnv) {
|
|
899
|
+
const pattern = /export\s+const\s+(\w+)\s*=\s*createLoader\s*\(/g;
|
|
900
|
+
let match;
|
|
901
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
902
|
+
const exportName = match[1];
|
|
903
|
+
const hashedId = hashLoaderId(relativePath, exportName);
|
|
904
|
+
loaderRegistry.set(hashedId, { filePath: relativePath, exportName });
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (!isRscEnv) {
|
|
908
|
+
const stubResult = generateClientLoaderStubs(code, relativePath, isBuild);
|
|
909
|
+
if (stubResult) {
|
|
910
|
+
return stubResult;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return transformLoaderExports(code, relativePath, id, isBuild);
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/vite/expose-handle-id.ts
|
|
919
|
+
import MagicString3 from "magic-string";
|
|
920
|
+
import path3 from "node:path";
|
|
921
|
+
import crypto2 from "node:crypto";
|
|
922
|
+
function normalizePath3(p) {
|
|
923
|
+
return p.split(path3.sep).join("/");
|
|
924
|
+
}
|
|
925
|
+
function hashHandleId(filePath, exportName) {
|
|
926
|
+
const input = `${filePath}#${exportName}`;
|
|
927
|
+
const hash = crypto2.createHash("sha256").update(input).digest("hex");
|
|
928
|
+
return `${hash.slice(0, 8)}#${exportName}`;
|
|
929
|
+
}
|
|
930
|
+
function hasCreateHandleImport(code) {
|
|
931
|
+
const pattern = /import\s*\{[^}]*\bcreateHandle\b[^}]*\}\s*from\s*["']@rangojs\/router(?:\/[^"']+)?["']/;
|
|
932
|
+
return pattern.test(code);
|
|
933
|
+
}
|
|
934
|
+
function analyzeCreateHandleArgs(code, startPos, endPos) {
|
|
935
|
+
const content = code.slice(startPos, endPos).trim();
|
|
936
|
+
if (!content) {
|
|
937
|
+
return { hasArgs: false, firstArgIsString: false, firstArgIsFunction: false };
|
|
938
|
+
}
|
|
939
|
+
const firstArgIsString = /^["']/.test(content);
|
|
940
|
+
const firstArgIsFunction = content.startsWith("(") || content.startsWith("function") || // Check for identifier that could be a collect function reference
|
|
941
|
+
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*(?:,|$)/.test(content);
|
|
942
|
+
return { hasArgs: true, firstArgIsString, firstArgIsFunction };
|
|
943
|
+
}
|
|
944
|
+
function transformHandleExports(code, filePath, sourceId, isBuild = false) {
|
|
945
|
+
if (!code.includes("createHandle")) {
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
if (!hasCreateHandleImport(code)) {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
const pattern = /export\s+const\s+(\w+)\s*=\s*createHandle\s*(?:<[^>]*>)?\s*\(/g;
|
|
952
|
+
const s = new MagicString3(code);
|
|
953
|
+
let hasChanges = false;
|
|
954
|
+
let match;
|
|
955
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
956
|
+
const exportName = match[1];
|
|
957
|
+
const matchEnd = match.index + match[0].length;
|
|
958
|
+
let parenDepth = 1;
|
|
959
|
+
let i = matchEnd;
|
|
960
|
+
while (i < code.length && parenDepth > 0) {
|
|
961
|
+
if (code[i] === "(") parenDepth++;
|
|
962
|
+
if (code[i] === ")") parenDepth--;
|
|
963
|
+
i++;
|
|
964
|
+
}
|
|
965
|
+
const closeParenPos = i - 1;
|
|
966
|
+
const args = analyzeCreateHandleArgs(code, matchEnd, closeParenPos);
|
|
967
|
+
let statementEnd = i;
|
|
968
|
+
while (statementEnd < code.length && /\s/.test(code[statementEnd])) {
|
|
969
|
+
statementEnd++;
|
|
970
|
+
}
|
|
971
|
+
if (code[statementEnd] === ";") {
|
|
972
|
+
statementEnd++;
|
|
973
|
+
}
|
|
974
|
+
const handleId = isBuild ? hashHandleId(filePath, exportName) : `${filePath}#${exportName}`;
|
|
975
|
+
let paramInjection;
|
|
976
|
+
if (!args.hasArgs) {
|
|
977
|
+
paramInjection = `undefined, "${handleId}"`;
|
|
978
|
+
} else {
|
|
979
|
+
paramInjection = `, "${handleId}"`;
|
|
980
|
+
}
|
|
981
|
+
s.appendLeft(closeParenPos, paramInjection);
|
|
982
|
+
const propInjection = `
|
|
983
|
+
${exportName}.$$id = "${handleId}";`;
|
|
984
|
+
s.appendRight(statementEnd, propInjection);
|
|
985
|
+
hasChanges = true;
|
|
986
|
+
}
|
|
987
|
+
if (!hasChanges) {
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
code: s.toString(),
|
|
992
|
+
map: s.generateMap({ source: sourceId, includeContent: true })
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
function exposeHandleId() {
|
|
996
|
+
let config;
|
|
997
|
+
let isBuild = false;
|
|
998
|
+
return {
|
|
999
|
+
name: "@rangojs/router:expose-handle-id",
|
|
1000
|
+
enforce: "post",
|
|
1001
|
+
configResolved(resolvedConfig) {
|
|
1002
|
+
config = resolvedConfig;
|
|
1003
|
+
isBuild = config.command === "build";
|
|
1004
|
+
},
|
|
1005
|
+
transform(code, id) {
|
|
1006
|
+
if (id.includes("/node_modules/")) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (!code.includes("createHandle")) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
if (!hasCreateHandleImport(code)) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const relativePath = normalizePath3(path3.relative(config.root, id));
|
|
1016
|
+
return transformHandleExports(code, relativePath, id, isBuild);
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// src/vite/expose-location-state-id.ts
|
|
1022
|
+
import MagicString4 from "magic-string";
|
|
1023
|
+
import path4 from "node:path";
|
|
1024
|
+
import crypto3 from "node:crypto";
|
|
1025
|
+
function normalizePath4(p) {
|
|
1026
|
+
return p.split(path4.sep).join("/");
|
|
1027
|
+
}
|
|
1028
|
+
function hashLocationStateKey(filePath, exportName) {
|
|
1029
|
+
const input = `${filePath}#${exportName}`;
|
|
1030
|
+
const hash = crypto3.createHash("sha256").update(input).digest("hex");
|
|
1031
|
+
return `${hash.slice(0, 8)}#${exportName}`;
|
|
1032
|
+
}
|
|
1033
|
+
function hasCreateLocationStateImport(code) {
|
|
1034
|
+
const pattern = /import\s*\{[^}]*\bcreateLocationState\b[^}]*\}\s*from\s*["']@rangojs\/router(?:\/[^"']+)?["']/;
|
|
1035
|
+
return pattern.test(code);
|
|
1036
|
+
}
|
|
1037
|
+
function transformLocationStateExports(code, filePath, sourceId, isBuild = false) {
|
|
1038
|
+
if (!code.includes("createLocationState")) {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
if (!hasCreateLocationStateImport(code)) {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
const pattern = /export\s+const\s+(\w+)\s*=\s*createLocationState\s*(?:<[^>]*>)?\s*\(/g;
|
|
1045
|
+
const s = new MagicString4(code);
|
|
1046
|
+
let hasChanges = false;
|
|
1047
|
+
let match;
|
|
1048
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
1049
|
+
const exportName = match[1];
|
|
1050
|
+
const matchEnd = match.index + match[0].length;
|
|
1051
|
+
let parenDepth = 1;
|
|
1052
|
+
let i = matchEnd;
|
|
1053
|
+
while (i < code.length && parenDepth > 0) {
|
|
1054
|
+
if (code[i] === "(") parenDepth++;
|
|
1055
|
+
if (code[i] === ")") parenDepth--;
|
|
1056
|
+
i++;
|
|
1057
|
+
}
|
|
1058
|
+
const closeParenPos = i - 1;
|
|
1059
|
+
const content = code.slice(matchEnd, closeParenPos).trim();
|
|
1060
|
+
const hasArgs = content.length > 0;
|
|
1061
|
+
let statementEnd = i;
|
|
1062
|
+
while (statementEnd < code.length && /\s/.test(code[statementEnd])) {
|
|
1063
|
+
statementEnd++;
|
|
1064
|
+
}
|
|
1065
|
+
if (code[statementEnd] === ";") {
|
|
1066
|
+
statementEnd++;
|
|
1067
|
+
}
|
|
1068
|
+
const stateKey = isBuild ? hashLocationStateKey(filePath, exportName) : `${filePath}#${exportName}`;
|
|
1069
|
+
if (!hasArgs) {
|
|
1070
|
+
s.appendLeft(closeParenPos, `"${stateKey}"`);
|
|
1071
|
+
} else {
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
const propInjection = `
|
|
1075
|
+
${exportName}.__rsc_ls_key = "__rsc_ls_${stateKey}";`;
|
|
1076
|
+
s.appendRight(statementEnd, propInjection);
|
|
1077
|
+
hasChanges = true;
|
|
1078
|
+
}
|
|
1079
|
+
if (!hasChanges) {
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
return {
|
|
1083
|
+
code: s.toString(),
|
|
1084
|
+
map: s.generateMap({ source: sourceId, includeContent: true })
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
function exposeLocationStateId() {
|
|
1088
|
+
let config;
|
|
1089
|
+
let isBuild = false;
|
|
1090
|
+
return {
|
|
1091
|
+
name: "@rangojs/router:expose-location-state-id",
|
|
1092
|
+
enforce: "post",
|
|
1093
|
+
configResolved(resolvedConfig) {
|
|
1094
|
+
config = resolvedConfig;
|
|
1095
|
+
isBuild = config.command === "build";
|
|
1096
|
+
},
|
|
1097
|
+
transform(code, id) {
|
|
1098
|
+
if (id.includes("/node_modules/")) {
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (!code.includes("createLocationState")) {
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (!hasCreateLocationStateImport(code)) {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const relativePath = normalizePath4(path4.relative(config.root, id));
|
|
1108
|
+
return transformLocationStateExports(code, relativePath, id, isBuild);
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// src/vite/expose-prerender-handler-id.ts
|
|
1114
|
+
import MagicString5 from "magic-string";
|
|
1115
|
+
import path5 from "node:path";
|
|
1116
|
+
import crypto4 from "node:crypto";
|
|
1117
|
+
function normalizePath5(p) {
|
|
1118
|
+
return p.split(path5.sep).join("/");
|
|
1119
|
+
}
|
|
1120
|
+
function hashPrerenderHandlerId(filePath, exportName) {
|
|
1121
|
+
const input = `${filePath}#${exportName}`;
|
|
1122
|
+
const hash = crypto4.createHash("sha256").update(input).digest("hex");
|
|
1123
|
+
return `${hash.slice(0, 8)}#${exportName}`;
|
|
1124
|
+
}
|
|
1125
|
+
function hasCreatePrerenderHandlerImport(code) {
|
|
1126
|
+
const pattern = /import\s*\{[^}]*\bcreatePrerenderHandler\b[^}]*\}\s*from\s*["']@rangojs\/router(?:\/[^"']+)?["']/;
|
|
1127
|
+
return pattern.test(code);
|
|
1128
|
+
}
|
|
1129
|
+
function skipStringOrComment(code, pos) {
|
|
1130
|
+
const ch = code[pos];
|
|
1131
|
+
if (ch === '"' || ch === "'") {
|
|
1132
|
+
for (let j = pos + 1; j < code.length; j++) {
|
|
1133
|
+
if (code[j] === "\\") {
|
|
1134
|
+
j++;
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (code[j] === ch) return j + 1;
|
|
1138
|
+
}
|
|
1139
|
+
return code.length;
|
|
1140
|
+
}
|
|
1141
|
+
if (ch === "`") {
|
|
1142
|
+
let j = pos + 1;
|
|
1143
|
+
while (j < code.length) {
|
|
1144
|
+
if (code[j] === "\\") {
|
|
1145
|
+
j += 2;
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
if (code[j] === "`") return j + 1;
|
|
1149
|
+
if (code[j] === "$" && j + 1 < code.length && code[j + 1] === "{") {
|
|
1150
|
+
j += 2;
|
|
1151
|
+
let braceDepth = 1;
|
|
1152
|
+
while (j < code.length && braceDepth > 0) {
|
|
1153
|
+
const inner = skipStringOrComment(code, j);
|
|
1154
|
+
if (inner > j) {
|
|
1155
|
+
j = inner;
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
if (code[j] === "{") braceDepth++;
|
|
1159
|
+
else if (code[j] === "}") braceDepth--;
|
|
1160
|
+
if (braceDepth > 0) j++;
|
|
1161
|
+
}
|
|
1162
|
+
if (braceDepth === 0) j++;
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
j++;
|
|
1166
|
+
}
|
|
1167
|
+
return j;
|
|
1168
|
+
}
|
|
1169
|
+
if (ch === "/" && pos + 1 < code.length) {
|
|
1170
|
+
if (code[pos + 1] === "/") {
|
|
1171
|
+
const eol = code.indexOf("\n", pos + 2);
|
|
1172
|
+
return eol === -1 ? code.length : eol + 1;
|
|
1173
|
+
}
|
|
1174
|
+
if (code[pos + 1] === "*") {
|
|
1175
|
+
const end = code.indexOf("*/", pos + 2);
|
|
1176
|
+
return end === -1 ? code.length : end + 2;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return pos;
|
|
1180
|
+
}
|
|
1181
|
+
function findMatchingParen(code, startPos) {
|
|
1182
|
+
let depth = 1;
|
|
1183
|
+
let i = startPos;
|
|
1184
|
+
while (i < code.length && depth > 0) {
|
|
1185
|
+
const skipped = skipStringOrComment(code, i);
|
|
1186
|
+
if (skipped > i) {
|
|
1187
|
+
i = skipped;
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
if (code[i] === "(") depth++;
|
|
1191
|
+
if (code[i] === ")") depth--;
|
|
1192
|
+
i++;
|
|
1193
|
+
}
|
|
1194
|
+
return i;
|
|
1195
|
+
}
|
|
1196
|
+
function countArgs(code, startPos, endPos) {
|
|
1197
|
+
let depth = 0;
|
|
1198
|
+
let argCount = 0;
|
|
1199
|
+
let hasContent = false;
|
|
1200
|
+
let i = startPos;
|
|
1201
|
+
while (i < endPos) {
|
|
1202
|
+
const skipped = skipStringOrComment(code, i);
|
|
1203
|
+
if (skipped > i) {
|
|
1204
|
+
hasContent = true;
|
|
1205
|
+
i = skipped;
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
const char = code[i];
|
|
1209
|
+
if (char === "(" || char === "[" || char === "{") {
|
|
1210
|
+
depth++;
|
|
1211
|
+
hasContent = true;
|
|
1212
|
+
} else if (char === ")" || char === "]" || char === "}") {
|
|
1213
|
+
depth--;
|
|
1214
|
+
} else if (char === "," && depth === 0) {
|
|
1215
|
+
argCount++;
|
|
1216
|
+
} else if (!/\s/.test(char)) {
|
|
1217
|
+
hasContent = true;
|
|
1218
|
+
}
|
|
1219
|
+
i++;
|
|
1220
|
+
}
|
|
1221
|
+
return hasContent ? argCount + 1 : 0;
|
|
1222
|
+
}
|
|
1223
|
+
function transformPrerenderHandlerExports(code, filePath, sourceId, isBuild = false) {
|
|
1224
|
+
if (!code.includes("createPrerenderHandler")) {
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
if (!hasCreatePrerenderHandlerImport(code)) {
|
|
1228
|
+
return null;
|
|
1229
|
+
}
|
|
1230
|
+
const pattern = /export\s+const\s+(\w+)\s*=\s*createPrerenderHandler\s*(?:<[^>]*>)?\s*\(/g;
|
|
1231
|
+
const s = new MagicString5(code);
|
|
1232
|
+
let hasChanges = false;
|
|
1233
|
+
let match;
|
|
1234
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
1235
|
+
const exportName = match[1];
|
|
1236
|
+
const matchEnd = match.index + match[0].length;
|
|
1237
|
+
const afterClose = findMatchingParen(code, matchEnd);
|
|
1238
|
+
const closeParenPos = afterClose - 1;
|
|
1239
|
+
const argCount = countArgs(code, matchEnd, closeParenPos);
|
|
1240
|
+
let statementEnd = afterClose;
|
|
1241
|
+
while (statementEnd < code.length && /\s/.test(code[statementEnd])) {
|
|
1242
|
+
statementEnd++;
|
|
1243
|
+
}
|
|
1244
|
+
if (code[statementEnd] === ";") {
|
|
1245
|
+
statementEnd++;
|
|
1246
|
+
}
|
|
1247
|
+
const handlerId = isBuild ? hashPrerenderHandlerId(filePath, exportName) : `${filePath}#${exportName}`;
|
|
1248
|
+
let paramInjection;
|
|
1249
|
+
if (argCount === 0) {
|
|
1250
|
+
paramInjection = `undefined, "${handlerId}"`;
|
|
1251
|
+
} else if (argCount === 1) {
|
|
1252
|
+
paramInjection = `, undefined, "${handlerId}"`;
|
|
1253
|
+
} else {
|
|
1254
|
+
paramInjection = `, "${handlerId}"`;
|
|
1255
|
+
}
|
|
1256
|
+
s.appendLeft(closeParenPos, paramInjection);
|
|
1257
|
+
const propInjection = `
|
|
1258
|
+
${exportName}.$$id = "${handlerId}";`;
|
|
1259
|
+
s.appendRight(statementEnd, propInjection);
|
|
1260
|
+
hasChanges = true;
|
|
1261
|
+
}
|
|
1262
|
+
if (!hasChanges) {
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
return {
|
|
1266
|
+
code: s.toString(),
|
|
1267
|
+
map: s.generateMap({ source: sourceId, includeContent: true, hires: "boundary" })
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function generateWholeFileHandlerStubs(code, filePath, isBuild) {
|
|
1271
|
+
const handlerPattern = /export\s+const\s+(\w+)\s*=\s*createPrerenderHandler\s*(?:<[^>]*>)?\s*\(/g;
|
|
1272
|
+
const handlers = [];
|
|
1273
|
+
let match;
|
|
1274
|
+
while ((match = handlerPattern.exec(code)) !== null) {
|
|
1275
|
+
handlers.push(match[1]);
|
|
1276
|
+
}
|
|
1277
|
+
if (handlers.length === 0) return null;
|
|
1278
|
+
if (/export\s*\{/.test(code) || /export\s*\*/.test(code)) {
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
const allExports = /export\s+(const|let|var|function|class|default)\s+(\w+)/g;
|
|
1282
|
+
let exportMatch;
|
|
1283
|
+
while ((exportMatch = allExports.exec(code)) !== null) {
|
|
1284
|
+
const name = exportMatch[2];
|
|
1285
|
+
if (!handlers.includes(name)) {
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
const stubs = handlers.map((name) => {
|
|
1290
|
+
const handlerId = isBuild ? hashPrerenderHandlerId(filePath, name) : `${filePath}#${name}`;
|
|
1291
|
+
return `export const ${name} = { __brand: "prerenderHandler", $$id: "${handlerId}" };`;
|
|
1292
|
+
});
|
|
1293
|
+
return { code: stubs.join("\n") + "\n", map: null };
|
|
1294
|
+
}
|
|
1295
|
+
function generatePrerenderHandlerStubs(code, filePath, sourceId, isBuild = false) {
|
|
1296
|
+
const pattern = /export\s+const\s+(\w+)\s*=\s*(createPrerenderHandler\s*(?:<[^>]*>)?\s*\()/g;
|
|
1297
|
+
const s = new MagicString5(code);
|
|
1298
|
+
let hasChanges = false;
|
|
1299
|
+
let match;
|
|
1300
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
1301
|
+
const exportName = match[1];
|
|
1302
|
+
const callStart = match.index + match[0].length - match[2].length;
|
|
1303
|
+
const openParenPos = match.index + match[0].length;
|
|
1304
|
+
const afterCloseParen = findMatchingParen(code, openParenPos);
|
|
1305
|
+
const handlerId = isBuild ? hashPrerenderHandlerId(filePath, exportName) : `${filePath}#${exportName}`;
|
|
1306
|
+
s.overwrite(
|
|
1307
|
+
callStart,
|
|
1308
|
+
afterCloseParen,
|
|
1309
|
+
`{ __brand: "prerenderHandler", $$id: "${handlerId}" }`
|
|
1310
|
+
);
|
|
1311
|
+
hasChanges = true;
|
|
1312
|
+
}
|
|
1313
|
+
if (!hasChanges) return null;
|
|
1314
|
+
return {
|
|
1315
|
+
code: s.toString(),
|
|
1316
|
+
map: s.generateMap({ source: sourceId, includeContent: true, hires: "boundary" })
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
function exposePrerenderHandlerId() {
|
|
1320
|
+
let config;
|
|
1321
|
+
let isBuild = false;
|
|
1322
|
+
const prerenderHandlerModules = /* @__PURE__ */ new Map();
|
|
1323
|
+
return {
|
|
1324
|
+
name: "@rangojs/router:expose-prerender-handler-id",
|
|
1325
|
+
enforce: "post",
|
|
1326
|
+
api: {
|
|
1327
|
+
prerenderHandlerModules
|
|
1328
|
+
},
|
|
1329
|
+
configResolved(resolvedConfig) {
|
|
1330
|
+
config = resolvedConfig;
|
|
1331
|
+
isBuild = config.command === "build";
|
|
1332
|
+
},
|
|
1333
|
+
transform(code, id) {
|
|
1334
|
+
if (id.includes("/node_modules/")) {
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if (!code.includes("createPrerenderHandler")) {
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (!hasCreatePrerenderHandlerImport(code)) {
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
const relativePath = normalizePath5(path5.relative(config.root, id));
|
|
1344
|
+
const isRscEnv = this.environment?.name === "rsc";
|
|
1345
|
+
if (!isRscEnv) {
|
|
1346
|
+
return generateWholeFileHandlerStubs(code, relativePath, isBuild) ?? generatePrerenderHandlerStubs(code, relativePath, id, isBuild);
|
|
1347
|
+
}
|
|
1348
|
+
if (isBuild) {
|
|
1349
|
+
const handlerPattern = /export\s+const\s+(\w+)\s*=\s*createPrerenderHandler\s*(?:<[^>]*>)?\s*\(/g;
|
|
1350
|
+
const exportNames = [];
|
|
1351
|
+
let m;
|
|
1352
|
+
while ((m = handlerPattern.exec(code)) !== null) {
|
|
1353
|
+
exportNames.push(m[1]);
|
|
1354
|
+
}
|
|
1355
|
+
if (exportNames.length > 0) {
|
|
1356
|
+
prerenderHandlerModules.set(id, exportNames);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return transformPrerenderHandlerExports(code, relativePath, id, isBuild);
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// src/vite/virtual-entries.ts
|
|
1365
|
+
var VIRTUAL_ENTRY_BROWSER = `
|
|
1366
|
+
import {
|
|
1367
|
+
createFromReadableStream,
|
|
1368
|
+
createFromFetch,
|
|
1369
|
+
setServerCallback,
|
|
1370
|
+
encodeReply,
|
|
1371
|
+
createTemporaryReferenceSet,
|
|
1372
|
+
} from "@rangojs/router/internal/deps/browser";
|
|
1373
|
+
import { createElement, StrictMode } from "react";
|
|
1374
|
+
import { hydrateRoot } from "react-dom/client";
|
|
1375
|
+
import { rscStream } from "@rangojs/router/internal/deps/html-stream-client";
|
|
1376
|
+
import { initBrowserApp, RSCRouter } from "@rangojs/router/browser";
|
|
1377
|
+
|
|
1378
|
+
async function initializeApp() {
|
|
1379
|
+
const deps = {
|
|
1380
|
+
createFromFetch,
|
|
1381
|
+
createFromReadableStream,
|
|
1382
|
+
encodeReply,
|
|
1383
|
+
setServerCallback,
|
|
1384
|
+
createTemporaryReferenceSet,
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
await initBrowserApp({ rscStream, deps });
|
|
1388
|
+
|
|
1389
|
+
hydrateRoot(
|
|
1390
|
+
document,
|
|
1391
|
+
createElement(StrictMode, null, createElement(RSCRouter))
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
initializeApp().catch(console.error);
|
|
1396
|
+
`.trim();
|
|
1397
|
+
var VIRTUAL_ENTRY_SSR = `
|
|
1398
|
+
import { createFromReadableStream } from "@rangojs/router/internal/deps/ssr";
|
|
1399
|
+
import { renderToReadableStream } from "react-dom/server.edge";
|
|
1400
|
+
import { injectRSCPayload } from "@rangojs/router/internal/deps/html-stream-server";
|
|
1401
|
+
import { createSSRHandler } from "@rangojs/router/ssr";
|
|
1402
|
+
|
|
1403
|
+
export const renderHTML = createSSRHandler({
|
|
1404
|
+
createFromReadableStream,
|
|
1405
|
+
renderToReadableStream,
|
|
1406
|
+
injectRSCPayload,
|
|
1407
|
+
loadBootstrapScriptContent: () =>
|
|
1408
|
+
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
1409
|
+
});
|
|
1410
|
+
`.trim();
|
|
1411
|
+
function getVirtualEntryRSC(routerPath) {
|
|
1412
|
+
return `
|
|
1413
|
+
import {
|
|
1414
|
+
renderToReadableStream,
|
|
1415
|
+
decodeReply,
|
|
1416
|
+
createTemporaryReferenceSet,
|
|
1417
|
+
loadServerAction,
|
|
1418
|
+
decodeAction,
|
|
1419
|
+
decodeFormState,
|
|
1420
|
+
} from "@rangojs/router/internal/deps/rsc";
|
|
1421
|
+
import { router } from "${routerPath}";
|
|
1422
|
+
import { createRSCHandler } from "@rangojs/router/internal/rsc-handler";
|
|
1423
|
+
import { VERSION } from "@rangojs/router:version";
|
|
1424
|
+
|
|
1425
|
+
// Import loader manifest to ensure all fetchable loaders are registered at startup
|
|
1426
|
+
// This is critical for serverless/multi-process deployments where the loader module
|
|
1427
|
+
// might not be imported before a GET request arrives
|
|
1428
|
+
import "virtual:rsc-router/loader-manifest";
|
|
1429
|
+
|
|
1430
|
+
// Import pre-generated route manifest so href() works immediately on cold start.
|
|
1431
|
+
// In build mode, this contains the full route map generated at build time.
|
|
1432
|
+
// In dev mode, this is a no-op (manifest is populated in-memory by the discovery plugin).
|
|
1433
|
+
import "virtual:rsc-router/routes-manifest";
|
|
1434
|
+
|
|
1435
|
+
export default createRSCHandler({
|
|
1436
|
+
router,
|
|
1437
|
+
version: VERSION,
|
|
1438
|
+
deps: {
|
|
1439
|
+
renderToReadableStream,
|
|
1440
|
+
decodeReply,
|
|
1441
|
+
createTemporaryReferenceSet,
|
|
1442
|
+
loadServerAction,
|
|
1443
|
+
decodeAction,
|
|
1444
|
+
decodeFormState,
|
|
1445
|
+
},
|
|
1446
|
+
loadSSRModule: () =>
|
|
1447
|
+
import.meta.viteRsc.loadModule("ssr", "index"),
|
|
1448
|
+
});
|
|
1449
|
+
`.trim();
|
|
1450
|
+
}
|
|
1451
|
+
var VIRTUAL_IDS = {
|
|
1452
|
+
browser: "virtual:rsc-router/entry.browser.js",
|
|
1453
|
+
ssr: "virtual:rsc-router/entry.ssr.js",
|
|
1454
|
+
rsc: "virtual:rsc-router/entry.rsc.js",
|
|
1455
|
+
version: "@rangojs/router:version"
|
|
1456
|
+
};
|
|
1457
|
+
function getVirtualVersionContent(version) {
|
|
1458
|
+
return `export const VERSION = ${JSON.stringify(version)};`;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/vite/package-resolution.ts
|
|
1462
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
1463
|
+
import { resolve as resolve2 } from "node:path";
|
|
1464
|
+
|
|
1465
|
+
// package.json
|
|
1466
|
+
var package_default = {
|
|
1467
|
+
name: "@rangojs/router",
|
|
1468
|
+
version: "0.0.0-experimental.10",
|
|
1469
|
+
type: "module",
|
|
1470
|
+
description: "Django-inspired RSC router with composable URL patterns",
|
|
1471
|
+
author: "Ivo Todorov",
|
|
1472
|
+
license: "MIT",
|
|
1473
|
+
repository: {
|
|
1474
|
+
type: "git",
|
|
1475
|
+
url: "git+https://github.com/ivogt/vite-rsc.git",
|
|
1476
|
+
directory: "packages/rangojs-router"
|
|
1477
|
+
},
|
|
1478
|
+
homepage: "https://github.com/ivogt/vite-rsc#readme",
|
|
1479
|
+
bugs: {
|
|
1480
|
+
url: "https://github.com/ivogt/vite-rsc/issues"
|
|
1481
|
+
},
|
|
1482
|
+
publishConfig: {
|
|
1483
|
+
access: "public",
|
|
1484
|
+
tag: "experimental"
|
|
1485
|
+
},
|
|
1486
|
+
keywords: [
|
|
1487
|
+
"react",
|
|
1488
|
+
"rsc",
|
|
1489
|
+
"react-server-components",
|
|
1490
|
+
"router",
|
|
1491
|
+
"vite"
|
|
1492
|
+
],
|
|
1493
|
+
exports: {
|
|
1494
|
+
".": {
|
|
1495
|
+
"react-server": "./src/index.rsc.ts",
|
|
1496
|
+
types: "./src/index.rsc.ts",
|
|
1497
|
+
default: "./src/index.ts"
|
|
1498
|
+
},
|
|
1499
|
+
"./server": {
|
|
1500
|
+
types: "./src/server.ts",
|
|
1501
|
+
import: "./src/server.ts"
|
|
1502
|
+
},
|
|
1503
|
+
"./client": {
|
|
1504
|
+
"react-server": "./src/client.rsc.tsx",
|
|
1505
|
+
types: "./src/client.tsx",
|
|
1506
|
+
default: "./src/client.tsx"
|
|
1507
|
+
},
|
|
1508
|
+
"./browser": {
|
|
1509
|
+
types: "./src/browser/index.ts",
|
|
1510
|
+
default: "./src/browser/index.ts"
|
|
1511
|
+
},
|
|
1512
|
+
"./ssr": {
|
|
1513
|
+
types: "./src/ssr/index.tsx",
|
|
1514
|
+
default: "./src/ssr/index.tsx"
|
|
1515
|
+
},
|
|
1516
|
+
"./rsc": {
|
|
1517
|
+
"react-server": "./src/rsc/index.ts",
|
|
1518
|
+
types: "./src/rsc/index.ts",
|
|
1519
|
+
default: "./src/rsc/index.ts"
|
|
1520
|
+
},
|
|
1521
|
+
"./vite": {
|
|
1522
|
+
types: "./src/vite/index.ts",
|
|
1523
|
+
import: "./dist/vite/index.js"
|
|
1524
|
+
},
|
|
1525
|
+
"./types": {
|
|
1526
|
+
types: "./src/vite/version.d.ts"
|
|
1527
|
+
},
|
|
1528
|
+
"./__internal": {
|
|
1529
|
+
types: "./src/__internal.ts",
|
|
1530
|
+
default: "./src/__internal.ts"
|
|
1531
|
+
},
|
|
1532
|
+
"./internal/deps/browser": {
|
|
1533
|
+
types: "./src/deps/browser.ts",
|
|
1534
|
+
default: "./src/deps/browser.ts"
|
|
1535
|
+
},
|
|
1536
|
+
"./internal/deps/ssr": {
|
|
1537
|
+
types: "./src/deps/ssr.ts",
|
|
1538
|
+
default: "./src/deps/ssr.ts"
|
|
1539
|
+
},
|
|
1540
|
+
"./internal/deps/rsc": {
|
|
1541
|
+
"react-server": "./src/deps/rsc.ts",
|
|
1542
|
+
types: "./src/deps/rsc.ts",
|
|
1543
|
+
default: "./src/deps/rsc.ts"
|
|
1544
|
+
},
|
|
1545
|
+
"./internal/deps/html-stream-client": {
|
|
1546
|
+
types: "./src/deps/html-stream-client.ts",
|
|
1547
|
+
default: "./src/deps/html-stream-client.ts"
|
|
1548
|
+
},
|
|
1549
|
+
"./internal/deps/html-stream-server": {
|
|
1550
|
+
types: "./src/deps/html-stream-server.ts",
|
|
1551
|
+
default: "./src/deps/html-stream-server.ts"
|
|
1552
|
+
},
|
|
1553
|
+
"./internal/rsc-handler": {
|
|
1554
|
+
"react-server": "./src/rsc/handler.ts",
|
|
1555
|
+
types: "./src/rsc/handler.ts",
|
|
1556
|
+
default: "./src/rsc/handler.ts"
|
|
1557
|
+
},
|
|
1558
|
+
"./cache": {
|
|
1559
|
+
"react-server": "./src/cache/index.ts",
|
|
1560
|
+
types: "./src/cache/index.ts",
|
|
1561
|
+
default: "./src/cache/index.ts"
|
|
1562
|
+
},
|
|
1563
|
+
"./theme": {
|
|
1564
|
+
types: "./src/theme/index.ts",
|
|
1565
|
+
default: "./src/theme/index.ts"
|
|
1566
|
+
},
|
|
1567
|
+
"./build": {
|
|
1568
|
+
types: "./src/build/index.ts",
|
|
1569
|
+
import: "./src/build/index.ts"
|
|
1570
|
+
},
|
|
1571
|
+
"./host": {
|
|
1572
|
+
"react-server": "./src/host/index.ts",
|
|
1573
|
+
types: "./src/host/index.ts",
|
|
1574
|
+
default: "./src/host/index.ts"
|
|
1575
|
+
},
|
|
1576
|
+
"./host/testing": {
|
|
1577
|
+
types: "./src/host/testing.ts",
|
|
1578
|
+
default: "./src/host/testing.ts"
|
|
1579
|
+
}
|
|
1580
|
+
},
|
|
1581
|
+
files: [
|
|
1582
|
+
"src",
|
|
1583
|
+
"!src/**/__tests__",
|
|
1584
|
+
"!src/**/__mocks__",
|
|
1585
|
+
"!src/**/*.test.ts",
|
|
1586
|
+
"!src/**/*.test.tsx",
|
|
1587
|
+
"dist",
|
|
1588
|
+
"skills",
|
|
1589
|
+
"CLAUDE.md",
|
|
1590
|
+
"README.md"
|
|
1591
|
+
],
|
|
1592
|
+
bin: {
|
|
1593
|
+
rango: "./dist/bin/rango.js"
|
|
1594
|
+
},
|
|
1595
|
+
scripts: {
|
|
1596
|
+
build: "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node'",
|
|
1597
|
+
prepublishOnly: "pnpm build",
|
|
1598
|
+
typecheck: "tsc --noEmit",
|
|
1599
|
+
test: "playwright test",
|
|
1600
|
+
"test:ui": "playwright test --ui",
|
|
1601
|
+
"test:unit": "vitest run",
|
|
1602
|
+
"test:unit:watch": "vitest"
|
|
1603
|
+
},
|
|
1604
|
+
peerDependencies: {
|
|
1605
|
+
"@cloudflare/vite-plugin": "^1.21.0",
|
|
1606
|
+
"@vitejs/plugin-rsc": "^0.5.14",
|
|
1607
|
+
react: "^18.0.0 || ^19.0.0",
|
|
1608
|
+
vite: "^7.3.0"
|
|
1609
|
+
},
|
|
1610
|
+
peerDependenciesMeta: {
|
|
1611
|
+
"@cloudflare/vite-plugin": {
|
|
1612
|
+
optional: true
|
|
1613
|
+
},
|
|
1614
|
+
vite: {
|
|
1615
|
+
optional: true
|
|
1616
|
+
}
|
|
1617
|
+
},
|
|
1618
|
+
dependencies: {
|
|
1619
|
+
"@vitejs/plugin-rsc": "^0.5.14",
|
|
1620
|
+
"magic-string": "^0.30.17",
|
|
1621
|
+
picomatch: "^4.0.3",
|
|
1622
|
+
"rsc-html-stream": "^0.0.7"
|
|
1623
|
+
},
|
|
1624
|
+
devDependencies: {
|
|
1625
|
+
"@playwright/test": "^1.49.1",
|
|
1626
|
+
"@types/node": "^24.10.1",
|
|
1627
|
+
"@types/react": "catalog:",
|
|
1628
|
+
"@types/react-dom": "catalog:",
|
|
1629
|
+
esbuild: "^0.27.0",
|
|
1630
|
+
jiti: "^2.6.1",
|
|
1631
|
+
react: "catalog:",
|
|
1632
|
+
"react-dom": "catalog:",
|
|
1633
|
+
tinyexec: "^0.3.2",
|
|
1634
|
+
typescript: "^5.3.0",
|
|
1635
|
+
vitest: "^4.0.0"
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
// src/vite/package-resolution.ts
|
|
1640
|
+
var VIRTUAL_PACKAGE_NAME = "@rangojs/router";
|
|
1641
|
+
function getPublishedPackageName() {
|
|
1642
|
+
return package_default.name;
|
|
1643
|
+
}
|
|
1644
|
+
function isInstalledFromNpm() {
|
|
1645
|
+
const packageName = getPublishedPackageName();
|
|
1646
|
+
return existsSync2(resolve2(process.cwd(), "node_modules", packageName));
|
|
1647
|
+
}
|
|
1648
|
+
function isWorkspaceDevelopment() {
|
|
1649
|
+
return !isInstalledFromNpm();
|
|
1650
|
+
}
|
|
1651
|
+
var PACKAGE_SUBPATHS = [
|
|
1652
|
+
"",
|
|
1653
|
+
"/browser",
|
|
1654
|
+
"/client",
|
|
1655
|
+
"/server",
|
|
1656
|
+
"/rsc",
|
|
1657
|
+
"/ssr",
|
|
1658
|
+
"/internal/deps/browser",
|
|
1659
|
+
"/internal/deps/html-stream-client",
|
|
1660
|
+
"/internal/deps/ssr",
|
|
1661
|
+
"/internal/deps/rsc"
|
|
1662
|
+
];
|
|
1663
|
+
function getExcludeDeps() {
|
|
1664
|
+
const packageName = getPublishedPackageName();
|
|
1665
|
+
const excludes = [];
|
|
1666
|
+
for (const subpath of PACKAGE_SUBPATHS) {
|
|
1667
|
+
excludes.push(`${packageName}${subpath}`);
|
|
1668
|
+
if (packageName !== VIRTUAL_PACKAGE_NAME) {
|
|
1669
|
+
excludes.push(`${VIRTUAL_PACKAGE_NAME}${subpath}`);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
return excludes;
|
|
1673
|
+
}
|
|
1674
|
+
var ALIAS_SUBPATHS = [
|
|
1675
|
+
"/internal/deps/browser",
|
|
1676
|
+
"/internal/deps/ssr",
|
|
1677
|
+
"/internal/deps/rsc",
|
|
1678
|
+
"/internal/deps/html-stream-client",
|
|
1679
|
+
"/internal/deps/html-stream-server",
|
|
1680
|
+
"/browser",
|
|
1681
|
+
"/client",
|
|
1682
|
+
"/server",
|
|
1683
|
+
"/rsc",
|
|
1684
|
+
"/ssr"
|
|
1685
|
+
];
|
|
1686
|
+
function getPackageAliases() {
|
|
1687
|
+
if (isWorkspaceDevelopment()) {
|
|
1688
|
+
return {};
|
|
1689
|
+
}
|
|
1690
|
+
const packageName = getPublishedPackageName();
|
|
1691
|
+
const aliases = {};
|
|
1692
|
+
for (const subpath of ALIAS_SUBPATHS) {
|
|
1693
|
+
aliases[`${VIRTUAL_PACKAGE_NAME}${subpath}`] = `${packageName}${subpath}`;
|
|
1694
|
+
}
|
|
1695
|
+
return aliases;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// src/vite/index.ts
|
|
1699
|
+
var versionEsbuildPlugin = {
|
|
1700
|
+
name: "@rangojs/router-version",
|
|
1701
|
+
setup(build) {
|
|
1702
|
+
build.onResolve({ filter: /^rsc-router:version$/ }, (args) => ({
|
|
1703
|
+
path: args.path,
|
|
1704
|
+
namespace: "@rangojs/router-virtual"
|
|
1705
|
+
}));
|
|
1706
|
+
build.onLoad({ filter: /.*/, namespace: "@rangojs/router-virtual" }, () => ({
|
|
1707
|
+
contents: `export const VERSION = "dev";`,
|
|
1708
|
+
loader: "js"
|
|
1709
|
+
}));
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
var sharedEsbuildOptions = {
|
|
1713
|
+
plugins: [versionEsbuildPlugin]
|
|
1714
|
+
};
|
|
1715
|
+
function createVirtualEntriesPlugin(entries, routerPath) {
|
|
1716
|
+
const virtualModules = {};
|
|
1717
|
+
if (entries.client === VIRTUAL_IDS.browser) {
|
|
1718
|
+
virtualModules[VIRTUAL_IDS.browser] = VIRTUAL_ENTRY_BROWSER;
|
|
1719
|
+
}
|
|
1720
|
+
if (entries.ssr === VIRTUAL_IDS.ssr) {
|
|
1721
|
+
virtualModules[VIRTUAL_IDS.ssr] = VIRTUAL_ENTRY_SSR;
|
|
1722
|
+
}
|
|
1723
|
+
if (entries.rsc === VIRTUAL_IDS.rsc && routerPath) {
|
|
1724
|
+
const absoluteRouterPath = routerPath.startsWith(".") ? "/" + routerPath.slice(2) : routerPath;
|
|
1725
|
+
virtualModules[VIRTUAL_IDS.rsc] = getVirtualEntryRSC(absoluteRouterPath);
|
|
1726
|
+
}
|
|
1727
|
+
return {
|
|
1728
|
+
name: "@rangojs/router:virtual-entries",
|
|
1729
|
+
enforce: "pre",
|
|
1730
|
+
resolveId(id) {
|
|
1731
|
+
if (id in virtualModules) {
|
|
1732
|
+
return "\0" + id;
|
|
1733
|
+
}
|
|
1734
|
+
if (id.startsWith("\0") && id.slice(1) in virtualModules) {
|
|
1735
|
+
return id;
|
|
1736
|
+
}
|
|
1737
|
+
return null;
|
|
1738
|
+
},
|
|
1739
|
+
load(id) {
|
|
1740
|
+
if (id.startsWith("\0virtual:rsc-router/")) {
|
|
1741
|
+
const virtualId = id.slice(1);
|
|
1742
|
+
if (virtualId in virtualModules) {
|
|
1743
|
+
return virtualModules[virtualId];
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return null;
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
function getManualChunks(id) {
|
|
1751
|
+
const normalized = Vite.normalizePath(id);
|
|
1752
|
+
if (normalized.includes("node_modules/react/") || normalized.includes("node_modules/react-dom/") || normalized.includes("node_modules/react-server-dom-webpack/") || normalized.includes("node_modules/@vitejs/plugin-rsc/")) {
|
|
1753
|
+
return "react";
|
|
1754
|
+
}
|
|
1755
|
+
const packageName = getPublishedPackageName();
|
|
1756
|
+
if (normalized.includes(`node_modules/${packageName}/`) || normalized.includes("packages/rsc-router/") || normalized.includes("packages/rangojs-router/")) {
|
|
1757
|
+
return "router";
|
|
1758
|
+
}
|
|
1759
|
+
return void 0;
|
|
1760
|
+
}
|
|
1761
|
+
function createVersionPlugin() {
|
|
1762
|
+
const buildVersion = Date.now().toString(16);
|
|
1763
|
+
let currentVersion = buildVersion;
|
|
1764
|
+
let isDev = false;
|
|
1765
|
+
let server = null;
|
|
1766
|
+
return {
|
|
1767
|
+
name: "@rangojs/router:version",
|
|
1768
|
+
enforce: "pre",
|
|
1769
|
+
configResolved(config) {
|
|
1770
|
+
isDev = config.command === "serve";
|
|
1771
|
+
},
|
|
1772
|
+
configureServer(devServer) {
|
|
1773
|
+
server = devServer;
|
|
1774
|
+
},
|
|
1775
|
+
resolveId(id) {
|
|
1776
|
+
if (id === VIRTUAL_IDS.version) {
|
|
1777
|
+
return "\0" + id;
|
|
1778
|
+
}
|
|
1779
|
+
return null;
|
|
1780
|
+
},
|
|
1781
|
+
load(id) {
|
|
1782
|
+
if (id === "\0" + VIRTUAL_IDS.version) {
|
|
1783
|
+
return getVirtualVersionContent(currentVersion);
|
|
1784
|
+
}
|
|
1785
|
+
return null;
|
|
1786
|
+
},
|
|
1787
|
+
// Track RSC module changes and update version
|
|
1788
|
+
hotUpdate(ctx) {
|
|
1789
|
+
if (!isDev) return;
|
|
1790
|
+
const isRscModule = this.environment?.name === "rsc";
|
|
1791
|
+
if (isRscModule && ctx.modules.length > 0) {
|
|
1792
|
+
currentVersion = Date.now().toString(16);
|
|
1793
|
+
console.log(
|
|
1794
|
+
`[rsc-router] RSC module changed, version updated: ${currentVersion}`
|
|
1795
|
+
);
|
|
1796
|
+
if (server) {
|
|
1797
|
+
const rscEnv = server.environments?.rsc;
|
|
1798
|
+
if (rscEnv?.moduleGraph) {
|
|
1799
|
+
const versionMod = rscEnv.moduleGraph.getModuleById(
|
|
1800
|
+
"\0" + VIRTUAL_IDS.version
|
|
1801
|
+
);
|
|
1802
|
+
if (versionMod) {
|
|
1803
|
+
rscEnv.moduleGraph.invalidateModule(versionMod);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
function flattenLeafEntries(prefixTree, routeManifest, result) {
|
|
1812
|
+
function visit(node) {
|
|
1813
|
+
const children = node.children || {};
|
|
1814
|
+
if (Object.keys(children).length === 0 && node.routes && node.routes.length > 0) {
|
|
1815
|
+
const routes = {};
|
|
1816
|
+
for (const name of node.routes) {
|
|
1817
|
+
if (name in routeManifest) {
|
|
1818
|
+
routes[name] = routeManifest[name];
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
result.push({ staticPrefix: node.staticPrefix, routes });
|
|
1822
|
+
} else {
|
|
1823
|
+
for (const child of Object.values(children)) {
|
|
1824
|
+
visit(child);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
for (const node of Object.values(prefixTree)) {
|
|
1829
|
+
visit(node);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
function buildRouteToStaticPrefix(prefixTree, result) {
|
|
1833
|
+
function visit(node) {
|
|
1834
|
+
const sp = node.staticPrefix || "";
|
|
1835
|
+
for (const name of node.routes || []) {
|
|
1836
|
+
result[name] = sp;
|
|
1837
|
+
}
|
|
1838
|
+
for (const child of Object.values(node.children || {})) {
|
|
1839
|
+
visit(child);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
for (const node of Object.values(prefixTree)) {
|
|
1843
|
+
visit(node);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
1847
|
+
let projectRoot = "";
|
|
1848
|
+
let isBuildMode = false;
|
|
1849
|
+
let userResolveAlias = void 0;
|
|
1850
|
+
let scanFilter;
|
|
1851
|
+
let cachedRouterFiles;
|
|
1852
|
+
let mergedRouteManifest = null;
|
|
1853
|
+
let perRouterManifests = [];
|
|
1854
|
+
let prerenderBuildUrls = null;
|
|
1855
|
+
let prerenderRouteHashMap = {};
|
|
1856
|
+
let rscPluginManager = null;
|
|
1857
|
+
let cfIntegrationApi = null;
|
|
1858
|
+
let discoveryDone = null;
|
|
1859
|
+
let mergedPrecomputedEntries = null;
|
|
1860
|
+
let mergedRouteTrie = null;
|
|
1861
|
+
async function discoverRouters(rscEnv) {
|
|
1862
|
+
await rscEnv.runner.import(entryPath);
|
|
1863
|
+
const serverMod = await rscEnv.runner.import("@rangojs/router/server");
|
|
1864
|
+
let registry = serverMod.RouterRegistry;
|
|
1865
|
+
if (!registry || registry.size === 0) {
|
|
1866
|
+
try {
|
|
1867
|
+
const hostMod = await rscEnv.runner.import("@rangojs/router/host");
|
|
1868
|
+
const hostRegistry = hostMod.HostRouterRegistry;
|
|
1869
|
+
if (hostRegistry && hostRegistry.size > 0) {
|
|
1870
|
+
console.log(
|
|
1871
|
+
`[rsc-router] Found ${hostRegistry.size} host router(s), resolving lazy handlers...`
|
|
1872
|
+
);
|
|
1873
|
+
for (const [, entry] of hostRegistry) {
|
|
1874
|
+
for (const route of entry.routes) {
|
|
1875
|
+
if (typeof route.handler === "function") {
|
|
1876
|
+
try {
|
|
1877
|
+
await route.handler();
|
|
1878
|
+
} catch {
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
if (entry.fallback && typeof entry.fallback.handler === "function") {
|
|
1883
|
+
try {
|
|
1884
|
+
await entry.fallback.handler();
|
|
1885
|
+
} catch {
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
const freshServerMod = await rscEnv.runner.import("@rangojs/router/server");
|
|
1890
|
+
const freshRegistry = freshServerMod.RouterRegistry;
|
|
1891
|
+
if (freshRegistry && freshRegistry.size > 0) {
|
|
1892
|
+
Object.assign(serverMod, freshServerMod);
|
|
1893
|
+
registry = freshRegistry;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
} catch {
|
|
1897
|
+
}
|
|
1898
|
+
if (!registry || registry.size === 0) {
|
|
1899
|
+
throw new Error(
|
|
1900
|
+
`[rsc-router] No routers found in registry after importing ${entryPath}`
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
const buildMod = await rscEnv.runner.import("@rangojs/router/build");
|
|
1905
|
+
const generateManifest = buildMod.generateManifest;
|
|
1906
|
+
mergedRouteManifest = {};
|
|
1907
|
+
mergedPrecomputedEntries = [];
|
|
1908
|
+
perRouterManifests = [];
|
|
1909
|
+
let mergedRouteAncestry = {};
|
|
1910
|
+
let mergedRouteTrailingSlash = {};
|
|
1911
|
+
let routerMountIndex = 0;
|
|
1912
|
+
const allManifests = [];
|
|
1913
|
+
for (const [id, router] of registry) {
|
|
1914
|
+
if (!router.urlpatterns || !generateManifest) {
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
const manifest = generateManifest(router.urlpatterns, routerMountIndex);
|
|
1918
|
+
routerMountIndex++;
|
|
1919
|
+
allManifests.push({ id, manifest });
|
|
1920
|
+
const routeCount = Object.keys(manifest.routeManifest).length;
|
|
1921
|
+
const staticRoutes = Object.values(manifest.routeManifest).filter(
|
|
1922
|
+
(p) => !p.includes(":") && !p.includes("*")
|
|
1923
|
+
).length;
|
|
1924
|
+
const dynamicRoutes = routeCount - staticRoutes;
|
|
1925
|
+
Object.assign(mergedRouteManifest, manifest.routeManifest);
|
|
1926
|
+
perRouterManifests.push({ id, routeManifest: manifest.routeManifest, sourceFile: router.__sourceFile });
|
|
1927
|
+
if (manifest._routeAncestry) {
|
|
1928
|
+
Object.assign(mergedRouteAncestry, manifest._routeAncestry);
|
|
1929
|
+
}
|
|
1930
|
+
if (manifest.routeTrailingSlash) {
|
|
1931
|
+
Object.assign(mergedRouteTrailingSlash, manifest.routeTrailingSlash);
|
|
1932
|
+
}
|
|
1933
|
+
flattenLeafEntries(manifest.prefixTree, manifest.routeManifest, mergedPrecomputedEntries);
|
|
1934
|
+
const hash = hashRouterId(id);
|
|
1935
|
+
const outDir = join2(projectRoot, "dist", "static", `__${hash}`);
|
|
1936
|
+
mkdirSync(outDir, { recursive: true });
|
|
1937
|
+
writeFileSync2(
|
|
1938
|
+
join2(outDir, "routes.json"),
|
|
1939
|
+
JSON.stringify(manifest.routeManifest, null, 2) + "\n"
|
|
1940
|
+
);
|
|
1941
|
+
writeFileSync2(
|
|
1942
|
+
join2(outDir, "prefixes.json"),
|
|
1943
|
+
JSON.stringify(manifest.prefixTree, null, 2) + "\n"
|
|
1944
|
+
);
|
|
1945
|
+
console.log(
|
|
1946
|
+
`[rsc-router] Router "${id}" -> ${routeCount} routes (${staticRoutes} static, ${dynamicRoutes} dynamic) -> dist/static/__${hash}/`
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
if (mergedRouteManifest && Object.keys(mergedRouteManifest).length > 0) {
|
|
1950
|
+
const buildRouteTrie = buildMod.buildRouteTrie;
|
|
1951
|
+
if (buildRouteTrie && mergedRouteAncestry) {
|
|
1952
|
+
const routeToStaticPrefix = {};
|
|
1953
|
+
for (const { manifest } of allManifests) {
|
|
1954
|
+
for (const name of Object.keys(manifest.routeManifest)) {
|
|
1955
|
+
if (!(name in routeToStaticPrefix)) {
|
|
1956
|
+
routeToStaticPrefix[name] = "";
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
buildRouteToStaticPrefix(manifest.prefixTree, routeToStaticPrefix);
|
|
1960
|
+
}
|
|
1961
|
+
const prerenderRouteNames = /* @__PURE__ */ new Set();
|
|
1962
|
+
const passthroughRouteNames = /* @__PURE__ */ new Set();
|
|
1963
|
+
const mergedResponseTypeRoutes = {};
|
|
1964
|
+
for (const { manifest } of allManifests) {
|
|
1965
|
+
if (manifest.prerenderRoutes) {
|
|
1966
|
+
for (const name of manifest.prerenderRoutes) {
|
|
1967
|
+
prerenderRouteNames.add(name);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
if (manifest.passthroughRoutes) {
|
|
1971
|
+
for (const name of manifest.passthroughRoutes) {
|
|
1972
|
+
passthroughRouteNames.add(name);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
if (manifest.responseTypeRoutes) {
|
|
1976
|
+
Object.assign(mergedResponseTypeRoutes, manifest.responseTypeRoutes);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
mergedRouteTrie = buildRouteTrie(
|
|
1980
|
+
mergedRouteManifest,
|
|
1981
|
+
mergedRouteAncestry,
|
|
1982
|
+
routeToStaticPrefix,
|
|
1983
|
+
Object.keys(mergedRouteTrailingSlash).length > 0 ? mergedRouteTrailingSlash : void 0,
|
|
1984
|
+
prerenderRouteNames.size > 0 ? prerenderRouteNames : void 0,
|
|
1985
|
+
passthroughRouteNames.size > 0 ? passthroughRouteNames : void 0,
|
|
1986
|
+
Object.keys(mergedResponseTypeRoutes).length > 0 ? mergedResponseTypeRoutes : void 0
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
if (opts?.enableBuildPrerender) {
|
|
1991
|
+
const urls = [];
|
|
1992
|
+
const routeHashMap = {};
|
|
1993
|
+
for (const { id, manifest } of allManifests) {
|
|
1994
|
+
if (!manifest.prerenderRoutes) continue;
|
|
1995
|
+
const rHash = hashRouterId(id);
|
|
1996
|
+
const defs = manifest._prerenderDefs || {};
|
|
1997
|
+
for (const routeName of manifest.prerenderRoutes) {
|
|
1998
|
+
routeHashMap[routeName] = rHash;
|
|
1999
|
+
const pattern = manifest.routeManifest[routeName];
|
|
2000
|
+
if (!pattern) continue;
|
|
2001
|
+
const hasDynamic = pattern.includes(":") || pattern.includes("*");
|
|
2002
|
+
if (!hasDynamic) {
|
|
2003
|
+
urls.push(pattern.replace(/\/$/, "") || "/");
|
|
2004
|
+
} else {
|
|
2005
|
+
const def = defs[routeName];
|
|
2006
|
+
if (def?.getParams) {
|
|
2007
|
+
try {
|
|
2008
|
+
const paramsList = await def.getParams();
|
|
2009
|
+
for (const params of paramsList) {
|
|
2010
|
+
let url = pattern;
|
|
2011
|
+
for (const [key, value] of Object.entries(params)) {
|
|
2012
|
+
url = url.replace(`:${key}`, encodeURIComponent(String(value)));
|
|
2013
|
+
}
|
|
2014
|
+
urls.push(url.replace(/\/$/, "") || "/");
|
|
2015
|
+
}
|
|
2016
|
+
} catch (err) {
|
|
2017
|
+
console.warn(
|
|
2018
|
+
`[rsc-router] Failed to get params for prerender route "${routeName}": ${err.message}`
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
} else {
|
|
2022
|
+
console.warn(
|
|
2023
|
+
`[rsc-router] Dynamic prerender route "${routeName}" has no getParams(), skipping`
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
if (urls.length > 0) {
|
|
2030
|
+
prerenderBuildUrls = urls;
|
|
2031
|
+
prerenderRouteHashMap = routeHashMap;
|
|
2032
|
+
console.log(
|
|
2033
|
+
`[rsc-router] Pre-render URLs: ${urls.join(", ")}`
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
return serverMod;
|
|
2038
|
+
}
|
|
2039
|
+
function writeRouteTypesFiles() {
|
|
2040
|
+
if (perRouterManifests.length === 0) return;
|
|
2041
|
+
try {
|
|
2042
|
+
const entryDir = dirname2(resolve3(projectRoot, entryPath));
|
|
2043
|
+
const oldCombinedPath = join2(entryDir, "named-routes.gen.ts");
|
|
2044
|
+
if (existsSync3(oldCombinedPath)) {
|
|
2045
|
+
unlinkSync2(oldCombinedPath);
|
|
2046
|
+
console.log(`[rsc-router] Removed stale combined route types: ${oldCombinedPath}`);
|
|
2047
|
+
}
|
|
2048
|
+
} catch {
|
|
2049
|
+
}
|
|
2050
|
+
for (const { routeManifest, sourceFile } of perRouterManifests) {
|
|
2051
|
+
if (!sourceFile) continue;
|
|
2052
|
+
try {
|
|
2053
|
+
const routerDir = dirname2(sourceFile);
|
|
2054
|
+
const routerBasename = basename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
|
|
2055
|
+
const outPath = join2(routerDir, `${routerBasename}.named-routes.gen.ts`);
|
|
2056
|
+
const source = generateRouteTypesSource(routeManifest);
|
|
2057
|
+
const existing = existsSync3(outPath) ? readFileSync2(outPath, "utf-8") : null;
|
|
2058
|
+
if (existing !== source) {
|
|
2059
|
+
writeFileSync2(outPath, source);
|
|
2060
|
+
console.log(`[rsc-router] Generated route types -> ${outPath}`);
|
|
2061
|
+
}
|
|
2062
|
+
} catch (err) {
|
|
2063
|
+
console.warn(`[rsc-router] Failed to write named-routes.gen.ts: ${err.message}`);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
return {
|
|
2068
|
+
name: "@rangojs/router:discovery",
|
|
2069
|
+
configResolved(config) {
|
|
2070
|
+
projectRoot = config.root;
|
|
2071
|
+
isBuildMode = config.command === "build";
|
|
2072
|
+
userResolveAlias = config.resolve.alias;
|
|
2073
|
+
if (opts?.include || opts?.exclude) {
|
|
2074
|
+
scanFilter = createScanFilter(projectRoot, {
|
|
2075
|
+
include: opts.include,
|
|
2076
|
+
exclude: opts.exclude
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
if (opts?.staticRouteTypesGeneration !== false) {
|
|
2080
|
+
writePerModuleRouteTypes(projectRoot, scanFilter);
|
|
2081
|
+
cachedRouterFiles = findRouterFiles(projectRoot, scanFilter);
|
|
2082
|
+
writeCombinedRouteTypes(projectRoot, cachedRouterFiles);
|
|
2083
|
+
}
|
|
2084
|
+
if (opts?.enableBuildPrerender) {
|
|
2085
|
+
const rscPlugin = config.plugins.find((p) => p.name === "rsc:minimal");
|
|
2086
|
+
if (rscPlugin?.api?.manager) {
|
|
2087
|
+
rscPluginManager = rscPlugin.api.manager;
|
|
2088
|
+
}
|
|
2089
|
+
const cfPlugin = config.plugins.find(
|
|
2090
|
+
(p) => p.name === "@rangojs/router:cloudflare-integration"
|
|
2091
|
+
);
|
|
2092
|
+
if (cfPlugin?.api) {
|
|
2093
|
+
cfIntegrationApi = cfPlugin.api;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
},
|
|
2097
|
+
// Dev mode: discover routers and populate manifest in memory.
|
|
2098
|
+
// Skipped in build mode (buildStart handles it).
|
|
2099
|
+
configureServer(server) {
|
|
2100
|
+
if (isBuildMode) return;
|
|
2101
|
+
if (globalThis.__rscRouterDiscoveryActive) return;
|
|
2102
|
+
let resolveDiscovery;
|
|
2103
|
+
const discoveryPromise = new Promise((resolve4) => {
|
|
2104
|
+
resolveDiscovery = resolve4;
|
|
2105
|
+
});
|
|
2106
|
+
const discover = async () => {
|
|
2107
|
+
const rscEnv = server.environments?.rsc;
|
|
2108
|
+
if (!rscEnv?.runner) {
|
|
2109
|
+
resolveDiscovery();
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
try {
|
|
2113
|
+
const serverMod = await rscEnv.runner.import("@rangojs/router/server");
|
|
2114
|
+
if (serverMod?.setManifestReadyPromise) {
|
|
2115
|
+
serverMod.setManifestReadyPromise(discoveryPromise);
|
|
2116
|
+
}
|
|
2117
|
+
await discoverRouters(rscEnv);
|
|
2118
|
+
if (opts?.staticRouteTypesGeneration === false) {
|
|
2119
|
+
writeRouteTypesFiles();
|
|
2120
|
+
}
|
|
2121
|
+
if (mergedRouteManifest && serverMod?.setCachedManifest) {
|
|
2122
|
+
serverMod.setCachedManifest(mergedRouteManifest);
|
|
2123
|
+
}
|
|
2124
|
+
if (mergedPrecomputedEntries && mergedPrecomputedEntries.length > 0 && serverMod?.setPrecomputedEntries) {
|
|
2125
|
+
serverMod.setPrecomputedEntries(mergedPrecomputedEntries);
|
|
2126
|
+
}
|
|
2127
|
+
if (mergedRouteTrie && serverMod?.setRouteTrie) {
|
|
2128
|
+
serverMod.setRouteTrie(mergedRouteTrie);
|
|
2129
|
+
}
|
|
2130
|
+
} catch (err) {
|
|
2131
|
+
console.warn(
|
|
2132
|
+
`[rsc-router] Router discovery failed: ${err.message}
|
|
2133
|
+
${err.stack}`
|
|
2134
|
+
);
|
|
2135
|
+
} finally {
|
|
2136
|
+
resolveDiscovery();
|
|
2137
|
+
}
|
|
2138
|
+
};
|
|
2139
|
+
discoveryDone = new Promise((resolve4) => {
|
|
2140
|
+
setTimeout(() => discover().then(resolve4, resolve4), 0);
|
|
2141
|
+
});
|
|
2142
|
+
if (opts?.staticRouteTypesGeneration !== false) {
|
|
2143
|
+
server.watcher.on("change", (filePath) => {
|
|
2144
|
+
if (filePath.endsWith(".gen.ts")) return;
|
|
2145
|
+
if (!filePath.endsWith(".ts") && !filePath.endsWith(".tsx")) return;
|
|
2146
|
+
if (scanFilter && !scanFilter(filePath)) return;
|
|
2147
|
+
try {
|
|
2148
|
+
const source = readFileSync2(filePath, "utf-8");
|
|
2149
|
+
const trimmed = source.trimStart();
|
|
2150
|
+
if (trimmed.startsWith('"use client"') || trimmed.startsWith("'use client'")) return;
|
|
2151
|
+
const hasUrls = source.includes("urls(");
|
|
2152
|
+
const hasCreateRouter = /\bcreateRouter\s*[<(]/.test(source);
|
|
2153
|
+
if (!hasUrls && !hasCreateRouter) return;
|
|
2154
|
+
if (hasUrls) {
|
|
2155
|
+
writePerModuleRouteTypesForFile(filePath);
|
|
2156
|
+
}
|
|
2157
|
+
if (hasCreateRouter) {
|
|
2158
|
+
cachedRouterFiles = void 0;
|
|
2159
|
+
}
|
|
2160
|
+
writeCombinedRouteTypes(projectRoot, cachedRouterFiles);
|
|
2161
|
+
} catch {
|
|
2162
|
+
}
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
},
|
|
2166
|
+
// Build mode: create a temporary Vite dev server to access the RSC
|
|
2167
|
+
// environment's module runner, then discover routers and generate manifests.
|
|
2168
|
+
// The manifest data is stored for the virtual module's load hook.
|
|
2169
|
+
async buildStart() {
|
|
2170
|
+
if (!isBuildMode) return;
|
|
2171
|
+
if (mergedRouteManifest !== null) return;
|
|
2172
|
+
let tempServer = null;
|
|
2173
|
+
try {
|
|
2174
|
+
globalThis.__rscRouterDiscoveryActive = true;
|
|
2175
|
+
const { default: rsc } = await import("@vitejs/plugin-rsc");
|
|
2176
|
+
tempServer = await createViteServer({
|
|
2177
|
+
root: projectRoot,
|
|
2178
|
+
configFile: false,
|
|
2179
|
+
server: { middlewareMode: true },
|
|
2180
|
+
appType: "custom",
|
|
2181
|
+
logLevel: "silent",
|
|
2182
|
+
// Use the resolved aliases from the real config (includes user's path aliases
|
|
2183
|
+
// like @/ -> src/ AND package aliases from rsc-router)
|
|
2184
|
+
resolve: { alias: userResolveAlias },
|
|
2185
|
+
// Enable automatic JSX runtime so .tsx files don't need `import React`.
|
|
2186
|
+
// Without this, esbuild defaults to classic mode (React.createElement)
|
|
2187
|
+
// which fails when lazy host-router handlers load sub-app modules with JSX.
|
|
2188
|
+
esbuild: { jsx: "automatic", jsxImportSource: "react" },
|
|
2189
|
+
plugins: [
|
|
2190
|
+
rsc({ entries: { client: "virtual:entry-client", ssr: "virtual:entry-ssr", rsc: entryPath } }),
|
|
2191
|
+
createVersionPlugin(),
|
|
2192
|
+
// Stub virtual modules that the RSC entry may import
|
|
2193
|
+
// (e.g., virtual:rsc-router/routes-manifest, virtual:rsc-router/loader-manifest)
|
|
2194
|
+
createVirtualStubPlugin()
|
|
2195
|
+
]
|
|
2196
|
+
});
|
|
2197
|
+
const rscEnv = tempServer.environments?.rsc;
|
|
2198
|
+
if (!rscEnv?.runner) {
|
|
2199
|
+
console.warn(
|
|
2200
|
+
"[rsc-router] RSC environment runner not available during build, skipping manifest generation"
|
|
2201
|
+
);
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
await discoverRouters(rscEnv);
|
|
2205
|
+
writeRouteTypesFiles();
|
|
2206
|
+
} catch (err) {
|
|
2207
|
+
delete globalThis.__rscRouterDiscoveryActive;
|
|
2208
|
+
if (tempServer) {
|
|
2209
|
+
await tempServer.close();
|
|
2210
|
+
}
|
|
2211
|
+
throw new Error(
|
|
2212
|
+
`[rsc-router] Build-time router discovery failed: ${err.message}`
|
|
2213
|
+
);
|
|
2214
|
+
} finally {
|
|
2215
|
+
delete globalThis.__rscRouterDiscoveryActive;
|
|
2216
|
+
if (tempServer) {
|
|
2217
|
+
await tempServer.close();
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
},
|
|
2221
|
+
// Virtual module: provides the pre-generated route manifest as a JS module
|
|
2222
|
+
// that calls setCachedManifest() at import time.
|
|
2223
|
+
resolveId(id) {
|
|
2224
|
+
if (id === VIRTUAL_ROUTES_MANIFEST_ID) {
|
|
2225
|
+
return "\0" + VIRTUAL_ROUTES_MANIFEST_ID;
|
|
2226
|
+
}
|
|
2227
|
+
return null;
|
|
2228
|
+
},
|
|
2229
|
+
async load(id) {
|
|
2230
|
+
if (id === "\0" + VIRTUAL_ROUTES_MANIFEST_ID) {
|
|
2231
|
+
if (discoveryDone) {
|
|
2232
|
+
await discoveryDone;
|
|
2233
|
+
console.log(`[rsc-router] Virtual module loaded after discovery (${mergedRouteManifest ? Object.keys(mergedRouteManifest).length + " routes" : "no data"})`);
|
|
2234
|
+
}
|
|
2235
|
+
const hasManifest = mergedRouteManifest && Object.keys(mergedRouteManifest).length > 0;
|
|
2236
|
+
if (hasManifest) {
|
|
2237
|
+
const lines = [
|
|
2238
|
+
`import { setCachedManifest, setPrecomputedEntries, setRouteTrie } from "@rangojs/router/server";`,
|
|
2239
|
+
`setCachedManifest(${JSON.stringify(mergedRouteManifest)});`
|
|
2240
|
+
];
|
|
2241
|
+
if (mergedPrecomputedEntries && mergedPrecomputedEntries.length > 0) {
|
|
2242
|
+
lines.push(`setPrecomputedEntries(${JSON.stringify(mergedPrecomputedEntries)});`);
|
|
2243
|
+
}
|
|
2244
|
+
if (mergedRouteTrie) {
|
|
2245
|
+
lines.push(`setRouteTrie(${JSON.stringify(mergedRouteTrie)});`);
|
|
2246
|
+
}
|
|
2247
|
+
return lines.join("\n");
|
|
2248
|
+
}
|
|
2249
|
+
return `// Route manifest will be populated at runtime`;
|
|
2250
|
+
}
|
|
2251
|
+
return null;
|
|
2252
|
+
},
|
|
2253
|
+
// Build-time pre-rendering: spawn a child Node.js process to import the
|
|
2254
|
+
// built worker and render each prerender URL to static HTML.
|
|
2255
|
+
// A separate process is needed because Vite registers module resolution
|
|
2256
|
+
// hooks that interfere with importing the bundled worker.
|
|
2257
|
+
//
|
|
2258
|
+
// RETRY SEMANTICS:
|
|
2259
|
+
// Vite's environment-aware builder calls closeBundle once per environment
|
|
2260
|
+
// build (rsc -> client -> ssr). Pre-rendering requires BOTH the client
|
|
2261
|
+
// assets manifest AND the SSR bundle, so early calls bail out silently.
|
|
2262
|
+
// The `order: "post"` ensures this runs after other plugins' closeBundle
|
|
2263
|
+
// hooks. `sequential: true` prevents concurrent closeBundle execution
|
|
2264
|
+
// across environments, avoiding race conditions on shared state like
|
|
2265
|
+
// prerenderBuildUrls. The null-guard on prerenderBuildUrls ensures we
|
|
2266
|
+
// run at most once even if closeBundle fires again after the SSR build.
|
|
2267
|
+
closeBundle: {
|
|
2268
|
+
order: "post",
|
|
2269
|
+
sequential: true,
|
|
2270
|
+
async handler() {
|
|
2271
|
+
if (!isBuildMode || !prerenderBuildUrls?.length) return;
|
|
2272
|
+
if (!rscPluginManager?.buildAssetsManifest) return;
|
|
2273
|
+
const ssrPath = resolve3(projectRoot, "dist/rsc/ssr/index.js");
|
|
2274
|
+
if (!existsSync3(ssrPath)) return;
|
|
2275
|
+
if (prerenderBuildUrls === null) return;
|
|
2276
|
+
const urlsToRender = prerenderBuildUrls;
|
|
2277
|
+
prerenderBuildUrls = null;
|
|
2278
|
+
try {
|
|
2279
|
+
rscPluginManager.writeAssetsManifest(["ssr", "rsc"]);
|
|
2280
|
+
} catch (err) {
|
|
2281
|
+
console.warn(
|
|
2282
|
+
`[rsc-router] Failed to write assets manifest early: ${err.message}`
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
console.log(
|
|
2286
|
+
`[rsc-router] Pre-rendering ${urlsToRender.length} route(s)...`
|
|
2287
|
+
);
|
|
2288
|
+
const scriptPath = resolve3(projectRoot, "dist/.prerender.mjs");
|
|
2289
|
+
const scriptContent = generatePrerenderScript(projectRoot, urlsToRender, prerenderRouteHashMap);
|
|
2290
|
+
writeFileSync2(scriptPath, scriptContent);
|
|
2291
|
+
try {
|
|
2292
|
+
const { execFileSync } = await import("node:child_process");
|
|
2293
|
+
const cleanEnv = { ...process.env };
|
|
2294
|
+
delete cleanEnv.NODE_OPTIONS;
|
|
2295
|
+
delete cleanEnv.TSX;
|
|
2296
|
+
execFileSync(process.execPath, ["--no-warnings", scriptPath], {
|
|
2297
|
+
stdio: "inherit",
|
|
2298
|
+
cwd: projectRoot,
|
|
2299
|
+
env: cleanEnv
|
|
2300
|
+
});
|
|
2301
|
+
const chunkInfo = cfIntegrationApi?.handlerChunkInfo;
|
|
2302
|
+
if (chunkInfo) {
|
|
2303
|
+
const chunkPath = resolve3(projectRoot, "dist/rsc", chunkInfo.fileName);
|
|
2304
|
+
try {
|
|
2305
|
+
let code = readFileSync2(chunkPath, "utf-8");
|
|
2306
|
+
const originalSize = Buffer.byteLength(code);
|
|
2307
|
+
for (const { name, handlerId, passthrough } of chunkInfo.exports) {
|
|
2308
|
+
if (passthrough) continue;
|
|
2309
|
+
const callStartRe = new RegExp(
|
|
2310
|
+
`const\\s+${name}\\s*=\\s*createPrerenderHandler\\s*(?:<[^>]*>)?\\s*\\(`
|
|
2311
|
+
);
|
|
2312
|
+
const startMatch = callStartRe.exec(code);
|
|
2313
|
+
if (!startMatch) continue;
|
|
2314
|
+
const openParenPos = startMatch.index + startMatch[0].length;
|
|
2315
|
+
let depth = 1;
|
|
2316
|
+
let pos = openParenPos;
|
|
2317
|
+
while (pos < code.length && depth > 0) {
|
|
2318
|
+
const ch = code[pos];
|
|
2319
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
2320
|
+
pos++;
|
|
2321
|
+
while (pos < code.length && code[pos] !== ch) {
|
|
2322
|
+
if (code[pos] === "\\") pos++;
|
|
2323
|
+
pos++;
|
|
2324
|
+
}
|
|
2325
|
+
} else if (ch === "(") {
|
|
2326
|
+
depth++;
|
|
2327
|
+
} else if (ch === ")") {
|
|
2328
|
+
depth--;
|
|
2329
|
+
}
|
|
2330
|
+
pos++;
|
|
2331
|
+
}
|
|
2332
|
+
if (depth !== 0) continue;
|
|
2333
|
+
let rangeEnd = pos;
|
|
2334
|
+
while (rangeEnd < code.length && /\s/.test(code[rangeEnd])) rangeEnd++;
|
|
2335
|
+
if (code[rangeEnd] === ";") rangeEnd++;
|
|
2336
|
+
const matched = code.slice(startMatch.index, rangeEnd);
|
|
2337
|
+
if (!matched.includes(handlerId)) continue;
|
|
2338
|
+
const stub = `const ${name} = { __brand: "prerenderHandler", $$id: "${handlerId}" };`;
|
|
2339
|
+
code = code.slice(0, startMatch.index) + stub + code.slice(rangeEnd);
|
|
2340
|
+
code = code.replace(
|
|
2341
|
+
new RegExp(`\\n${name}\\.\\$\\$id\\s*=\\s*"[^"]+";`),
|
|
2342
|
+
""
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
writeFileSync2(chunkPath, code);
|
|
2346
|
+
const newSize = Buffer.byteLength(code);
|
|
2347
|
+
const savedKB = ((originalSize - newSize) / 1024).toFixed(1);
|
|
2348
|
+
console.log(
|
|
2349
|
+
`[rsc-router] Evicted handler code from RSC bundle (${savedKB} KB saved): ${chunkInfo.fileName}`
|
|
2350
|
+
);
|
|
2351
|
+
} catch (replaceErr) {
|
|
2352
|
+
console.warn(
|
|
2353
|
+
`[rsc-router] Failed to evict handler code: ${replaceErr.message}`
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
try {
|
|
2358
|
+
const { readdirSync: readDir } = await import("node:fs");
|
|
2359
|
+
const prerenderData = {};
|
|
2360
|
+
const staticDir = resolve3(projectRoot, "dist/static");
|
|
2361
|
+
for (const hashDir of readDir(staticDir).filter((d) => d.startsWith("__"))) {
|
|
2362
|
+
const prerenderDir = resolve3(staticDir, hashDir, "prerender");
|
|
2363
|
+
if (!existsSync3(prerenderDir)) continue;
|
|
2364
|
+
for (const routeDir of readDir(prerenderDir)) {
|
|
2365
|
+
const routePath = resolve3(prerenderDir, routeDir);
|
|
2366
|
+
for (const file of readDir(routePath).filter((f) => f.endsWith(".flight"))) {
|
|
2367
|
+
const paramHash = file.slice(0, -7);
|
|
2368
|
+
const key = `${routeDir}/${paramHash}`;
|
|
2369
|
+
const content = readFileSync2(resolve3(routePath, file), "utf-8");
|
|
2370
|
+
prerenderData[key] = JSON.parse(content);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
if (Object.keys(prerenderData).length > 0) {
|
|
2375
|
+
const rscEntryPath = resolve3(projectRoot, "dist/rsc/index.js");
|
|
2376
|
+
if (existsSync3(rscEntryPath)) {
|
|
2377
|
+
let rscCode = readFileSync2(rscEntryPath, "utf-8");
|
|
2378
|
+
const injection = `globalThis.__PRERENDER_DATA = ${JSON.stringify(prerenderData)};
|
|
2379
|
+
`;
|
|
2380
|
+
rscCode = injection + rscCode;
|
|
2381
|
+
writeFileSync2(rscEntryPath, rscCode);
|
|
2382
|
+
const dataSize = (Buffer.byteLength(injection) / 1024).toFixed(1);
|
|
2383
|
+
console.log(
|
|
2384
|
+
`[rsc-router] Injected prerender data into RSC bundle (${dataSize} KB, ${Object.keys(prerenderData).length} entries)`
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
} catch (injectErr) {
|
|
2389
|
+
console.warn(
|
|
2390
|
+
`[rsc-router] Failed to inject prerender data: ${injectErr.message}`
|
|
2391
|
+
);
|
|
2392
|
+
}
|
|
2393
|
+
} catch (err) {
|
|
2394
|
+
console.warn(
|
|
2395
|
+
`[rsc-router] Build-time pre-rendering failed: ${err.message}`
|
|
2396
|
+
);
|
|
2397
|
+
} finally {
|
|
2398
|
+
try {
|
|
2399
|
+
const { rmSync } = await import("node:fs");
|
|
2400
|
+
rmSync(scriptPath, { force: true });
|
|
2401
|
+
} catch {
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
function generatePrerenderScript(projectRoot, urls, routeHashMap) {
|
|
2409
|
+
return `
|
|
2410
|
+
import { mkdirSync, writeFileSync, symlinkSync, existsSync, readdirSync, statSync, lstatSync, rmSync } from "node:fs";
|
|
2411
|
+
import { resolve } from "node:path";
|
|
2412
|
+
|
|
2413
|
+
const projectRoot = ${JSON.stringify(projectRoot)};
|
|
2414
|
+
const urls = ${JSON.stringify(urls)};
|
|
2415
|
+
const routeHashMap = ${JSON.stringify(routeHashMap)};
|
|
2416
|
+
|
|
2417
|
+
// DJB2 hash matching the runtime param-hash utility
|
|
2418
|
+
function djb2Hex(str) {
|
|
2419
|
+
let hash = 5381;
|
|
2420
|
+
for (let i = 0; i < str.length; i++) {
|
|
2421
|
+
hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0;
|
|
2422
|
+
}
|
|
2423
|
+
return hash.toString(16).padStart(8, "0");
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
function hashParams(params) {
|
|
2427
|
+
const entries = Object.entries(params);
|
|
2428
|
+
if (entries.length === 0) return "_";
|
|
2429
|
+
const sorted = entries.sort(([a], [b]) => a.localeCompare(b));
|
|
2430
|
+
const str = sorted.map(([k, v]) => k + "=" + v).join("&");
|
|
2431
|
+
return djb2Hex(str);
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
// Extract params from a URL by matching against the route pattern.
|
|
2435
|
+
// The route pattern uses :paramName syntax.
|
|
2436
|
+
function extractParams(urlPath, pattern) {
|
|
2437
|
+
const urlParts = urlPath.split("/").filter(Boolean);
|
|
2438
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
2439
|
+
const params = {};
|
|
2440
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
2441
|
+
if (patternParts[i].startsWith(":")) {
|
|
2442
|
+
const paramName = patternParts[i].slice(1);
|
|
2443
|
+
params[paramName] = decodeURIComponent(urlParts[i] || "");
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
return params;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
// Mock workerd globals (bundled worker accesses globalThis.Cloudflare.compatibilityFlags)
|
|
2450
|
+
globalThis.Cloudflare = { compatibilityFlags: { enable_nodejs_process_v2: false } };
|
|
2451
|
+
|
|
2452
|
+
// Create symlinks for project root directories under dist/ so that relative
|
|
2453
|
+
// paths from import.meta.dirname (dist/rsc/assets/) resolve correctly.
|
|
2454
|
+
const symlinks = [];
|
|
2455
|
+
try {
|
|
2456
|
+
for (const entry of readdirSync(projectRoot)) {
|
|
2457
|
+
if (entry === "dist" || entry === "node_modules" || entry.startsWith(".")) continue;
|
|
2458
|
+
const target = resolve(projectRoot, entry);
|
|
2459
|
+
const link = resolve(projectRoot, "dist", entry);
|
|
2460
|
+
try {
|
|
2461
|
+
if (!existsSync(link) && statSync(target).isDirectory()) {
|
|
2462
|
+
symlinkSync(target, link);
|
|
2463
|
+
symlinks.push(link);
|
|
2464
|
+
}
|
|
2465
|
+
} catch {}
|
|
2466
|
+
}
|
|
2467
|
+
} catch {}
|
|
2468
|
+
|
|
2469
|
+
const mockEnv = new Proxy({}, {
|
|
2470
|
+
get(_, prop) {
|
|
2471
|
+
if (prop === "toString" || prop === Symbol.toPrimitive) return () => "[PrerenderEnv]";
|
|
2472
|
+
if (prop === Symbol.toStringTag) return "PrerenderEnv";
|
|
2473
|
+
if (prop === "Variables") return {};
|
|
2474
|
+
if (prop === "ASSETS") return { fetch: () => new Response("", { status: 404 }) };
|
|
2475
|
+
throw new Error("Cloudflare binding \\"" + String(prop) + "\\" not available in prerender");
|
|
2476
|
+
},
|
|
2477
|
+
});
|
|
2478
|
+
const mockCtx = { waitUntil: () => {}, passThroughOnException: () => {} };
|
|
2479
|
+
|
|
2480
|
+
try {
|
|
2481
|
+
const mod = await import(resolve(projectRoot, "dist/rsc/index.js"));
|
|
2482
|
+
const worker = mod.default;
|
|
2483
|
+
if (!worker?.fetch) {
|
|
2484
|
+
console.warn("[rsc-router] Built worker has no fetch handler, skipping pre-render");
|
|
2485
|
+
process.exit(0);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
let rendered = 0;
|
|
2489
|
+
for (const urlPath of urls) {
|
|
2490
|
+
try {
|
|
2491
|
+
// Collect serialized segments for this route
|
|
2492
|
+
const response = await worker.fetch(
|
|
2493
|
+
new Request("http://localhost" + urlPath + "?__no_cache&__prerender_collect", {
|
|
2494
|
+
headers: { Accept: "text/html" },
|
|
2495
|
+
}),
|
|
2496
|
+
mockEnv,
|
|
2497
|
+
mockCtx,
|
|
2498
|
+
);
|
|
2499
|
+
if (response.status !== 200) {
|
|
2500
|
+
console.warn("[rsc-router] Pre-render collect " + urlPath + " returned " + response.status + ", skipping");
|
|
2501
|
+
continue;
|
|
2502
|
+
}
|
|
2503
|
+
const data = await response.json();
|
|
2504
|
+
const { segments, handles, routeName } = data;
|
|
2505
|
+
if (!routeName || !segments) {
|
|
2506
|
+
console.warn("[rsc-router] Pre-render collect " + urlPath + " missing routeName or segments, skipping");
|
|
2507
|
+
continue;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
const routerHash = routeHashMap[routeName];
|
|
2511
|
+
if (!routerHash) {
|
|
2512
|
+
console.warn("[rsc-router] No router hash for route " + routeName + ", skipping");
|
|
2513
|
+
continue;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
// Compute param hash from the matched route params
|
|
2517
|
+
// The response carries routeName; we compute params from the URL
|
|
2518
|
+
// using the route manifest pattern. For static routes, paramHash is "_".
|
|
2519
|
+
const paramHash = hashParams(data.params || {});
|
|
2520
|
+
|
|
2521
|
+
// Write .flight file
|
|
2522
|
+
const flightDir = resolve(projectRoot, "dist", "static",
|
|
2523
|
+
"__" + routerHash, "prerender", routeName);
|
|
2524
|
+
mkdirSync(flightDir, { recursive: true });
|
|
2525
|
+
const flightPath = resolve(flightDir, paramHash + ".flight");
|
|
2526
|
+
writeFileSync(flightPath, JSON.stringify({ segments, handles }));
|
|
2527
|
+
|
|
2528
|
+
rendered++;
|
|
2529
|
+
console.log("[rsc-router] Pre-rendered: " + routeName + " (" + urlPath + ") -> " + paramHash + ".flight");
|
|
2530
|
+
} catch (err) {
|
|
2531
|
+
console.warn("[rsc-router] Pre-render failed for " + urlPath + ": " + err.message);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
if (rendered > 0) {
|
|
2536
|
+
console.log("[rsc-router] Pre-rendered " + rendered + "/" + urls.length + " route(s) to dist/static/");
|
|
2537
|
+
}
|
|
2538
|
+
} finally {
|
|
2539
|
+
for (const link of symlinks) {
|
|
2540
|
+
try { if (lstatSync(link).isSymbolicLink()) rmSync(link); } catch {}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
`.trim();
|
|
2544
|
+
}
|
|
2545
|
+
var VIRTUAL_ROUTES_MANIFEST_ID = "virtual:rsc-router/routes-manifest";
|
|
2546
|
+
function resolveDiscoveryEntryPath(options, routerPath) {
|
|
2547
|
+
if (options.preset === "cloudflare") {
|
|
2548
|
+
const wranglerPaths = ["wrangler.json", "wrangler.jsonc"];
|
|
2549
|
+
for (const filename of wranglerPaths) {
|
|
2550
|
+
if (existsSync3(filename)) {
|
|
2551
|
+
try {
|
|
2552
|
+
const raw = readFileSync2(filename, "utf-8");
|
|
2553
|
+
const cleaned = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
2554
|
+
const config = JSON.parse(cleaned);
|
|
2555
|
+
if (config.main) {
|
|
2556
|
+
return config.main;
|
|
2557
|
+
}
|
|
2558
|
+
} catch {
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
return void 0;
|
|
2563
|
+
}
|
|
2564
|
+
return routerPath;
|
|
2565
|
+
}
|
|
2566
|
+
function createVirtualStubPlugin() {
|
|
2567
|
+
const STUB_PREFIXES = [
|
|
2568
|
+
"virtual:rsc-router/",
|
|
2569
|
+
"virtual:entry-",
|
|
2570
|
+
"virtual:vite-rsc/"
|
|
2571
|
+
];
|
|
2572
|
+
return {
|
|
2573
|
+
name: "@rangojs/router:virtual-stubs",
|
|
2574
|
+
resolveId(id) {
|
|
2575
|
+
if (STUB_PREFIXES.some((p) => id.startsWith(p))) {
|
|
2576
|
+
return "\0stub:" + id;
|
|
2577
|
+
}
|
|
2578
|
+
return null;
|
|
2579
|
+
},
|
|
2580
|
+
load(id) {
|
|
2581
|
+
if (id.startsWith("\0stub:")) {
|
|
2582
|
+
return "export default {}";
|
|
2583
|
+
}
|
|
2584
|
+
return null;
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
function hashRouterId(id) {
|
|
2589
|
+
return createHash("sha256").update(id).digest("hex").slice(0, 12);
|
|
2590
|
+
}
|
|
2591
|
+
function createVersionInjectorPlugin(rscEntryPath) {
|
|
2592
|
+
let projectRoot = "";
|
|
2593
|
+
let resolvedEntryPath = "";
|
|
2594
|
+
return {
|
|
2595
|
+
name: "@rangojs/router:version-injector",
|
|
2596
|
+
enforce: "pre",
|
|
2597
|
+
configResolved(config) {
|
|
2598
|
+
projectRoot = config.root;
|
|
2599
|
+
resolvedEntryPath = resolve3(projectRoot, rscEntryPath);
|
|
2600
|
+
},
|
|
2601
|
+
transform(code, id) {
|
|
2602
|
+
const normalizedId = Vite.normalizePath(id);
|
|
2603
|
+
const normalizedEntry = Vite.normalizePath(resolvedEntryPath);
|
|
2604
|
+
if (normalizedId !== normalizedEntry) {
|
|
2605
|
+
return null;
|
|
2606
|
+
}
|
|
2607
|
+
const prepend = [];
|
|
2608
|
+
let newCode = code;
|
|
2609
|
+
if (!code.includes("virtual:rsc-router/routes-manifest")) {
|
|
2610
|
+
prepend.push(`import "virtual:rsc-router/routes-manifest";`);
|
|
2611
|
+
}
|
|
2612
|
+
const needsVersion = code.includes("createRSCHandler") && !code.includes("@rangojs/router:version") && /createRSCHandler\s*\(\s*\{/.test(code);
|
|
2613
|
+
if (needsVersion) {
|
|
2614
|
+
prepend.push(`import { VERSION } from "@rangojs/router:version";`);
|
|
2615
|
+
newCode = newCode.replace(
|
|
2616
|
+
/createRSCHandler\s*\(\s*\{/,
|
|
2617
|
+
"createRSCHandler({\n version: VERSION,"
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2620
|
+
if (prepend.length === 0 && newCode === code) return null;
|
|
2621
|
+
newCode = prepend.join("\n") + (prepend.length > 0 ? "\n" : "") + newCode;
|
|
2622
|
+
return {
|
|
2623
|
+
code: newCode,
|
|
2624
|
+
map: null
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
var _require = createRequire(import.meta.url);
|
|
2630
|
+
var _rangoVersion = _require("../../package.json").version;
|
|
2631
|
+
var _bannerPrinted = false;
|
|
2632
|
+
function printBanner(mode, preset, version) {
|
|
2633
|
+
if (_bannerPrinted) return;
|
|
2634
|
+
_bannerPrinted = true;
|
|
2635
|
+
const dim = "\x1B[2m";
|
|
2636
|
+
const bold = "\x1B[1m";
|
|
2637
|
+
const reset = "\x1B[0m";
|
|
2638
|
+
const banner = `
|
|
2639
|
+
${dim} \u2726 \u2726 \u2727. . .${reset}
|
|
2640
|
+
${dim} \u2571${reset} ${bold}\u2554\u2550\u2557${reset}${dim} * \u2571 \u2726 *${reset}
|
|
2641
|
+
${dim} ${reset}${bold}\u2551 \u2551${reset} ${bold}\u2554\u2550\u2557${reset}${dim} * \u2727. \u2571${reset}
|
|
2642
|
+
${dim} ${reset}${bold}\u2554\u2557 \u2551 \u2551 \u2551 \u2551${reset}${dim} * \u2571${reset}
|
|
2643
|
+
${dim} ${reset}${bold}\u2551\u2551 \u2551 \u2551 \u2551 \u2551 \u2566\u2550\u2557\u2554\u2550\u2557\u2554\u2557\u2554\u2554\u2550\u2557\u2554\u2550\u2557${reset}${dim} \u2727 \u2726${reset}
|
|
2644
|
+
${dim} ${reset}${bold}\u2550\u2563\u2551 \u2551 \u2560\u2550\u255D \u2551 \u2560\u2566\u255D\u2560\u2550\u2563\u2551\u2551\u2551\u2551 \u2566\u2551 \u2551${reset}${dim} * \u2727${reset}
|
|
2645
|
+
${dim} ${reset}${bold}\u2551\u255A\u2550\u255D \u2554\u2550\u2550\u2550\u255D \u2569\u255A\u2550\u2569 \u2569\u255D\u255A\u255D\u255A\u2550\u255D\u255A\u2550\u255D${reset}${dim} \u2726 . *${reset}
|
|
2646
|
+
${dim} ${reset}${bold}\u255A\u2550\u2550\u2557 \u2551${reset}${dim} * RSC Wrangler \u2727 \u2726${reset}
|
|
2647
|
+
${dim} * ${reset}${bold}\u2551 \u2560\u2550${reset}${dim} * \u2727. \u2571${reset}
|
|
2648
|
+
${bold}\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2569\u2550\u2550\u2550${reset}${dim} \u2726 *${reset}
|
|
2649
|
+
|
|
2650
|
+
v${version} \xB7 ${preset} \xB7 ${mode}
|
|
2651
|
+
`;
|
|
2652
|
+
console.log(banner);
|
|
2653
|
+
}
|
|
2654
|
+
async function rango(options) {
|
|
2655
|
+
const resolvedOptions = options ?? { preset: "node" };
|
|
2656
|
+
const preset = resolvedOptions.preset ?? "node";
|
|
2657
|
+
const showBanner = resolvedOptions.banner ?? true;
|
|
2658
|
+
const plugins = [];
|
|
2659
|
+
const rangoAliases = getPackageAliases();
|
|
2660
|
+
const excludeDeps = getExcludeDeps();
|
|
2661
|
+
let rscEntryPath = null;
|
|
2662
|
+
let routerPath;
|
|
2663
|
+
const prerenderEnabled = preset === "cloudflare";
|
|
2664
|
+
if (preset === "cloudflare") {
|
|
2665
|
+
const { default: rsc } = await import("@vitejs/plugin-rsc");
|
|
2666
|
+
const finalEntries = {
|
|
2667
|
+
client: VIRTUAL_IDS.browser,
|
|
2668
|
+
ssr: VIRTUAL_IDS.ssr
|
|
2669
|
+
};
|
|
2670
|
+
const cfApi = { handlerChunkInfo: null };
|
|
2671
|
+
let resolvedPrerenderModules;
|
|
2672
|
+
plugins.push({
|
|
2673
|
+
name: "@rangojs/router:cloudflare-integration",
|
|
2674
|
+
enforce: "pre",
|
|
2675
|
+
api: cfApi,
|
|
2676
|
+
config() {
|
|
2677
|
+
return {
|
|
2678
|
+
// Exclude rsc-router modules from optimization to prevent module duplication
|
|
2679
|
+
// This ensures the same Context instance is used by both browser entry and RSC proxy modules
|
|
2680
|
+
optimizeDeps: {
|
|
2681
|
+
exclude: excludeDeps,
|
|
2682
|
+
esbuildOptions: sharedEsbuildOptions
|
|
2683
|
+
},
|
|
2684
|
+
resolve: {
|
|
2685
|
+
alias: rangoAliases
|
|
2686
|
+
},
|
|
2687
|
+
environments: {
|
|
2688
|
+
client: {
|
|
2689
|
+
build: {
|
|
2690
|
+
rollupOptions: {
|
|
2691
|
+
output: {
|
|
2692
|
+
manualChunks: getManualChunks
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
},
|
|
2696
|
+
// Pre-bundle rsc-html-stream to prevent discovery during first request
|
|
2697
|
+
// Exclude rsc-router modules to ensure same Context instance
|
|
2698
|
+
optimizeDeps: {
|
|
2699
|
+
include: ["rsc-html-stream/client"],
|
|
2700
|
+
exclude: excludeDeps,
|
|
2701
|
+
esbuildOptions: sharedEsbuildOptions
|
|
2702
|
+
}
|
|
2703
|
+
},
|
|
2704
|
+
ssr: {
|
|
2705
|
+
// Build SSR inside RSC directory so wrangler can deploy self-contained dist/rsc
|
|
2706
|
+
build: {
|
|
2707
|
+
outDir: "./dist/rsc/ssr"
|
|
2708
|
+
},
|
|
2709
|
+
resolve: {
|
|
2710
|
+
// Ensure single React instance in SSR child environment
|
|
2711
|
+
dedupe: ["react", "react-dom"]
|
|
2712
|
+
},
|
|
2713
|
+
// Pre-bundle SSR entry and React for proper module linking with childEnvironments
|
|
2714
|
+
// All deps must be listed to avoid late discovery triggering ERR_OUTDATED_OPTIMIZED_DEP
|
|
2715
|
+
optimizeDeps: {
|
|
2716
|
+
entries: [finalEntries.ssr],
|
|
2717
|
+
include: [
|
|
2718
|
+
"react",
|
|
2719
|
+
"react-dom",
|
|
2720
|
+
"react-dom/server.edge",
|
|
2721
|
+
"react-dom/static.edge",
|
|
2722
|
+
"react/jsx-runtime",
|
|
2723
|
+
"react/jsx-dev-runtime",
|
|
2724
|
+
"rsc-html-stream/server",
|
|
2725
|
+
"@vitejs/plugin-rsc/vendor/react-server-dom/client.edge"
|
|
2726
|
+
],
|
|
2727
|
+
exclude: excludeDeps,
|
|
2728
|
+
esbuildOptions: sharedEsbuildOptions
|
|
2729
|
+
}
|
|
2730
|
+
},
|
|
2731
|
+
rsc: {
|
|
2732
|
+
build: {
|
|
2733
|
+
rollupOptions: {
|
|
2734
|
+
output: {
|
|
2735
|
+
manualChunks(id) {
|
|
2736
|
+
if (resolvedPrerenderModules?.has(id)) {
|
|
2737
|
+
return "__prerender-handlers";
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
},
|
|
2743
|
+
// RSC environment needs exclude list and esbuild options
|
|
2744
|
+
// Exclude rsc-router modules to prevent createContext in RSC environment
|
|
2745
|
+
optimizeDeps: {
|
|
2746
|
+
exclude: excludeDeps,
|
|
2747
|
+
esbuildOptions: sharedEsbuildOptions
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
};
|
|
2752
|
+
},
|
|
2753
|
+
configResolved(config) {
|
|
2754
|
+
if (showBanner) {
|
|
2755
|
+
const mode = config.command === "serve" ? process.argv.includes("preview") ? "preview" : "dev" : "build";
|
|
2756
|
+
printBanner(mode, "cloudflare", _rangoVersion);
|
|
2757
|
+
}
|
|
2758
|
+
const prerenderPlugin = config.plugins.find(
|
|
2759
|
+
(p) => p.name === "@rangojs/router:expose-prerender-handler-id"
|
|
2760
|
+
);
|
|
2761
|
+
resolvedPrerenderModules = prerenderPlugin?.api?.prerenderHandlerModules;
|
|
2762
|
+
},
|
|
2763
|
+
// Record handler chunk metadata during RSC build for post-prerender replacement.
|
|
2764
|
+
// Rollup minifies EXPORT names (e.g. ArticlesIndex -> r) but keeps internal
|
|
2765
|
+
// variable names intact. We search for original names from prerenderHandlerModules.
|
|
2766
|
+
generateBundle(_options, bundle) {
|
|
2767
|
+
if (this.environment?.name !== "rsc") return;
|
|
2768
|
+
if (!resolvedPrerenderModules?.size) return;
|
|
2769
|
+
for (const [fileName, chunk] of Object.entries(bundle)) {
|
|
2770
|
+
if (chunk.type !== "chunk") continue;
|
|
2771
|
+
if (!fileName.includes("__prerender-handlers")) continue;
|
|
2772
|
+
const handlers = [];
|
|
2773
|
+
for (const [, handlerNames] of resolvedPrerenderModules) {
|
|
2774
|
+
for (const name of handlerNames) {
|
|
2775
|
+
const idPattern = new RegExp(
|
|
2776
|
+
`\\b${name}\\.\\$\\$id\\s*=\\s*"([^"]+)"`
|
|
2777
|
+
);
|
|
2778
|
+
const match = chunk.code.match(idPattern);
|
|
2779
|
+
if (match) {
|
|
2780
|
+
const callStartRe = new RegExp(
|
|
2781
|
+
`const\\s+${name}\\s*=\\s*createPrerenderHandler\\s*(?:<[^>]*>)?\\s*\\(`
|
|
2782
|
+
);
|
|
2783
|
+
const callStart = callStartRe.exec(chunk.code);
|
|
2784
|
+
let isPassthrough = false;
|
|
2785
|
+
if (callStart) {
|
|
2786
|
+
const openPos = callStart.index + callStart[0].length;
|
|
2787
|
+
let depth = 1;
|
|
2788
|
+
let p = openPos;
|
|
2789
|
+
while (p < chunk.code.length && depth > 0) {
|
|
2790
|
+
const ch = chunk.code[p];
|
|
2791
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
2792
|
+
p++;
|
|
2793
|
+
while (p < chunk.code.length && chunk.code[p] !== ch) {
|
|
2794
|
+
if (chunk.code[p] === "\\") p++;
|
|
2795
|
+
p++;
|
|
2796
|
+
}
|
|
2797
|
+
} else if (ch === "(") {
|
|
2798
|
+
depth++;
|
|
2799
|
+
} else if (ch === ")") {
|
|
2800
|
+
depth--;
|
|
2801
|
+
}
|
|
2802
|
+
p++;
|
|
2803
|
+
}
|
|
2804
|
+
if (depth === 0) {
|
|
2805
|
+
const callBody = chunk.code.slice(callStart.index, p);
|
|
2806
|
+
isPassthrough = /passthrough\s*:\s*(!0|true)/.test(callBody);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
handlers.push({ name, handlerId: match[1], passthrough: isPassthrough });
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
if (handlers.length > 0) {
|
|
2814
|
+
cfApi.handlerChunkInfo = { fileName, exports: handlers };
|
|
2815
|
+
}
|
|
2816
|
+
break;
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
});
|
|
2820
|
+
plugins.push(createVirtualEntriesPlugin(finalEntries));
|
|
2821
|
+
plugins.push(
|
|
2822
|
+
rsc({
|
|
2823
|
+
get entries() {
|
|
2824
|
+
return finalEntries;
|
|
2825
|
+
},
|
|
2826
|
+
serverHandler: false
|
|
2827
|
+
})
|
|
2828
|
+
);
|
|
2829
|
+
} else {
|
|
2830
|
+
const nodeOptions = resolvedOptions;
|
|
2831
|
+
routerPath = nodeOptions.router;
|
|
2832
|
+
if (!routerPath) {
|
|
2833
|
+
const earlyFilter = createScanFilter(process.cwd(), {
|
|
2834
|
+
include: resolvedOptions.include,
|
|
2835
|
+
exclude: resolvedOptions.exclude
|
|
2836
|
+
});
|
|
2837
|
+
const candidates = findRouterFiles(process.cwd(), earlyFilter);
|
|
2838
|
+
if (candidates.length === 1) {
|
|
2839
|
+
const abs = candidates[0];
|
|
2840
|
+
const rel = abs.startsWith(process.cwd()) ? "./" + abs.slice(process.cwd().length + 1) : abs;
|
|
2841
|
+
routerPath = rel;
|
|
2842
|
+
} else if (candidates.length > 1) {
|
|
2843
|
+
const cwd = process.cwd();
|
|
2844
|
+
const list = candidates.map((f) => " - " + (f.startsWith(cwd) ? f.slice(cwd.length + 1) : f)).join("\n");
|
|
2845
|
+
throw new Error(
|
|
2846
|
+
`[rsc-router] Multiple routers found. Specify \`router\` to choose one:
|
|
2847
|
+
${list}`
|
|
2848
|
+
);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
const rscOption = nodeOptions.rsc ?? true;
|
|
2852
|
+
if (rscOption !== false) {
|
|
2853
|
+
const { default: rsc } = await import("@vitejs/plugin-rsc");
|
|
2854
|
+
const userEntries = typeof rscOption === "boolean" ? {} : rscOption.entries || {};
|
|
2855
|
+
const finalEntries = {
|
|
2856
|
+
client: userEntries.client ?? VIRTUAL_IDS.browser,
|
|
2857
|
+
ssr: userEntries.ssr ?? VIRTUAL_IDS.ssr,
|
|
2858
|
+
rsc: userEntries.rsc ?? VIRTUAL_IDS.rsc
|
|
2859
|
+
};
|
|
2860
|
+
rscEntryPath = userEntries.rsc ?? null;
|
|
2861
|
+
let hasWarnedDuplicate = false;
|
|
2862
|
+
plugins.push({
|
|
2863
|
+
name: "@rangojs/router:rsc-integration",
|
|
2864
|
+
enforce: "pre",
|
|
2865
|
+
config() {
|
|
2866
|
+
const useVirtualClient = finalEntries.client === VIRTUAL_IDS.browser;
|
|
2867
|
+
const useVirtualSSR = finalEntries.ssr === VIRTUAL_IDS.ssr;
|
|
2868
|
+
const useVirtualRSC = finalEntries.rsc === VIRTUAL_IDS.rsc;
|
|
2869
|
+
return {
|
|
2870
|
+
// Exclude rsc-router modules from optimization to prevent module duplication
|
|
2871
|
+
// This ensures the same Context instance is used by both browser entry and RSC proxy modules
|
|
2872
|
+
optimizeDeps: {
|
|
2873
|
+
exclude: excludeDeps,
|
|
2874
|
+
esbuildOptions: sharedEsbuildOptions
|
|
2875
|
+
},
|
|
2876
|
+
resolve: {
|
|
2877
|
+
alias: rangoAliases
|
|
2878
|
+
},
|
|
2879
|
+
environments: {
|
|
2880
|
+
client: {
|
|
2881
|
+
build: {
|
|
2882
|
+
rollupOptions: {
|
|
2883
|
+
output: {
|
|
2884
|
+
manualChunks: getManualChunks
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
},
|
|
2888
|
+
// Always exclude rsc-router modules, conditionally add virtual entry
|
|
2889
|
+
optimizeDeps: {
|
|
2890
|
+
exclude: excludeDeps,
|
|
2891
|
+
esbuildOptions: sharedEsbuildOptions,
|
|
2892
|
+
...useVirtualClient && {
|
|
2893
|
+
// Tell Vite to scan the virtual entry for dependencies
|
|
2894
|
+
entries: [VIRTUAL_IDS.browser]
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
},
|
|
2898
|
+
...useVirtualSSR && {
|
|
2899
|
+
ssr: {
|
|
2900
|
+
optimizeDeps: {
|
|
2901
|
+
entries: [VIRTUAL_IDS.ssr],
|
|
2902
|
+
// Pre-bundle all SSR deps to prevent late discovery triggering ERR_OUTDATED_OPTIMIZED_DEP
|
|
2903
|
+
include: [
|
|
2904
|
+
"react",
|
|
2905
|
+
"react-dom",
|
|
2906
|
+
"react-dom/server.edge",
|
|
2907
|
+
"react-dom/static.edge",
|
|
2908
|
+
"react/jsx-runtime",
|
|
2909
|
+
"react/jsx-dev-runtime",
|
|
2910
|
+
"@vitejs/plugin-rsc/vendor/react-server-dom/client.edge"
|
|
2911
|
+
],
|
|
2912
|
+
exclude: excludeDeps,
|
|
2913
|
+
esbuildOptions: sharedEsbuildOptions
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
},
|
|
2917
|
+
...useVirtualRSC && {
|
|
2918
|
+
rsc: {
|
|
2919
|
+
optimizeDeps: {
|
|
2920
|
+
entries: [VIRTUAL_IDS.rsc],
|
|
2921
|
+
// Pre-bundle React for RSC to ensure single instance
|
|
2922
|
+
include: ["react", "react/jsx-runtime"],
|
|
2923
|
+
esbuildOptions: sharedEsbuildOptions
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
};
|
|
2929
|
+
},
|
|
2930
|
+
configResolved(config) {
|
|
2931
|
+
if (showBanner) {
|
|
2932
|
+
const mode = config.command === "serve" ? process.argv.includes("preview") ? "preview" : "dev" : "build";
|
|
2933
|
+
printBanner(mode, "node", _rangoVersion);
|
|
2934
|
+
}
|
|
2935
|
+
const rscMinimalCount = config.plugins.filter(
|
|
2936
|
+
(p) => p.name === "rsc:minimal"
|
|
2937
|
+
).length;
|
|
2938
|
+
if (rscMinimalCount > 1 && !hasWarnedDuplicate) {
|
|
2939
|
+
hasWarnedDuplicate = true;
|
|
2940
|
+
console.warn(
|
|
2941
|
+
"[rsc-router] Duplicate @vitejs/plugin-rsc detected. Remove rsc() from your config or use rango({ rsc: false }) for manual configuration."
|
|
2942
|
+
);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
});
|
|
2946
|
+
plugins.push(createVirtualEntriesPlugin(finalEntries, routerPath));
|
|
2947
|
+
plugins.push(
|
|
2948
|
+
rsc({
|
|
2949
|
+
entries: finalEntries
|
|
2950
|
+
})
|
|
2951
|
+
);
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
plugins.push(exposeActionId());
|
|
2955
|
+
plugins.push(exposeLoaderId());
|
|
2956
|
+
plugins.push(exposeHandleId());
|
|
2957
|
+
plugins.push(exposeLocationStateId());
|
|
2958
|
+
plugins.push(exposePrerenderHandlerId());
|
|
2959
|
+
plugins.push(createVersionPlugin());
|
|
2960
|
+
const discoveryEntryPath = resolveDiscoveryEntryPath(
|
|
2961
|
+
resolvedOptions,
|
|
2962
|
+
preset !== "cloudflare" ? routerPath : void 0
|
|
2963
|
+
);
|
|
2964
|
+
const injectorEntryPath = rscEntryPath ?? (preset === "cloudflare" ? discoveryEntryPath : null);
|
|
2965
|
+
if (injectorEntryPath) {
|
|
2966
|
+
plugins.push(createVersionInjectorPlugin(injectorEntryPath));
|
|
2967
|
+
}
|
|
2968
|
+
plugins.push(createCjsToEsmPlugin());
|
|
2969
|
+
if (discoveryEntryPath) {
|
|
2970
|
+
plugins.push(createRouterDiscoveryPlugin(discoveryEntryPath, {
|
|
2971
|
+
enableBuildPrerender: prerenderEnabled,
|
|
2972
|
+
staticRouteTypesGeneration: resolvedOptions.staticRouteTypesGeneration,
|
|
2973
|
+
include: resolvedOptions.include,
|
|
2974
|
+
exclude: resolvedOptions.exclude
|
|
2975
|
+
}));
|
|
2976
|
+
}
|
|
2977
|
+
return plugins;
|
|
2978
|
+
}
|
|
2979
|
+
function createCjsToEsmPlugin() {
|
|
2980
|
+
return {
|
|
2981
|
+
name: "@rangojs/router:cjs-to-esm",
|
|
2982
|
+
enforce: "pre",
|
|
2983
|
+
transform(code, id) {
|
|
2984
|
+
const cleanId = id.split("?")[0];
|
|
2985
|
+
if (cleanId.includes("vendor/react-server-dom/client.browser.js") || cleanId.includes("vendor\\react-server-dom\\client.browser.js")) {
|
|
2986
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
2987
|
+
const cjsFile = isProd ? "./cjs/react-server-dom-webpack-client.browser.production.js" : "./cjs/react-server-dom-webpack-client.browser.development.js";
|
|
2988
|
+
return {
|
|
2989
|
+
code: `export * from "${cjsFile}";`,
|
|
2990
|
+
map: null
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
if ((cleanId.includes("vendor/react-server-dom/cjs/") || cleanId.includes("vendor\\react-server-dom\\cjs\\")) && cleanId.includes("client.browser")) {
|
|
2994
|
+
let transformed = code;
|
|
2995
|
+
const licenseMatch = transformed.match(/^\/\*\*[\s\S]*?\*\//);
|
|
2996
|
+
const license = licenseMatch ? licenseMatch[0] : "";
|
|
2997
|
+
if (license) {
|
|
2998
|
+
transformed = transformed.slice(license.length);
|
|
2999
|
+
}
|
|
3000
|
+
transformed = transformed.replace(/^\s*["']use strict["'];\s*/, "");
|
|
3001
|
+
transformed = transformed.replace(
|
|
3002
|
+
/^\s*["']production["']\s*!==\s*process\.env\.NODE_ENV\s*&&\s*\(function\s*\(\)\s*\{/,
|
|
3003
|
+
""
|
|
3004
|
+
);
|
|
3005
|
+
transformed = transformed.replace(/\}\)\(\);?\s*$/, "");
|
|
3006
|
+
transformed = transformed.replace(
|
|
3007
|
+
/var\s+React\s*=\s*require\s*\(\s*["']react["']\s*\)\s*,[\s\n]+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
|
|
3008
|
+
'import React from "react";\nimport ReactDOM from "react-dom";\nvar '
|
|
3009
|
+
);
|
|
3010
|
+
transformed = transformed.replace(
|
|
3011
|
+
/var\s+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
|
|
3012
|
+
'import ReactDOM from "react-dom";\nvar '
|
|
3013
|
+
);
|
|
3014
|
+
transformed = transformed.replace(
|
|
3015
|
+
/exports\.(\w+)\s*=\s*function\s*\(/g,
|
|
3016
|
+
"export function $1("
|
|
3017
|
+
);
|
|
3018
|
+
transformed = transformed.replace(
|
|
3019
|
+
/exports\.(\w+)\s*=/g,
|
|
3020
|
+
"export const $1 ="
|
|
3021
|
+
);
|
|
3022
|
+
transformed = license + "\n" + transformed;
|
|
3023
|
+
return {
|
|
3024
|
+
code: transformed,
|
|
3025
|
+
map: null
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
return null;
|
|
3029
|
+
}
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
export {
|
|
3033
|
+
exposeActionId,
|
|
3034
|
+
exposeHandleId,
|
|
3035
|
+
exposeLoaderId,
|
|
3036
|
+
exposeLocationStateId,
|
|
3037
|
+
exposePrerenderHandlerId,
|
|
3038
|
+
rango
|
|
3039
|
+
};
|