@ivogt/rsc-router 0.0.0-experimental.7 → 0.0.0-experimental.8

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.
@@ -0,0 +1,1237 @@
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.8",
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
+ return plugins;
1230
+ }
1231
+ export {
1232
+ exposeActionId,
1233
+ exposeHandleId,
1234
+ exposeLoaderId,
1235
+ exposeLocationStateId,
1236
+ rscRouter
1237
+ };