@ivogt/rsc-router 0.0.0-experimental.7 → 0.0.0-experimental.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/expose-action-id.d.ts +18 -0
- package/dist/expose-handle-id.d.ts +19 -0
- package/dist/expose-loader-id.d.ts +16 -0
- package/dist/expose-location-state-id.d.ts +19 -0
- package/dist/index.d.ts +123 -0
- package/dist/package-resolution.d.ts +42 -0
- package/dist/virtual-entries.d.ts +24 -0
- package/dist/vite/index.js +1286 -0
- package/package.json +5 -2
- package/src/vite/index.ts +86 -0
- package/src/vite/package-resolution.ts +4 -31
|
@@ -0,0 +1,1286 @@
|
|
|
1
|
+
// src/vite/index.ts
|
|
2
|
+
import * as Vite from "vite";
|
|
3
|
+
import { resolve as resolve2 } from "node:path";
|
|
4
|
+
|
|
5
|
+
// src/vite/expose-action-id.ts
|
|
6
|
+
import MagicString from "magic-string";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
function getRscPluginApi(config) {
|
|
10
|
+
let plugin = config.plugins.find((p) => p.name === "rsc:minimal");
|
|
11
|
+
if (!plugin) {
|
|
12
|
+
plugin = config.plugins.find(
|
|
13
|
+
(p) => p.api?.manager?.serverReferenceMetaMap !== void 0
|
|
14
|
+
);
|
|
15
|
+
if (plugin) {
|
|
16
|
+
console.warn(
|
|
17
|
+
`[rsc-router:expose-action-id] RSC plugin found by API structure (name: "${plugin.name}"). Consider updating the name lookup if the plugin was renamed.`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return plugin?.api;
|
|
22
|
+
}
|
|
23
|
+
function normalizePath(p) {
|
|
24
|
+
return p.split(path.sep).join("/");
|
|
25
|
+
}
|
|
26
|
+
function isUseServerModule(filePath) {
|
|
27
|
+
try {
|
|
28
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
29
|
+
const trimmed = content.replace(/^\s*\/\/[^\n]*\n/gm, "").replace(/^\s*\/\*[\s\S]*?\*\/\s*/gm, "").trimStart();
|
|
30
|
+
return trimmed.startsWith('"use server"') || trimmed.startsWith("'use server'");
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function transformServerReferences(code, sourceId, hashToFileMap) {
|
|
36
|
+
if (!code.includes("createServerReference(")) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const pattern = /((?:\$\$\w+\.)?createServerReference)\(("[^"]+#[^"]+")([^)]*)\)/g;
|
|
40
|
+
const s = new MagicString(code);
|
|
41
|
+
let hasChanges = false;
|
|
42
|
+
let match;
|
|
43
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
44
|
+
hasChanges = true;
|
|
45
|
+
const [fullMatch, fnCall, idArg, rest] = match;
|
|
46
|
+
const start = match.index;
|
|
47
|
+
const end = start + fullMatch.length;
|
|
48
|
+
let finalIdArg = idArg;
|
|
49
|
+
if (hashToFileMap) {
|
|
50
|
+
const idValue = idArg.slice(1, -1);
|
|
51
|
+
const hashMatch = idValue.match(/^([^#]+)#(.+)$/);
|
|
52
|
+
if (hashMatch) {
|
|
53
|
+
const [, hash, actionName] = hashMatch;
|
|
54
|
+
const filePath = hashToFileMap.get(hash);
|
|
55
|
+
if (filePath) {
|
|
56
|
+
finalIdArg = `"${filePath}#${actionName}"`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const replacement = `(function(fn) { fn.$$id = ${finalIdArg}; return fn; })(${fnCall}(${idArg}${rest}))`;
|
|
61
|
+
s.overwrite(start, end, replacement);
|
|
62
|
+
}
|
|
63
|
+
if (!hasChanges) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
code: s.toString(),
|
|
68
|
+
map: s.generateMap({ source: sourceId, includeContent: true })
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function transformRegisterServerReference(code, sourceId, hashToFileMap) {
|
|
72
|
+
if (!hashToFileMap || !code.includes("registerServerReference(")) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const pattern = /registerServerReference\(([^,]+),\s*"([^"]+)",\s*"([^"]+)"\)/g;
|
|
76
|
+
const s = new MagicString(code);
|
|
77
|
+
let hasChanges = false;
|
|
78
|
+
let match;
|
|
79
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
80
|
+
const [fullMatch, fnArg, hash, exportName] = match;
|
|
81
|
+
const start = match.index;
|
|
82
|
+
const end = start + fullMatch.length;
|
|
83
|
+
const filePath = hashToFileMap.get(hash);
|
|
84
|
+
if (filePath) {
|
|
85
|
+
hasChanges = true;
|
|
86
|
+
const filePathId = `${filePath}#${exportName}`;
|
|
87
|
+
const replacement = `(function(fn) { fn.$id = "${filePathId}"; return fn; })(registerServerReference(${fnArg}, "${hash}", "${exportName}"))`;
|
|
88
|
+
s.overwrite(start, end, replacement);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!hasChanges) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
code: s.toString(),
|
|
96
|
+
map: s.generateMap({ source: sourceId, includeContent: true })
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function exposeActionId() {
|
|
100
|
+
let config;
|
|
101
|
+
let isBuild = false;
|
|
102
|
+
let hashToFileMap;
|
|
103
|
+
let rscPluginApi;
|
|
104
|
+
return {
|
|
105
|
+
name: "rsc-router:expose-action-id",
|
|
106
|
+
// Run after all other plugins (including RSC plugin's transforms)
|
|
107
|
+
enforce: "post",
|
|
108
|
+
configResolved(resolvedConfig) {
|
|
109
|
+
config = resolvedConfig;
|
|
110
|
+
isBuild = config.command === "build";
|
|
111
|
+
rscPluginApi = getRscPluginApi(config);
|
|
112
|
+
},
|
|
113
|
+
buildStart() {
|
|
114
|
+
if (!rscPluginApi) {
|
|
115
|
+
rscPluginApi = getRscPluginApi(config);
|
|
116
|
+
}
|
|
117
|
+
if (!rscPluginApi) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
"[rsc-router] Could not find @vitejs/plugin-rsc. rsc-router requires the Vite RSC plugin.\nThe RSC plugin should be included automatically. If you disabled it with\nrscRouter({ rsc: false }), add rsc() before rscRouter() in your config."
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (!isBuild) return;
|
|
123
|
+
hashToFileMap = /* @__PURE__ */ new Map();
|
|
124
|
+
const { serverReferenceMetaMap } = rscPluginApi.manager;
|
|
125
|
+
for (const [absolutePath, meta] of Object.entries(
|
|
126
|
+
serverReferenceMetaMap
|
|
127
|
+
)) {
|
|
128
|
+
if (!isUseServerModule(absolutePath)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const relativePath = normalizePath(
|
|
132
|
+
path.relative(config.root, absolutePath)
|
|
133
|
+
);
|
|
134
|
+
hashToFileMap.set(meta.referenceKey, relativePath);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
// Dev mode only: transform hook runs after RSC plugin creates server references
|
|
138
|
+
// In dev mode, IDs already contain file paths, not hashes
|
|
139
|
+
transform(code, id) {
|
|
140
|
+
if (isBuild) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (!code.includes("createServerReference(")) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (id.includes("/node_modules/")) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
return transformServerReferences(code, id);
|
|
150
|
+
},
|
|
151
|
+
// Build mode: renderChunk runs after all transforms and bundling complete
|
|
152
|
+
renderChunk(code, chunk) {
|
|
153
|
+
const isRscEnv = this.environment?.name === "rsc";
|
|
154
|
+
const effectiveMap = isRscEnv ? hashToFileMap : void 0;
|
|
155
|
+
const result = transformServerReferences(
|
|
156
|
+
code,
|
|
157
|
+
chunk.fileName,
|
|
158
|
+
effectiveMap
|
|
159
|
+
);
|
|
160
|
+
if (isRscEnv && hashToFileMap) {
|
|
161
|
+
const codeToTransform = result ? result.code : code;
|
|
162
|
+
const registerResult = transformRegisterServerReference(
|
|
163
|
+
codeToTransform,
|
|
164
|
+
chunk.fileName,
|
|
165
|
+
hashToFileMap
|
|
166
|
+
);
|
|
167
|
+
if (registerResult) {
|
|
168
|
+
return { code: registerResult.code, map: registerResult.map };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (result) {
|
|
172
|
+
return { code: result.code, map: result.map };
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/vite/expose-loader-id.ts
|
|
180
|
+
import MagicString2 from "magic-string";
|
|
181
|
+
import path2 from "node:path";
|
|
182
|
+
import crypto from "node:crypto";
|
|
183
|
+
function normalizePath2(p) {
|
|
184
|
+
return p.split(path2.sep).join("/");
|
|
185
|
+
}
|
|
186
|
+
function hashLoaderId(filePath, exportName) {
|
|
187
|
+
const input = `${filePath}#${exportName}`;
|
|
188
|
+
const hash = crypto.createHash("sha256").update(input).digest("hex");
|
|
189
|
+
return `${hash.slice(0, 8)}#${exportName}`;
|
|
190
|
+
}
|
|
191
|
+
function hasCreateLoaderImport(code) {
|
|
192
|
+
const pattern = /import\s*\{[^}]*\bcreateLoader\b[^}]*\}\s*from\s*["']rsc-router(?:\/server)?["']/;
|
|
193
|
+
return pattern.test(code);
|
|
194
|
+
}
|
|
195
|
+
function countCreateLoaderArgs(code, startPos, endPos) {
|
|
196
|
+
let depth = 0;
|
|
197
|
+
let argCount = 0;
|
|
198
|
+
let hasContent = false;
|
|
199
|
+
for (let i = startPos; i < endPos; i++) {
|
|
200
|
+
const char = code[i];
|
|
201
|
+
if (char === "(" || char === "[" || char === "{") {
|
|
202
|
+
depth++;
|
|
203
|
+
hasContent = true;
|
|
204
|
+
} else if (char === ")" || char === "]" || char === "}") {
|
|
205
|
+
depth--;
|
|
206
|
+
} else if (char === "," && depth === 0) {
|
|
207
|
+
argCount++;
|
|
208
|
+
} else if (!/\s/.test(char)) {
|
|
209
|
+
hasContent = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return hasContent ? argCount + 1 : 0;
|
|
213
|
+
}
|
|
214
|
+
function transformLoaderExports(code, filePath, sourceId, isBuild = false) {
|
|
215
|
+
if (!code.includes("createLoader")) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
if (!hasCreateLoaderImport(code)) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const pattern = /export\s+const\s+(\w+)\s*=\s*createLoader\s*\(/g;
|
|
222
|
+
const s = new MagicString2(code);
|
|
223
|
+
let hasChanges = false;
|
|
224
|
+
let match;
|
|
225
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
226
|
+
const exportName = match[1];
|
|
227
|
+
const matchEnd = match.index + match[0].length;
|
|
228
|
+
let parenDepth = 1;
|
|
229
|
+
let i = matchEnd;
|
|
230
|
+
while (i < code.length && parenDepth > 0) {
|
|
231
|
+
if (code[i] === "(") parenDepth++;
|
|
232
|
+
if (code[i] === ")") parenDepth--;
|
|
233
|
+
i++;
|
|
234
|
+
}
|
|
235
|
+
const closeParenPos = i - 1;
|
|
236
|
+
const argCount = countCreateLoaderArgs(code, matchEnd, closeParenPos);
|
|
237
|
+
let statementEnd = i;
|
|
238
|
+
while (statementEnd < code.length && /\s/.test(code[statementEnd])) {
|
|
239
|
+
statementEnd++;
|
|
240
|
+
}
|
|
241
|
+
if (code[statementEnd] === ";") {
|
|
242
|
+
statementEnd++;
|
|
243
|
+
}
|
|
244
|
+
const loaderId = isBuild ? hashLoaderId(filePath, exportName) : `${filePath}#${exportName}`;
|
|
245
|
+
const paramInjection = argCount === 1 ? `, undefined, "${loaderId}"` : `, "${loaderId}"`;
|
|
246
|
+
s.appendLeft(closeParenPos, paramInjection);
|
|
247
|
+
const propInjection = `
|
|
248
|
+
${exportName}.$$id = "${loaderId}";`;
|
|
249
|
+
s.appendRight(statementEnd, propInjection);
|
|
250
|
+
hasChanges = true;
|
|
251
|
+
}
|
|
252
|
+
if (!hasChanges) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
code: s.toString(),
|
|
257
|
+
map: s.generateMap({ source: sourceId, includeContent: true })
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
var VIRTUAL_LOADER_MANIFEST = "virtual:rsc-router/loader-manifest";
|
|
261
|
+
var RESOLVED_VIRTUAL_LOADER_MANIFEST = "\0" + VIRTUAL_LOADER_MANIFEST;
|
|
262
|
+
function exposeLoaderId() {
|
|
263
|
+
let config;
|
|
264
|
+
let isBuild = false;
|
|
265
|
+
const loaderRegistry = /* @__PURE__ */ new Map();
|
|
266
|
+
const pendingLoaderScans = /* @__PURE__ */ new Map();
|
|
267
|
+
return {
|
|
268
|
+
name: "rsc-router:expose-loader-id",
|
|
269
|
+
enforce: "post",
|
|
270
|
+
configResolved(resolvedConfig) {
|
|
271
|
+
config = resolvedConfig;
|
|
272
|
+
isBuild = config.command === "build";
|
|
273
|
+
},
|
|
274
|
+
async buildStart() {
|
|
275
|
+
if (!isBuild) return;
|
|
276
|
+
const fs2 = await import("node:fs/promises");
|
|
277
|
+
async function scanDir(dir) {
|
|
278
|
+
const results = [];
|
|
279
|
+
try {
|
|
280
|
+
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
281
|
+
for (const entry of entries) {
|
|
282
|
+
const fullPath = path2.join(dir, entry.name);
|
|
283
|
+
if (entry.isDirectory()) {
|
|
284
|
+
if (entry.name !== "node_modules") {
|
|
285
|
+
results.push(...await scanDir(fullPath));
|
|
286
|
+
}
|
|
287
|
+
} else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|
|
288
|
+
results.push(fullPath);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
return results;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const srcDir = path2.join(config.root, "src");
|
|
297
|
+
const files = await scanDir(srcDir);
|
|
298
|
+
for (const filePath of files) {
|
|
299
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
300
|
+
if (!content.includes("createLoader")) continue;
|
|
301
|
+
if (!hasCreateLoaderImport(content)) continue;
|
|
302
|
+
const pattern = /export\s+const\s+(\w+)\s*=\s*createLoader\s*\(/g;
|
|
303
|
+
const relativePath = normalizePath2(
|
|
304
|
+
path2.relative(config.root, filePath)
|
|
305
|
+
);
|
|
306
|
+
let match;
|
|
307
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
308
|
+
const exportName = match[1];
|
|
309
|
+
const hashedId = hashLoaderId(relativePath, exportName);
|
|
310
|
+
loaderRegistry.set(hashedId, {
|
|
311
|
+
filePath: relativePath,
|
|
312
|
+
exportName
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
console.warn("[exposeLoaderId] Pre-scan failed:", error);
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
resolveId(id) {
|
|
321
|
+
if (id === VIRTUAL_LOADER_MANIFEST) {
|
|
322
|
+
return RESOLVED_VIRTUAL_LOADER_MANIFEST;
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
load(id) {
|
|
326
|
+
if (id === RESOLVED_VIRTUAL_LOADER_MANIFEST) {
|
|
327
|
+
if (!isBuild) {
|
|
328
|
+
return `import { setLoaderImports } from "rsc-router/server";
|
|
329
|
+
|
|
330
|
+
// Dev mode: empty map, loaders are resolved dynamically via path parsing
|
|
331
|
+
setLoaderImports({});
|
|
332
|
+
`;
|
|
333
|
+
}
|
|
334
|
+
const lazyImports = [];
|
|
335
|
+
for (const [hashedId, { filePath, exportName }] of loaderRegistry) {
|
|
336
|
+
lazyImports.push(
|
|
337
|
+
` "${hashedId}": () => import("/${filePath}").then(m => m.${exportName})`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
if (lazyImports.length === 0) {
|
|
341
|
+
return `import { setLoaderImports } from "rsc-router/server";
|
|
342
|
+
|
|
343
|
+
// No fetchable loaders discovered during build
|
|
344
|
+
setLoaderImports({});
|
|
345
|
+
`;
|
|
346
|
+
}
|
|
347
|
+
const code = `import { setLoaderImports } from "rsc-router/server";
|
|
348
|
+
|
|
349
|
+
// Lazy import map - loaders are loaded on-demand when first requested
|
|
350
|
+
setLoaderImports({
|
|
351
|
+
${lazyImports.join(",\n")}
|
|
352
|
+
});
|
|
353
|
+
`;
|
|
354
|
+
return code;
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
transform(code, id) {
|
|
358
|
+
if (id.includes("/node_modules/")) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (!code.includes("createLoader")) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (!hasCreateLoaderImport(code)) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const envName = this.environment?.name;
|
|
368
|
+
const isRscEnv = envName === "rsc";
|
|
369
|
+
const relativePath = normalizePath2(path2.relative(config.root, id));
|
|
370
|
+
if (isRscEnv) {
|
|
371
|
+
const pattern = /export\s+const\s+(\w+)\s*=\s*createLoader\s*\(/g;
|
|
372
|
+
let match;
|
|
373
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
374
|
+
const exportName = match[1];
|
|
375
|
+
const hashedId = hashLoaderId(relativePath, exportName);
|
|
376
|
+
loaderRegistry.set(hashedId, { filePath: relativePath, exportName });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return transformLoaderExports(code, relativePath, id, isBuild);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/vite/expose-handle-id.ts
|
|
385
|
+
import MagicString3 from "magic-string";
|
|
386
|
+
import path3 from "node:path";
|
|
387
|
+
import crypto2 from "node:crypto";
|
|
388
|
+
function normalizePath3(p) {
|
|
389
|
+
return p.split(path3.sep).join("/");
|
|
390
|
+
}
|
|
391
|
+
function hashHandleId(filePath, exportName) {
|
|
392
|
+
const input = `${filePath}#${exportName}`;
|
|
393
|
+
const hash = crypto2.createHash("sha256").update(input).digest("hex");
|
|
394
|
+
return `${hash.slice(0, 8)}#${exportName}`;
|
|
395
|
+
}
|
|
396
|
+
function hasCreateHandleImport(code) {
|
|
397
|
+
const pattern = /import\s*\{[^}]*\bcreateHandle\b[^}]*\}\s*from\s*["']rsc-router(?:\/[^"']+)?["']/;
|
|
398
|
+
return pattern.test(code);
|
|
399
|
+
}
|
|
400
|
+
function analyzeCreateHandleArgs(code, startPos, endPos) {
|
|
401
|
+
const content = code.slice(startPos, endPos).trim();
|
|
402
|
+
if (!content) {
|
|
403
|
+
return { hasArgs: false, firstArgIsString: false, firstArgIsFunction: false };
|
|
404
|
+
}
|
|
405
|
+
const firstArgIsString = /^["']/.test(content);
|
|
406
|
+
const firstArgIsFunction = content.startsWith("(") || content.startsWith("function") || // Check for identifier that could be a collect function reference
|
|
407
|
+
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*(?:,|$)/.test(content);
|
|
408
|
+
return { hasArgs: true, firstArgIsString, firstArgIsFunction };
|
|
409
|
+
}
|
|
410
|
+
function transformHandleExports(code, filePath, sourceId, isBuild = false) {
|
|
411
|
+
if (!code.includes("createHandle")) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
if (!hasCreateHandleImport(code)) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
const pattern = /export\s+const\s+(\w+)\s*=\s*createHandle\s*(?:<[^>]*>)?\s*\(/g;
|
|
418
|
+
const s = new MagicString3(code);
|
|
419
|
+
let hasChanges = false;
|
|
420
|
+
let match;
|
|
421
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
422
|
+
const exportName = match[1];
|
|
423
|
+
const matchEnd = match.index + match[0].length;
|
|
424
|
+
let parenDepth = 1;
|
|
425
|
+
let i = matchEnd;
|
|
426
|
+
while (i < code.length && parenDepth > 0) {
|
|
427
|
+
if (code[i] === "(") parenDepth++;
|
|
428
|
+
if (code[i] === ")") parenDepth--;
|
|
429
|
+
i++;
|
|
430
|
+
}
|
|
431
|
+
const closeParenPos = i - 1;
|
|
432
|
+
const args = analyzeCreateHandleArgs(code, matchEnd, closeParenPos);
|
|
433
|
+
let statementEnd = i;
|
|
434
|
+
while (statementEnd < code.length && /\s/.test(code[statementEnd])) {
|
|
435
|
+
statementEnd++;
|
|
436
|
+
}
|
|
437
|
+
if (code[statementEnd] === ";") {
|
|
438
|
+
statementEnd++;
|
|
439
|
+
}
|
|
440
|
+
const handleId = isBuild ? hashHandleId(filePath, exportName) : `${filePath}#${exportName}`;
|
|
441
|
+
let paramInjection;
|
|
442
|
+
if (!args.hasArgs) {
|
|
443
|
+
paramInjection = `undefined, "${handleId}"`;
|
|
444
|
+
} else {
|
|
445
|
+
paramInjection = `, "${handleId}"`;
|
|
446
|
+
}
|
|
447
|
+
s.appendLeft(closeParenPos, paramInjection);
|
|
448
|
+
const propInjection = `
|
|
449
|
+
${exportName}.$$id = "${handleId}";`;
|
|
450
|
+
s.appendRight(statementEnd, propInjection);
|
|
451
|
+
hasChanges = true;
|
|
452
|
+
}
|
|
453
|
+
if (!hasChanges) {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
code: s.toString(),
|
|
458
|
+
map: s.generateMap({ source: sourceId, includeContent: true })
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function exposeHandleId() {
|
|
462
|
+
let config;
|
|
463
|
+
let isBuild = false;
|
|
464
|
+
return {
|
|
465
|
+
name: "rsc-router:expose-handle-id",
|
|
466
|
+
enforce: "post",
|
|
467
|
+
configResolved(resolvedConfig) {
|
|
468
|
+
config = resolvedConfig;
|
|
469
|
+
isBuild = config.command === "build";
|
|
470
|
+
},
|
|
471
|
+
transform(code, id) {
|
|
472
|
+
if (id.includes("/node_modules/")) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (!code.includes("createHandle")) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (!hasCreateHandleImport(code)) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const relativePath = normalizePath3(path3.relative(config.root, id));
|
|
482
|
+
return transformHandleExports(code, relativePath, id, isBuild);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/vite/expose-location-state-id.ts
|
|
488
|
+
import MagicString4 from "magic-string";
|
|
489
|
+
import path4 from "node:path";
|
|
490
|
+
import crypto3 from "node:crypto";
|
|
491
|
+
function normalizePath4(p) {
|
|
492
|
+
return p.split(path4.sep).join("/");
|
|
493
|
+
}
|
|
494
|
+
function hashLocationStateKey(filePath, exportName) {
|
|
495
|
+
const input = `${filePath}#${exportName}`;
|
|
496
|
+
const hash = crypto3.createHash("sha256").update(input).digest("hex");
|
|
497
|
+
return `${hash.slice(0, 8)}#${exportName}`;
|
|
498
|
+
}
|
|
499
|
+
function hasCreateLocationStateImport(code) {
|
|
500
|
+
const pattern = /import\s*\{[^}]*\bcreateLocationState\b[^}]*\}\s*from\s*["']rsc-router(?:\/[^"']+)?["']/;
|
|
501
|
+
return pattern.test(code);
|
|
502
|
+
}
|
|
503
|
+
function transformLocationStateExports(code, filePath, sourceId, isBuild = false) {
|
|
504
|
+
if (!code.includes("createLocationState")) {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
if (!hasCreateLocationStateImport(code)) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
const pattern = /export\s+const\s+(\w+)\s*=\s*createLocationState\s*(?:<[^>]*>)?\s*\(/g;
|
|
511
|
+
const s = new MagicString4(code);
|
|
512
|
+
let hasChanges = false;
|
|
513
|
+
let match;
|
|
514
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
515
|
+
const exportName = match[1];
|
|
516
|
+
const matchEnd = match.index + match[0].length;
|
|
517
|
+
let parenDepth = 1;
|
|
518
|
+
let i = matchEnd;
|
|
519
|
+
while (i < code.length && parenDepth > 0) {
|
|
520
|
+
if (code[i] === "(") parenDepth++;
|
|
521
|
+
if (code[i] === ")") parenDepth--;
|
|
522
|
+
i++;
|
|
523
|
+
}
|
|
524
|
+
const closeParenPos = i - 1;
|
|
525
|
+
const content = code.slice(matchEnd, closeParenPos).trim();
|
|
526
|
+
const hasArgs = content.length > 0;
|
|
527
|
+
let statementEnd = i;
|
|
528
|
+
while (statementEnd < code.length && /\s/.test(code[statementEnd])) {
|
|
529
|
+
statementEnd++;
|
|
530
|
+
}
|
|
531
|
+
if (code[statementEnd] === ";") {
|
|
532
|
+
statementEnd++;
|
|
533
|
+
}
|
|
534
|
+
const stateKey = isBuild ? hashLocationStateKey(filePath, exportName) : `${filePath}#${exportName}`;
|
|
535
|
+
if (!hasArgs) {
|
|
536
|
+
s.appendLeft(closeParenPos, `"${stateKey}"`);
|
|
537
|
+
} else {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const propInjection = `
|
|
541
|
+
${exportName}.__rsc_ls_key = "__rsc_ls_${stateKey}";`;
|
|
542
|
+
s.appendRight(statementEnd, propInjection);
|
|
543
|
+
hasChanges = true;
|
|
544
|
+
}
|
|
545
|
+
if (!hasChanges) {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
code: s.toString(),
|
|
550
|
+
map: s.generateMap({ source: sourceId, includeContent: true })
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function exposeLocationStateId() {
|
|
554
|
+
let config;
|
|
555
|
+
let isBuild = false;
|
|
556
|
+
return {
|
|
557
|
+
name: "rsc-router:expose-location-state-id",
|
|
558
|
+
enforce: "post",
|
|
559
|
+
configResolved(resolvedConfig) {
|
|
560
|
+
config = resolvedConfig;
|
|
561
|
+
isBuild = config.command === "build";
|
|
562
|
+
},
|
|
563
|
+
transform(code, id) {
|
|
564
|
+
if (id.includes("/node_modules/")) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (!code.includes("createLocationState")) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (!hasCreateLocationStateImport(code)) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const relativePath = normalizePath4(path4.relative(config.root, id));
|
|
574
|
+
return transformLocationStateExports(code, relativePath, id, isBuild);
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/vite/virtual-entries.ts
|
|
580
|
+
var VIRTUAL_ENTRY_BROWSER = `
|
|
581
|
+
import {
|
|
582
|
+
createFromReadableStream,
|
|
583
|
+
createFromFetch,
|
|
584
|
+
setServerCallback,
|
|
585
|
+
encodeReply,
|
|
586
|
+
createTemporaryReferenceSet,
|
|
587
|
+
} from "rsc-router/internal/deps/browser";
|
|
588
|
+
import { createElement, StrictMode } from "react";
|
|
589
|
+
import { hydrateRoot } from "react-dom/client";
|
|
590
|
+
import { rscStream } from "rsc-router/internal/deps/html-stream-client";
|
|
591
|
+
import { initBrowserApp, RSCRouter } from "rsc-router/browser";
|
|
592
|
+
|
|
593
|
+
async function initializeApp() {
|
|
594
|
+
const deps = {
|
|
595
|
+
createFromFetch,
|
|
596
|
+
createFromReadableStream,
|
|
597
|
+
encodeReply,
|
|
598
|
+
setServerCallback,
|
|
599
|
+
createTemporaryReferenceSet,
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
await initBrowserApp({ rscStream, deps });
|
|
603
|
+
|
|
604
|
+
hydrateRoot(
|
|
605
|
+
document,
|
|
606
|
+
createElement(StrictMode, null, createElement(RSCRouter))
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
initializeApp().catch(console.error);
|
|
611
|
+
`.trim();
|
|
612
|
+
var VIRTUAL_ENTRY_SSR = `
|
|
613
|
+
import { createFromReadableStream } from "rsc-router/internal/deps/ssr";
|
|
614
|
+
import { renderToReadableStream } from "react-dom/server.edge";
|
|
615
|
+
import { injectRSCPayload } from "rsc-router/internal/deps/html-stream-server";
|
|
616
|
+
import { createSSRHandler } from "rsc-router/ssr";
|
|
617
|
+
|
|
618
|
+
export const renderHTML = createSSRHandler({
|
|
619
|
+
createFromReadableStream,
|
|
620
|
+
renderToReadableStream,
|
|
621
|
+
injectRSCPayload,
|
|
622
|
+
loadBootstrapScriptContent: () =>
|
|
623
|
+
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
624
|
+
});
|
|
625
|
+
`.trim();
|
|
626
|
+
function getVirtualEntryRSC(routerPath) {
|
|
627
|
+
return `
|
|
628
|
+
import {
|
|
629
|
+
renderToReadableStream,
|
|
630
|
+
decodeReply,
|
|
631
|
+
createTemporaryReferenceSet,
|
|
632
|
+
loadServerAction,
|
|
633
|
+
decodeAction,
|
|
634
|
+
decodeFormState,
|
|
635
|
+
} from "rsc-router/internal/deps/rsc";
|
|
636
|
+
import { router } from "${routerPath}";
|
|
637
|
+
import { createRSCHandler } from "rsc-router/rsc";
|
|
638
|
+
import { VERSION } from "rsc-router:version";
|
|
639
|
+
|
|
640
|
+
// Import loader manifest to ensure all fetchable loaders are registered at startup
|
|
641
|
+
// This is critical for serverless/multi-process deployments where the loader module
|
|
642
|
+
// might not be imported before a GET request arrives
|
|
643
|
+
import "virtual:rsc-router/loader-manifest";
|
|
644
|
+
|
|
645
|
+
export default createRSCHandler({
|
|
646
|
+
router,
|
|
647
|
+
version: VERSION,
|
|
648
|
+
deps: {
|
|
649
|
+
renderToReadableStream,
|
|
650
|
+
decodeReply,
|
|
651
|
+
createTemporaryReferenceSet,
|
|
652
|
+
loadServerAction,
|
|
653
|
+
decodeAction,
|
|
654
|
+
decodeFormState,
|
|
655
|
+
},
|
|
656
|
+
loadSSRModule: () =>
|
|
657
|
+
import.meta.viteRsc.loadModule("ssr", "index"),
|
|
658
|
+
});
|
|
659
|
+
`.trim();
|
|
660
|
+
}
|
|
661
|
+
var VIRTUAL_IDS = {
|
|
662
|
+
browser: "virtual:rsc-router/entry.browser.js",
|
|
663
|
+
ssr: "virtual:rsc-router/entry.ssr.js",
|
|
664
|
+
rsc: "virtual:rsc-router/entry.rsc.js",
|
|
665
|
+
version: "rsc-router:version"
|
|
666
|
+
};
|
|
667
|
+
function getVirtualVersionContent(version) {
|
|
668
|
+
return `export const VERSION = ${JSON.stringify(version)};`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// src/vite/package-resolution.ts
|
|
672
|
+
import { existsSync } from "node:fs";
|
|
673
|
+
import { resolve } from "node:path";
|
|
674
|
+
|
|
675
|
+
// package.json
|
|
676
|
+
var package_default = {
|
|
677
|
+
name: "@ivogt/rsc-router",
|
|
678
|
+
version: "0.0.0-experimental.9",
|
|
679
|
+
type: "module",
|
|
680
|
+
description: "Type-safe RSC router with partial rendering support",
|
|
681
|
+
author: "Ivo Todorov",
|
|
682
|
+
license: "MIT",
|
|
683
|
+
repository: {
|
|
684
|
+
type: "git",
|
|
685
|
+
url: "git+https://github.com/ivogt/vite-rsc.git",
|
|
686
|
+
directory: "packages/rsc-router"
|
|
687
|
+
},
|
|
688
|
+
homepage: "https://github.com/ivogt/vite-rsc#readme",
|
|
689
|
+
bugs: {
|
|
690
|
+
url: "https://github.com/ivogt/vite-rsc/issues"
|
|
691
|
+
},
|
|
692
|
+
publishConfig: {
|
|
693
|
+
access: "public",
|
|
694
|
+
tag: "experimental"
|
|
695
|
+
},
|
|
696
|
+
keywords: [
|
|
697
|
+
"react",
|
|
698
|
+
"rsc",
|
|
699
|
+
"react-server-components",
|
|
700
|
+
"router",
|
|
701
|
+
"vite"
|
|
702
|
+
],
|
|
703
|
+
exports: {
|
|
704
|
+
".": {
|
|
705
|
+
"react-server": "./src/index.rsc.ts",
|
|
706
|
+
types: "./src/index.ts",
|
|
707
|
+
default: "./src/index.ts"
|
|
708
|
+
},
|
|
709
|
+
"./server": {
|
|
710
|
+
types: "./src/server.ts",
|
|
711
|
+
import: "./src/server.ts"
|
|
712
|
+
},
|
|
713
|
+
"./client": {
|
|
714
|
+
"react-server": "./src/client.rsc.tsx",
|
|
715
|
+
types: "./src/client.tsx",
|
|
716
|
+
default: "./src/client.tsx"
|
|
717
|
+
},
|
|
718
|
+
"./browser": {
|
|
719
|
+
types: "./src/browser/index.ts",
|
|
720
|
+
default: "./src/browser/index.ts"
|
|
721
|
+
},
|
|
722
|
+
"./ssr": {
|
|
723
|
+
types: "./src/ssr/index.tsx",
|
|
724
|
+
default: "./src/ssr/index.tsx"
|
|
725
|
+
},
|
|
726
|
+
"./rsc": {
|
|
727
|
+
"react-server": "./src/rsc/index.ts",
|
|
728
|
+
types: "./src/rsc/index.ts",
|
|
729
|
+
default: "./src/rsc/index.ts"
|
|
730
|
+
},
|
|
731
|
+
"./vite": {
|
|
732
|
+
types: "./src/vite/index.ts",
|
|
733
|
+
import: "./dist/vite/index.js"
|
|
734
|
+
},
|
|
735
|
+
"./types": {
|
|
736
|
+
types: "./src/vite/version.d.ts"
|
|
737
|
+
},
|
|
738
|
+
"./internal/deps/browser": {
|
|
739
|
+
types: "./src/deps/browser.ts",
|
|
740
|
+
default: "./src/deps/browser.ts"
|
|
741
|
+
},
|
|
742
|
+
"./internal/deps/ssr": {
|
|
743
|
+
types: "./src/deps/ssr.ts",
|
|
744
|
+
default: "./src/deps/ssr.ts"
|
|
745
|
+
},
|
|
746
|
+
"./internal/deps/rsc": {
|
|
747
|
+
"react-server": "./src/deps/rsc.ts",
|
|
748
|
+
types: "./src/deps/rsc.ts",
|
|
749
|
+
default: "./src/deps/rsc.ts"
|
|
750
|
+
},
|
|
751
|
+
"./internal/deps/html-stream-client": {
|
|
752
|
+
types: "./src/deps/html-stream-client.ts",
|
|
753
|
+
default: "./src/deps/html-stream-client.ts"
|
|
754
|
+
},
|
|
755
|
+
"./internal/deps/html-stream-server": {
|
|
756
|
+
types: "./src/deps/html-stream-server.ts",
|
|
757
|
+
default: "./src/deps/html-stream-server.ts"
|
|
758
|
+
},
|
|
759
|
+
"./cache": {
|
|
760
|
+
"react-server": "./src/cache/index.ts",
|
|
761
|
+
types: "./src/cache/index.ts",
|
|
762
|
+
default: "./src/cache/index.ts"
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
files: [
|
|
766
|
+
"src",
|
|
767
|
+
"dist",
|
|
768
|
+
"README.md"
|
|
769
|
+
],
|
|
770
|
+
scripts: {
|
|
771
|
+
build: "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external",
|
|
772
|
+
prepublishOnly: "pnpm build",
|
|
773
|
+
typecheck: "tsc --noEmit",
|
|
774
|
+
test: "playwright test",
|
|
775
|
+
"test:ui": "playwright test --ui",
|
|
776
|
+
"test:unit": "vitest run",
|
|
777
|
+
"test:unit:watch": "vitest"
|
|
778
|
+
},
|
|
779
|
+
peerDependencies: {
|
|
780
|
+
"@cloudflare/vite-plugin": "^1.21.0",
|
|
781
|
+
"@vitejs/plugin-rsc": "^0.5.14",
|
|
782
|
+
react: "^18.0.0 || ^19.0.0",
|
|
783
|
+
vite: "^7.3.0"
|
|
784
|
+
},
|
|
785
|
+
peerDependenciesMeta: {
|
|
786
|
+
"@cloudflare/vite-plugin": {
|
|
787
|
+
optional: true
|
|
788
|
+
},
|
|
789
|
+
vite: {
|
|
790
|
+
optional: true
|
|
791
|
+
}
|
|
792
|
+
},
|
|
793
|
+
dependencies: {
|
|
794
|
+
"@vitejs/plugin-rsc": "^0.5.14",
|
|
795
|
+
"magic-string": "^0.30.17",
|
|
796
|
+
"rsc-html-stream": "^0.0.7"
|
|
797
|
+
},
|
|
798
|
+
devDependencies: {
|
|
799
|
+
"@playwright/test": "^1.49.1",
|
|
800
|
+
"@types/node": "^24.10.1",
|
|
801
|
+
"@types/react": "catalog:",
|
|
802
|
+
"@types/react-dom": "catalog:",
|
|
803
|
+
react: "catalog:",
|
|
804
|
+
"react-dom": "catalog:",
|
|
805
|
+
esbuild: "^0.27.0",
|
|
806
|
+
tinyexec: "^0.3.2",
|
|
807
|
+
typescript: "^5.3.0",
|
|
808
|
+
vitest: "^2.1.8"
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// src/vite/package-resolution.ts
|
|
813
|
+
var VIRTUAL_PACKAGE_NAME = "rsc-router";
|
|
814
|
+
function getPublishedPackageName() {
|
|
815
|
+
return package_default.name;
|
|
816
|
+
}
|
|
817
|
+
function isInstalledFromNpm() {
|
|
818
|
+
const packageName = getPublishedPackageName();
|
|
819
|
+
return existsSync(resolve(process.cwd(), "node_modules", packageName));
|
|
820
|
+
}
|
|
821
|
+
function isWorkspaceDevelopment() {
|
|
822
|
+
return !isInstalledFromNpm();
|
|
823
|
+
}
|
|
824
|
+
var PACKAGE_SUBPATHS = [
|
|
825
|
+
"",
|
|
826
|
+
"/browser",
|
|
827
|
+
"/client",
|
|
828
|
+
"/server",
|
|
829
|
+
"/rsc",
|
|
830
|
+
"/ssr",
|
|
831
|
+
"/internal/deps/browser",
|
|
832
|
+
"/internal/deps/html-stream-client",
|
|
833
|
+
"/internal/deps/ssr",
|
|
834
|
+
"/internal/deps/rsc"
|
|
835
|
+
];
|
|
836
|
+
function getExcludeDeps() {
|
|
837
|
+
const packageName = getPublishedPackageName();
|
|
838
|
+
const excludes = [];
|
|
839
|
+
for (const subpath of PACKAGE_SUBPATHS) {
|
|
840
|
+
excludes.push(`${packageName}${subpath}`);
|
|
841
|
+
if (packageName !== VIRTUAL_PACKAGE_NAME) {
|
|
842
|
+
excludes.push(`${VIRTUAL_PACKAGE_NAME}${subpath}`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return excludes;
|
|
846
|
+
}
|
|
847
|
+
var ALIAS_SUBPATHS = [
|
|
848
|
+
"/internal/deps/browser",
|
|
849
|
+
"/internal/deps/ssr",
|
|
850
|
+
"/internal/deps/rsc",
|
|
851
|
+
"/internal/deps/html-stream-client",
|
|
852
|
+
"/internal/deps/html-stream-server",
|
|
853
|
+
"/browser",
|
|
854
|
+
"/client",
|
|
855
|
+
"/server",
|
|
856
|
+
"/rsc",
|
|
857
|
+
"/ssr"
|
|
858
|
+
];
|
|
859
|
+
function getPackageAliases() {
|
|
860
|
+
if (isWorkspaceDevelopment()) {
|
|
861
|
+
return {};
|
|
862
|
+
}
|
|
863
|
+
const packageName = getPublishedPackageName();
|
|
864
|
+
const aliases = {};
|
|
865
|
+
for (const subpath of ALIAS_SUBPATHS) {
|
|
866
|
+
aliases[`${VIRTUAL_PACKAGE_NAME}${subpath}`] = `${packageName}${subpath}`;
|
|
867
|
+
}
|
|
868
|
+
return aliases;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/vite/index.ts
|
|
872
|
+
var versionEsbuildPlugin = {
|
|
873
|
+
name: "rsc-router-version",
|
|
874
|
+
setup(build) {
|
|
875
|
+
build.onResolve({ filter: /^rsc-router:version$/ }, (args) => ({
|
|
876
|
+
path: args.path,
|
|
877
|
+
namespace: "rsc-router-virtual"
|
|
878
|
+
}));
|
|
879
|
+
build.onLoad({ filter: /.*/, namespace: "rsc-router-virtual" }, () => ({
|
|
880
|
+
contents: `export const VERSION = "dev";`,
|
|
881
|
+
loader: "js"
|
|
882
|
+
}));
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
var sharedEsbuildOptions = {
|
|
886
|
+
plugins: [versionEsbuildPlugin]
|
|
887
|
+
};
|
|
888
|
+
function createVirtualEntriesPlugin(entries, routerPath) {
|
|
889
|
+
const virtualModules = {};
|
|
890
|
+
if (entries.client === VIRTUAL_IDS.browser) {
|
|
891
|
+
virtualModules[VIRTUAL_IDS.browser] = VIRTUAL_ENTRY_BROWSER;
|
|
892
|
+
}
|
|
893
|
+
if (entries.ssr === VIRTUAL_IDS.ssr) {
|
|
894
|
+
virtualModules[VIRTUAL_IDS.ssr] = VIRTUAL_ENTRY_SSR;
|
|
895
|
+
}
|
|
896
|
+
if (entries.rsc === VIRTUAL_IDS.rsc && routerPath) {
|
|
897
|
+
const absoluteRouterPath = routerPath.startsWith(".") ? "/" + routerPath.slice(2) : routerPath;
|
|
898
|
+
virtualModules[VIRTUAL_IDS.rsc] = getVirtualEntryRSC(absoluteRouterPath);
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
name: "rsc-router:virtual-entries",
|
|
902
|
+
enforce: "pre",
|
|
903
|
+
resolveId(id) {
|
|
904
|
+
if (id in virtualModules) {
|
|
905
|
+
return "\0" + id;
|
|
906
|
+
}
|
|
907
|
+
if (id.startsWith("\0") && id.slice(1) in virtualModules) {
|
|
908
|
+
return id;
|
|
909
|
+
}
|
|
910
|
+
return null;
|
|
911
|
+
},
|
|
912
|
+
load(id) {
|
|
913
|
+
if (id.startsWith("\0virtual:rsc-router/")) {
|
|
914
|
+
const virtualId = id.slice(1);
|
|
915
|
+
if (virtualId in virtualModules) {
|
|
916
|
+
return virtualModules[virtualId];
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function getManualChunks(id) {
|
|
924
|
+
const normalized = Vite.normalizePath(id);
|
|
925
|
+
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/")) {
|
|
926
|
+
return "react";
|
|
927
|
+
}
|
|
928
|
+
if (normalized.includes("node_modules/rsc-router/")) {
|
|
929
|
+
return "router";
|
|
930
|
+
}
|
|
931
|
+
return void 0;
|
|
932
|
+
}
|
|
933
|
+
function createVersionPlugin() {
|
|
934
|
+
const buildVersion = Date.now().toString(16);
|
|
935
|
+
let currentVersion = buildVersion;
|
|
936
|
+
let isDev = false;
|
|
937
|
+
let server = null;
|
|
938
|
+
return {
|
|
939
|
+
name: "rsc-router:version",
|
|
940
|
+
enforce: "pre",
|
|
941
|
+
configResolved(config) {
|
|
942
|
+
isDev = config.command === "serve";
|
|
943
|
+
},
|
|
944
|
+
configureServer(devServer) {
|
|
945
|
+
server = devServer;
|
|
946
|
+
},
|
|
947
|
+
resolveId(id) {
|
|
948
|
+
if (id === VIRTUAL_IDS.version) {
|
|
949
|
+
return "\0" + id;
|
|
950
|
+
}
|
|
951
|
+
return null;
|
|
952
|
+
},
|
|
953
|
+
load(id) {
|
|
954
|
+
if (id === "\0" + VIRTUAL_IDS.version) {
|
|
955
|
+
return getVirtualVersionContent(currentVersion);
|
|
956
|
+
}
|
|
957
|
+
return null;
|
|
958
|
+
},
|
|
959
|
+
// Track RSC module changes and update version
|
|
960
|
+
hotUpdate(ctx) {
|
|
961
|
+
if (!isDev) return;
|
|
962
|
+
const isRscModule = this.environment?.name === "rsc";
|
|
963
|
+
if (isRscModule && ctx.modules.length > 0) {
|
|
964
|
+
currentVersion = Date.now().toString(16);
|
|
965
|
+
console.log(
|
|
966
|
+
`[rsc-router] RSC module changed, version updated: ${currentVersion}`
|
|
967
|
+
);
|
|
968
|
+
if (server) {
|
|
969
|
+
const rscEnv = server.environments?.rsc;
|
|
970
|
+
if (rscEnv?.moduleGraph) {
|
|
971
|
+
const versionMod = rscEnv.moduleGraph.getModuleById(
|
|
972
|
+
"\0" + VIRTUAL_IDS.version
|
|
973
|
+
);
|
|
974
|
+
if (versionMod) {
|
|
975
|
+
rscEnv.moduleGraph.invalidateModule(versionMod);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
function createVersionInjectorPlugin(rscEntryPath) {
|
|
984
|
+
let projectRoot = "";
|
|
985
|
+
let resolvedEntryPath = "";
|
|
986
|
+
return {
|
|
987
|
+
name: "rsc-router:version-injector",
|
|
988
|
+
enforce: "pre",
|
|
989
|
+
configResolved(config) {
|
|
990
|
+
projectRoot = config.root;
|
|
991
|
+
resolvedEntryPath = resolve2(projectRoot, rscEntryPath);
|
|
992
|
+
},
|
|
993
|
+
transform(code, id) {
|
|
994
|
+
const normalizedId = Vite.normalizePath(id);
|
|
995
|
+
const normalizedEntry = Vite.normalizePath(resolvedEntryPath);
|
|
996
|
+
if (normalizedId !== normalizedEntry) {
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
if (!code.includes("createRSCHandler")) {
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
if (code.includes("rsc-router:version")) {
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
const handlerCallMatch = code.match(/createRSCHandler\s*\(\s*\{/);
|
|
1006
|
+
if (!handlerCallMatch) {
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
const lastImportIndex = code.lastIndexOf("import ");
|
|
1010
|
+
if (lastImportIndex === -1) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
const afterLastImport = code.indexOf("\n", lastImportIndex);
|
|
1014
|
+
if (afterLastImport === -1) {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
let insertIndex = afterLastImport + 1;
|
|
1018
|
+
while (insertIndex < code.length && (code.slice(insertIndex).match(/^\s*(from|import)\s/) || code[insertIndex] === "\n")) {
|
|
1019
|
+
const nextNewline = code.indexOf("\n", insertIndex);
|
|
1020
|
+
if (nextNewline === -1) break;
|
|
1021
|
+
insertIndex = nextNewline + 1;
|
|
1022
|
+
}
|
|
1023
|
+
const versionImport = `import { VERSION } from "rsc-router:version";
|
|
1024
|
+
`;
|
|
1025
|
+
let newCode = code.slice(0, insertIndex) + versionImport + code.slice(insertIndex);
|
|
1026
|
+
newCode = newCode.replace(
|
|
1027
|
+
/createRSCHandler\s*\(\s*\{/,
|
|
1028
|
+
"createRSCHandler({\n version: VERSION,"
|
|
1029
|
+
);
|
|
1030
|
+
return {
|
|
1031
|
+
code: newCode,
|
|
1032
|
+
map: null
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
async function rscRouter(options) {
|
|
1038
|
+
const preset = options.preset ?? "node";
|
|
1039
|
+
const enableExposeActionId = options.exposeActionId ?? true;
|
|
1040
|
+
const plugins = [];
|
|
1041
|
+
const rscRouterAliases = getPackageAliases();
|
|
1042
|
+
const excludeDeps = getExcludeDeps();
|
|
1043
|
+
let rscEntryPath = null;
|
|
1044
|
+
if (preset === "cloudflare") {
|
|
1045
|
+
const { default: rsc } = await import("@vitejs/plugin-rsc");
|
|
1046
|
+
const finalEntries = {
|
|
1047
|
+
client: VIRTUAL_IDS.browser,
|
|
1048
|
+
ssr: VIRTUAL_IDS.ssr
|
|
1049
|
+
};
|
|
1050
|
+
plugins.push({
|
|
1051
|
+
name: "rsc-router:cloudflare-integration",
|
|
1052
|
+
enforce: "pre",
|
|
1053
|
+
config() {
|
|
1054
|
+
return {
|
|
1055
|
+
// Exclude rsc-router modules from optimization to prevent module duplication
|
|
1056
|
+
// This ensures the same Context instance is used by both browser entry and RSC proxy modules
|
|
1057
|
+
optimizeDeps: {
|
|
1058
|
+
exclude: excludeDeps,
|
|
1059
|
+
esbuildOptions: sharedEsbuildOptions
|
|
1060
|
+
},
|
|
1061
|
+
resolve: {
|
|
1062
|
+
alias: rscRouterAliases
|
|
1063
|
+
},
|
|
1064
|
+
environments: {
|
|
1065
|
+
client: {
|
|
1066
|
+
build: {
|
|
1067
|
+
rollupOptions: {
|
|
1068
|
+
output: {
|
|
1069
|
+
manualChunks: getManualChunks
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
},
|
|
1073
|
+
// Pre-bundle rsc-html-stream to prevent discovery during first request
|
|
1074
|
+
// Exclude rsc-router modules to ensure same Context instance
|
|
1075
|
+
optimizeDeps: {
|
|
1076
|
+
include: ["rsc-html-stream/client"],
|
|
1077
|
+
exclude: excludeDeps,
|
|
1078
|
+
esbuildOptions: sharedEsbuildOptions
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
1081
|
+
ssr: {
|
|
1082
|
+
// Build SSR inside RSC directory so wrangler can deploy self-contained dist/rsc
|
|
1083
|
+
build: {
|
|
1084
|
+
outDir: "./dist/rsc/ssr"
|
|
1085
|
+
},
|
|
1086
|
+
resolve: {
|
|
1087
|
+
// Ensure single React instance in SSR child environment
|
|
1088
|
+
dedupe: ["react", "react-dom"]
|
|
1089
|
+
},
|
|
1090
|
+
// Pre-bundle SSR entry and React for proper module linking with childEnvironments
|
|
1091
|
+
// Exclude rsc-router modules to ensure same Context instance
|
|
1092
|
+
optimizeDeps: {
|
|
1093
|
+
entries: [finalEntries.ssr],
|
|
1094
|
+
include: [
|
|
1095
|
+
"react",
|
|
1096
|
+
"react-dom/server.edge",
|
|
1097
|
+
"react/jsx-runtime",
|
|
1098
|
+
"rsc-html-stream/server"
|
|
1099
|
+
],
|
|
1100
|
+
exclude: excludeDeps,
|
|
1101
|
+
esbuildOptions: sharedEsbuildOptions
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
rsc: {
|
|
1105
|
+
// RSC environment needs exclude list and esbuild options
|
|
1106
|
+
// Exclude rsc-router modules to prevent createContext in RSC environment
|
|
1107
|
+
optimizeDeps: {
|
|
1108
|
+
exclude: excludeDeps,
|
|
1109
|
+
esbuildOptions: sharedEsbuildOptions
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
plugins.push(createVirtualEntriesPlugin(finalEntries));
|
|
1117
|
+
plugins.push(
|
|
1118
|
+
rsc({
|
|
1119
|
+
get entries() {
|
|
1120
|
+
return finalEntries;
|
|
1121
|
+
},
|
|
1122
|
+
serverHandler: false
|
|
1123
|
+
})
|
|
1124
|
+
);
|
|
1125
|
+
} else {
|
|
1126
|
+
const nodeOptions = options;
|
|
1127
|
+
const routerPath = nodeOptions.router;
|
|
1128
|
+
const rscOption = nodeOptions.rsc ?? true;
|
|
1129
|
+
if (rscOption !== false) {
|
|
1130
|
+
const { default: rsc } = await import("@vitejs/plugin-rsc");
|
|
1131
|
+
const userEntries = typeof rscOption === "boolean" ? {} : rscOption.entries || {};
|
|
1132
|
+
const finalEntries = {
|
|
1133
|
+
client: userEntries.client ?? VIRTUAL_IDS.browser,
|
|
1134
|
+
ssr: userEntries.ssr ?? VIRTUAL_IDS.ssr,
|
|
1135
|
+
rsc: userEntries.rsc ?? VIRTUAL_IDS.rsc
|
|
1136
|
+
};
|
|
1137
|
+
rscEntryPath = userEntries.rsc ?? null;
|
|
1138
|
+
let hasWarnedDuplicate = false;
|
|
1139
|
+
plugins.push({
|
|
1140
|
+
name: "rsc-router:rsc-integration",
|
|
1141
|
+
enforce: "pre",
|
|
1142
|
+
config() {
|
|
1143
|
+
const useVirtualClient = finalEntries.client === VIRTUAL_IDS.browser;
|
|
1144
|
+
const useVirtualSSR = finalEntries.ssr === VIRTUAL_IDS.ssr;
|
|
1145
|
+
const useVirtualRSC = finalEntries.rsc === VIRTUAL_IDS.rsc;
|
|
1146
|
+
return {
|
|
1147
|
+
// Exclude rsc-router modules from optimization to prevent module duplication
|
|
1148
|
+
// This ensures the same Context instance is used by both browser entry and RSC proxy modules
|
|
1149
|
+
optimizeDeps: {
|
|
1150
|
+
exclude: excludeDeps,
|
|
1151
|
+
esbuildOptions: sharedEsbuildOptions
|
|
1152
|
+
},
|
|
1153
|
+
resolve: {
|
|
1154
|
+
alias: rscRouterAliases
|
|
1155
|
+
},
|
|
1156
|
+
environments: {
|
|
1157
|
+
client: {
|
|
1158
|
+
build: {
|
|
1159
|
+
rollupOptions: {
|
|
1160
|
+
output: {
|
|
1161
|
+
manualChunks: getManualChunks
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
},
|
|
1165
|
+
// Always exclude rsc-router modules, conditionally add virtual entry
|
|
1166
|
+
optimizeDeps: {
|
|
1167
|
+
exclude: excludeDeps,
|
|
1168
|
+
esbuildOptions: sharedEsbuildOptions,
|
|
1169
|
+
...useVirtualClient && {
|
|
1170
|
+
// Tell Vite to scan the virtual entry for dependencies
|
|
1171
|
+
entries: [VIRTUAL_IDS.browser]
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
},
|
|
1175
|
+
...useVirtualSSR && {
|
|
1176
|
+
ssr: {
|
|
1177
|
+
optimizeDeps: {
|
|
1178
|
+
entries: [VIRTUAL_IDS.ssr],
|
|
1179
|
+
// Pre-bundle React for SSR to ensure single instance
|
|
1180
|
+
include: ["react", "react-dom/server.edge", "react/jsx-runtime"],
|
|
1181
|
+
exclude: excludeDeps,
|
|
1182
|
+
esbuildOptions: sharedEsbuildOptions
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
},
|
|
1186
|
+
...useVirtualRSC && {
|
|
1187
|
+
rsc: {
|
|
1188
|
+
optimizeDeps: {
|
|
1189
|
+
entries: [VIRTUAL_IDS.rsc],
|
|
1190
|
+
// Pre-bundle React for RSC to ensure single instance
|
|
1191
|
+
include: ["react", "react/jsx-runtime"],
|
|
1192
|
+
esbuildOptions: sharedEsbuildOptions
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
},
|
|
1199
|
+
configResolved(config) {
|
|
1200
|
+
const rscMinimalCount = config.plugins.filter(
|
|
1201
|
+
(p) => p.name === "rsc:minimal"
|
|
1202
|
+
).length;
|
|
1203
|
+
if (rscMinimalCount > 1 && !hasWarnedDuplicate) {
|
|
1204
|
+
hasWarnedDuplicate = true;
|
|
1205
|
+
console.warn(
|
|
1206
|
+
"[rsc-router] Duplicate @vitejs/plugin-rsc detected. Remove rsc() from your config or use rscRouter({ rsc: false }) for manual configuration."
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
plugins.push(createVirtualEntriesPlugin(finalEntries, routerPath));
|
|
1212
|
+
plugins.push(
|
|
1213
|
+
rsc({
|
|
1214
|
+
entries: finalEntries
|
|
1215
|
+
})
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
if (enableExposeActionId) {
|
|
1220
|
+
plugins.push(exposeActionId());
|
|
1221
|
+
}
|
|
1222
|
+
plugins.push(exposeLoaderId());
|
|
1223
|
+
plugins.push(exposeHandleId());
|
|
1224
|
+
plugins.push(exposeLocationStateId());
|
|
1225
|
+
plugins.push(createVersionPlugin());
|
|
1226
|
+
if (rscEntryPath) {
|
|
1227
|
+
plugins.push(createVersionInjectorPlugin(rscEntryPath));
|
|
1228
|
+
}
|
|
1229
|
+
plugins.push(createCjsToEsmPlugin());
|
|
1230
|
+
return plugins;
|
|
1231
|
+
}
|
|
1232
|
+
function createCjsToEsmPlugin() {
|
|
1233
|
+
return {
|
|
1234
|
+
name: "rsc-router:cjs-to-esm",
|
|
1235
|
+
enforce: "pre",
|
|
1236
|
+
transform(code, id) {
|
|
1237
|
+
const cleanId = id.split("?")[0];
|
|
1238
|
+
if (cleanId.includes("vendor/react-server-dom/client.browser.js") || cleanId.includes("vendor\\react-server-dom\\client.browser.js")) {
|
|
1239
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
1240
|
+
const cjsFile = isProd ? "./cjs/react-server-dom-webpack-client.browser.production.js" : "./cjs/react-server-dom-webpack-client.browser.development.js";
|
|
1241
|
+
return {
|
|
1242
|
+
code: `export * from "${cjsFile}";`,
|
|
1243
|
+
map: null
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
if ((cleanId.includes("vendor/react-server-dom/cjs/") || cleanId.includes("vendor\\react-server-dom\\cjs\\")) && cleanId.includes("client.browser")) {
|
|
1247
|
+
let transformed = code;
|
|
1248
|
+
const licenseMatch = transformed.match(/^\/\*\*[\s\S]*?\*\//);
|
|
1249
|
+
const license = licenseMatch ? licenseMatch[0] : "";
|
|
1250
|
+
if (license) {
|
|
1251
|
+
transformed = transformed.slice(license.length);
|
|
1252
|
+
}
|
|
1253
|
+
transformed = transformed.replace(
|
|
1254
|
+
/^\s*["']use strict["'];\s*["']production["']\s*!==\s*process\.env\.NODE_ENV\s*&&\s*\(function\s*\(\)\s*\{/,
|
|
1255
|
+
""
|
|
1256
|
+
);
|
|
1257
|
+
transformed = transformed.replace(/\}\)\(\);?\s*$/, "");
|
|
1258
|
+
transformed = transformed.replace(
|
|
1259
|
+
/var\s+React\s*=\s*require\s*\(\s*["']react["']\s*\)\s*,[\s\n]+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
|
|
1260
|
+
'import React from "react";\nimport ReactDOM from "react-dom";\nvar '
|
|
1261
|
+
);
|
|
1262
|
+
transformed = transformed.replace(
|
|
1263
|
+
/exports\.(\w+)\s*=\s*function\s*\(/g,
|
|
1264
|
+
"export function $1("
|
|
1265
|
+
);
|
|
1266
|
+
transformed = transformed.replace(
|
|
1267
|
+
/exports\.(\w+)\s*=/g,
|
|
1268
|
+
"export const $1 ="
|
|
1269
|
+
);
|
|
1270
|
+
transformed = license + "\n" + transformed;
|
|
1271
|
+
return {
|
|
1272
|
+
code: transformed,
|
|
1273
|
+
map: null
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
export {
|
|
1281
|
+
exposeActionId,
|
|
1282
|
+
exposeHandleId,
|
|
1283
|
+
exposeLoaderId,
|
|
1284
|
+
exposeLocationStateId,
|
|
1285
|
+
rscRouter
|
|
1286
|
+
};
|