@rangojs/router 0.0.0-experimental.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/CLAUDE.md +7 -0
  2. package/README.md +19 -0
  3. package/dist/vite/index.js +1298 -0
  4. package/package.json +140 -0
  5. package/skills/caching/SKILL.md +319 -0
  6. package/skills/document-cache/SKILL.md +152 -0
  7. package/skills/hooks/SKILL.md +359 -0
  8. package/skills/intercept/SKILL.md +292 -0
  9. package/skills/layout/SKILL.md +216 -0
  10. package/skills/loader/SKILL.md +365 -0
  11. package/skills/middleware/SKILL.md +442 -0
  12. package/skills/parallel/SKILL.md +255 -0
  13. package/skills/route/SKILL.md +141 -0
  14. package/skills/router-setup/SKILL.md +403 -0
  15. package/skills/theme/SKILL.md +54 -0
  16. package/skills/typesafety/SKILL.md +352 -0
  17. package/src/__mocks__/version.ts +6 -0
  18. package/src/__tests__/component-utils.test.ts +76 -0
  19. package/src/__tests__/route-definition.test.ts +63 -0
  20. package/src/__tests__/urls.test.tsx +436 -0
  21. package/src/browser/event-controller.ts +876 -0
  22. package/src/browser/index.ts +18 -0
  23. package/src/browser/link-interceptor.ts +121 -0
  24. package/src/browser/lru-cache.ts +69 -0
  25. package/src/browser/merge-segment-loaders.ts +126 -0
  26. package/src/browser/navigation-bridge.ts +893 -0
  27. package/src/browser/navigation-client.ts +162 -0
  28. package/src/browser/navigation-store.ts +823 -0
  29. package/src/browser/partial-update.ts +559 -0
  30. package/src/browser/react/Link.tsx +248 -0
  31. package/src/browser/react/NavigationProvider.tsx +275 -0
  32. package/src/browser/react/ScrollRestoration.tsx +94 -0
  33. package/src/browser/react/context.ts +53 -0
  34. package/src/browser/react/index.ts +52 -0
  35. package/src/browser/react/location-state-shared.ts +120 -0
  36. package/src/browser/react/location-state.ts +62 -0
  37. package/src/browser/react/use-action.ts +240 -0
  38. package/src/browser/react/use-client-cache.ts +56 -0
  39. package/src/browser/react/use-handle.ts +178 -0
  40. package/src/browser/react/use-href.tsx +208 -0
  41. package/src/browser/react/use-link-status.ts +134 -0
  42. package/src/browser/react/use-navigation.ts +150 -0
  43. package/src/browser/react/use-segments.ts +188 -0
  44. package/src/browser/request-controller.ts +164 -0
  45. package/src/browser/rsc-router.tsx +353 -0
  46. package/src/browser/scroll-restoration.ts +324 -0
  47. package/src/browser/server-action-bridge.ts +747 -0
  48. package/src/browser/shallow.ts +35 -0
  49. package/src/browser/types.ts +464 -0
  50. package/src/cache/__tests__/document-cache.test.ts +522 -0
  51. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  52. package/src/cache/__tests__/memory-store.test.ts +484 -0
  53. package/src/cache/cache-scope.ts +565 -0
  54. package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
  55. package/src/cache/cf/cf-cache-store.ts +428 -0
  56. package/src/cache/cf/index.ts +19 -0
  57. package/src/cache/document-cache.ts +340 -0
  58. package/src/cache/index.ts +58 -0
  59. package/src/cache/memory-segment-store.ts +150 -0
  60. package/src/cache/memory-store.ts +253 -0
  61. package/src/cache/types.ts +387 -0
  62. package/src/client.rsc.tsx +88 -0
  63. package/src/client.tsx +621 -0
  64. package/src/component-utils.ts +76 -0
  65. package/src/components/DefaultDocument.tsx +23 -0
  66. package/src/default-error-boundary.tsx +88 -0
  67. package/src/deps/browser.ts +8 -0
  68. package/src/deps/html-stream-client.ts +2 -0
  69. package/src/deps/html-stream-server.ts +2 -0
  70. package/src/deps/rsc.ts +10 -0
  71. package/src/deps/ssr.ts +2 -0
  72. package/src/errors.ts +259 -0
  73. package/src/handle.ts +120 -0
  74. package/src/handles/MetaTags.tsx +193 -0
  75. package/src/handles/index.ts +6 -0
  76. package/src/handles/meta.ts +247 -0
  77. package/src/href-client.ts +128 -0
  78. package/src/href-context.ts +33 -0
  79. package/src/href.ts +177 -0
  80. package/src/index.rsc.ts +79 -0
  81. package/src/index.ts +87 -0
  82. package/src/loader.rsc.ts +204 -0
  83. package/src/loader.ts +47 -0
  84. package/src/network-error-thrower.tsx +21 -0
  85. package/src/outlet-context.ts +15 -0
  86. package/src/root-error-boundary.tsx +277 -0
  87. package/src/route-content-wrapper.tsx +198 -0
  88. package/src/route-definition.ts +1371 -0
  89. package/src/route-map-builder.ts +146 -0
  90. package/src/route-types.ts +198 -0
  91. package/src/route-utils.ts +89 -0
  92. package/src/router/__tests__/match-context.test.ts +104 -0
  93. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  94. package/src/router/__tests__/match-result.test.ts +566 -0
  95. package/src/router/__tests__/on-error.test.ts +935 -0
  96. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  97. package/src/router/error-handling.ts +287 -0
  98. package/src/router/handler-context.ts +158 -0
  99. package/src/router/loader-resolution.ts +326 -0
  100. package/src/router/manifest.ts +138 -0
  101. package/src/router/match-context.ts +264 -0
  102. package/src/router/match-middleware/background-revalidation.ts +236 -0
  103. package/src/router/match-middleware/cache-lookup.ts +261 -0
  104. package/src/router/match-middleware/cache-store.ts +266 -0
  105. package/src/router/match-middleware/index.ts +81 -0
  106. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  107. package/src/router/match-middleware/segment-resolution.ts +174 -0
  108. package/src/router/match-pipelines.ts +214 -0
  109. package/src/router/match-result.ts +214 -0
  110. package/src/router/metrics.ts +62 -0
  111. package/src/router/middleware.test.ts +1355 -0
  112. package/src/router/middleware.ts +748 -0
  113. package/src/router/pattern-matching.ts +272 -0
  114. package/src/router/revalidation.ts +190 -0
  115. package/src/router/router-context.ts +299 -0
  116. package/src/router/types.ts +96 -0
  117. package/src/router.ts +3876 -0
  118. package/src/rsc/__tests__/helpers.test.ts +175 -0
  119. package/src/rsc/handler.ts +1060 -0
  120. package/src/rsc/helpers.ts +64 -0
  121. package/src/rsc/index.ts +56 -0
  122. package/src/rsc/nonce.ts +18 -0
  123. package/src/rsc/types.ts +237 -0
  124. package/src/segment-system.tsx +456 -0
  125. package/src/server/__tests__/request-context.test.ts +171 -0
  126. package/src/server/context.ts +417 -0
  127. package/src/server/handle-store.ts +230 -0
  128. package/src/server/loader-registry.ts +174 -0
  129. package/src/server/request-context.ts +554 -0
  130. package/src/server/root-layout.tsx +10 -0
  131. package/src/server/tsconfig.json +14 -0
  132. package/src/server.ts +146 -0
  133. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  134. package/src/ssr/index.tsx +234 -0
  135. package/src/theme/ThemeProvider.tsx +291 -0
  136. package/src/theme/ThemeScript.tsx +61 -0
  137. package/src/theme/__tests__/theme.test.ts +120 -0
  138. package/src/theme/constants.ts +55 -0
  139. package/src/theme/index.ts +58 -0
  140. package/src/theme/theme-context.ts +70 -0
  141. package/src/theme/theme-script.ts +152 -0
  142. package/src/theme/types.ts +182 -0
  143. package/src/theme/use-theme.ts +44 -0
  144. package/src/types.ts +1561 -0
  145. package/src/urls.ts +726 -0
  146. package/src/use-loader.tsx +346 -0
  147. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  148. package/src/vite/expose-action-id.ts +344 -0
  149. package/src/vite/expose-handle-id.ts +209 -0
  150. package/src/vite/expose-loader-id.ts +357 -0
  151. package/src/vite/expose-location-state-id.ts +177 -0
  152. package/src/vite/index.ts +787 -0
  153. package/src/vite/package-resolution.ts +125 -0
  154. package/src/vite/version.d.ts +12 -0
  155. package/src/vite/virtual-entries.ts +109 -0
@@ -0,0 +1,787 @@
1
+ import type { Plugin, PluginOption } from "vite";
2
+ import * as Vite from "vite";
3
+ import { resolve } from "node:path";
4
+ import { exposeActionId } from "./expose-action-id.ts";
5
+ import { exposeLoaderId } from "./expose-loader-id.ts";
6
+ import { exposeHandleId } from "./expose-handle-id.ts";
7
+ import { exposeLocationStateId } from "./expose-location-state-id.ts";
8
+ import {
9
+ VIRTUAL_ENTRY_BROWSER,
10
+ VIRTUAL_ENTRY_SSR,
11
+ getVirtualEntryRSC,
12
+ getVirtualVersionContent,
13
+ VIRTUAL_IDS,
14
+ } from "./virtual-entries.ts";
15
+ import {
16
+ getExcludeDeps,
17
+ getPackageAliases,
18
+ getPublishedPackageName,
19
+ isWorkspaceDevelopment,
20
+ } from "./package-resolution.ts";
21
+
22
+ // Re-export plugins
23
+ export { exposeActionId } from "./expose-action-id.ts";
24
+ export { exposeLoaderId } from "./expose-loader-id.ts";
25
+ export { exposeHandleId } from "./expose-handle-id.ts";
26
+ export { exposeLocationStateId } from "./expose-location-state-id.ts";
27
+
28
+ // Virtual module type declarations in ./version.d.ts
29
+
30
+ /**
31
+ * esbuild plugin to provide rsc-router:version virtual module during optimization.
32
+ * This is needed because esbuild runs during Vite's dependency optimization phase,
33
+ * before Vite's plugin system can handle virtual modules.
34
+ */
35
+ const versionEsbuildPlugin = {
36
+ name: "@rangojs/router-version",
37
+ setup(build: any) {
38
+ build.onResolve({ filter: /^rsc-router:version$/ }, (args: any) => ({
39
+ path: args.path,
40
+ namespace: "@rangojs/router-virtual",
41
+ }));
42
+ build.onLoad({ filter: /.*/, namespace: "@rangojs/router-virtual" }, () => ({
43
+ contents: `export const VERSION = "dev";`,
44
+ loader: "js",
45
+ }));
46
+ },
47
+ };
48
+
49
+ /**
50
+ * Shared esbuild options for dependency optimization.
51
+ * Includes the version stub plugin for all environments.
52
+ */
53
+ const sharedEsbuildOptions = {
54
+ plugins: [versionEsbuildPlugin],
55
+ };
56
+
57
+ /**
58
+ * RSC plugin entry points configuration.
59
+ * All entries use virtual modules by default. Specify a path to use a custom entry file.
60
+ */
61
+ export interface RscEntries {
62
+ /**
63
+ * Path to a custom browser/client entry file.
64
+ * If not specified, a default virtual entry is used.
65
+ */
66
+ client?: string;
67
+
68
+ /**
69
+ * Path to a custom SSR entry file.
70
+ * If not specified, a default virtual entry is used.
71
+ */
72
+ ssr?: string;
73
+
74
+ /**
75
+ * Path to a custom RSC entry file.
76
+ * If not specified, a default virtual entry is used that imports the router from the `entry` option.
77
+ */
78
+ rsc?: string;
79
+ }
80
+
81
+ /**
82
+ * Options for @vitejs/plugin-rsc integration
83
+ */
84
+ export interface RscPluginOptions {
85
+ /**
86
+ * Entry points for client, ssr, and rsc environments.
87
+ * All entries use virtual modules by default.
88
+ * Specify paths only when you need custom entry files.
89
+ */
90
+ entries?: RscEntries;
91
+ }
92
+
93
+ /**
94
+ * Base options shared by all presets
95
+ */
96
+ interface RscRouterBaseOptions {
97
+ /**
98
+ * Expose $$id property on server action functions.
99
+ * Required for action-based revalidation to work.
100
+ * @default true
101
+ */
102
+ exposeActionId?: boolean;
103
+ }
104
+
105
+ /**
106
+ * Options for Node.js deployment (default)
107
+ */
108
+ export interface RscRouterNodeOptions extends RscRouterBaseOptions {
109
+ /**
110
+ * Deployment preset. Defaults to 'node' when not specified.
111
+ */
112
+ preset?: "node";
113
+
114
+ /**
115
+ * Path to your router configuration file that exports the route tree.
116
+ * This file must export a `router` object created with `createRouter()`.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * rscRouter({ router: './src/router.tsx' })
121
+ * ```
122
+ */
123
+ router: string;
124
+
125
+ /**
126
+ * RSC plugin configuration. By default, rsc-router includes @vitejs/plugin-rsc
127
+ * with sensible defaults.
128
+ *
129
+ * Entry files (browser, ssr, rsc) are optional - if they don't exist,
130
+ * virtual defaults are used.
131
+ *
132
+ * - Omit or pass `true`/`{}` to use defaults (recommended)
133
+ * - Pass `{ entries: {...} }` to customize entry paths
134
+ * - Pass `false` to disable (for manual @vitejs/plugin-rsc configuration)
135
+ *
136
+ * @default true
137
+ */
138
+ rsc?: boolean | RscPluginOptions;
139
+ }
140
+
141
+ /**
142
+ * Options for Cloudflare Workers deployment
143
+ */
144
+ export interface RscRouterCloudflareOptions extends RscRouterBaseOptions {
145
+ /**
146
+ * Deployment preset for Cloudflare Workers.
147
+ * When using cloudflare preset:
148
+ * - @vitejs/plugin-rsc is NOT added (cloudflare plugin adds it)
149
+ * - Your worker entry (e.g., worker.rsc.tsx) imports the router directly
150
+ * - Browser and SSR use virtual entries
151
+ */
152
+ preset: "cloudflare";
153
+ }
154
+
155
+ /**
156
+ * Options for rscRouter plugin
157
+ */
158
+ export type RscRouterOptions = RscRouterNodeOptions | RscRouterCloudflareOptions;
159
+
160
+ /**
161
+ * Create a virtual modules plugin for default entry files.
162
+ * Provides virtual module content when entries use VIRTUAL_IDS (no custom entry configured).
163
+ */
164
+ function createVirtualEntriesPlugin(
165
+ entries: { client: string; ssr: string; rsc?: string },
166
+ routerPath?: string
167
+ ): Plugin {
168
+
169
+ // Build virtual modules map based on which entries use virtual IDs
170
+ const virtualModules: Record<string, string> = {};
171
+
172
+ if (entries.client === VIRTUAL_IDS.browser) {
173
+ virtualModules[VIRTUAL_IDS.browser] = VIRTUAL_ENTRY_BROWSER;
174
+ }
175
+ if (entries.ssr === VIRTUAL_IDS.ssr) {
176
+ virtualModules[VIRTUAL_IDS.ssr] = VIRTUAL_ENTRY_SSR;
177
+ }
178
+ if (entries.rsc === VIRTUAL_IDS.rsc && routerPath) {
179
+ // Convert relative path to absolute for virtual module imports
180
+ const absoluteRouterPath = routerPath.startsWith(".")
181
+ ? "/" + routerPath.slice(2) // ./src/router.tsx -> /src/router.tsx
182
+ : routerPath;
183
+ virtualModules[VIRTUAL_IDS.rsc] = getVirtualEntryRSC(absoluteRouterPath);
184
+ }
185
+
186
+ return {
187
+ name: "@rangojs/router:virtual-entries",
188
+ enforce: "pre",
189
+
190
+ resolveId(id) {
191
+ if (id in virtualModules) {
192
+ return "\0" + id;
193
+ }
194
+ // Handle if the id already has the null prefix (RSC plugin wrapper imports)
195
+ if (id.startsWith("\0") && id.slice(1) in virtualModules) {
196
+ return id;
197
+ }
198
+ return null;
199
+ },
200
+
201
+ load(id) {
202
+ if (id.startsWith("\0virtual:rsc-router/")) {
203
+ const virtualId = id.slice(1);
204
+ if (virtualId in virtualModules) {
205
+ return virtualModules[virtualId];
206
+ }
207
+ }
208
+ return null;
209
+ },
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Manual chunks configuration for client build.
215
+ * Splits React and router packages into separate chunks for better caching.
216
+ */
217
+ function getManualChunks(id: string): string | undefined {
218
+ const normalized = Vite.normalizePath(id);
219
+
220
+ if (
221
+ normalized.includes("node_modules/react/") ||
222
+ normalized.includes("node_modules/react-dom/") ||
223
+ normalized.includes("node_modules/react-server-dom-webpack/") ||
224
+ normalized.includes("node_modules/@vitejs/plugin-rsc/")
225
+ ) {
226
+ return "react";
227
+ }
228
+ // Use dynamic package name from package.json
229
+ // Check both npm install path and workspace symlink resolved path
230
+ const packageName = getPublishedPackageName();
231
+ if (
232
+ normalized.includes(`node_modules/${packageName}/`) ||
233
+ normalized.includes("packages/rsc-router/") ||
234
+ normalized.includes("packages/rangojs-router/")
235
+ ) {
236
+ return "router";
237
+ }
238
+ return undefined;
239
+ }
240
+
241
+ /**
242
+ * Plugin providing rsc-router:version virtual module.
243
+ * Exports VERSION that changes when RSC modules change (dev) or at build time (production).
244
+ *
245
+ * The version is used for:
246
+ * 1. Cache invalidation - CFCacheStore uses VERSION to invalidate stale cache
247
+ * 2. Version mismatch detection - client sends version, server reloads on mismatch
248
+ *
249
+ * In dev mode, the version updates when:
250
+ * - Server starts (initial version)
251
+ * - RSC modules change via HMR (triggers version module invalidation)
252
+ *
253
+ * Client-only HMR changes don't update the version since they don't affect
254
+ * server-rendered content or cached RSC payloads.
255
+ * @internal
256
+ */
257
+ function createVersionPlugin(): Plugin {
258
+ // Generate version at plugin creation time (build/server start)
259
+ const buildVersion = Date.now().toString(16);
260
+ let currentVersion = buildVersion;
261
+ let isDev = false;
262
+ let server: any = null;
263
+
264
+ return {
265
+ name: "@rangojs/router:version",
266
+ enforce: "pre",
267
+
268
+ configResolved(config) {
269
+ isDev = config.command === "serve";
270
+ },
271
+
272
+ configureServer(devServer) {
273
+ server = devServer;
274
+ },
275
+
276
+ resolveId(id) {
277
+ if (id === VIRTUAL_IDS.version) {
278
+ return "\0" + id;
279
+ }
280
+ return null;
281
+ },
282
+
283
+ load(id) {
284
+ if (id === "\0" + VIRTUAL_IDS.version) {
285
+ return getVirtualVersionContent(currentVersion);
286
+ }
287
+ return null;
288
+ },
289
+
290
+ // Track RSC module changes and update version
291
+ hotUpdate(ctx) {
292
+ if (!isDev) return;
293
+
294
+ // Check if this is an RSC environment update (not client/ssr)
295
+ // RSC modules affect server-rendered content and cached payloads
296
+ // In Vite 6, environment is accessed via `this.environment`
297
+ const isRscModule = this.environment?.name === "rsc";
298
+
299
+ if (isRscModule && ctx.modules.length > 0) {
300
+ // Update version when RSC modules change
301
+ currentVersion = Date.now().toString(16);
302
+ console.log(
303
+ `[rsc-router] RSC module changed, version updated: ${currentVersion}`
304
+ );
305
+
306
+ // Invalidate the version module so it gets reloaded with new version
307
+ if (server) {
308
+ const rscEnv = server.environments?.rsc;
309
+ if (rscEnv?.moduleGraph) {
310
+ const versionMod = rscEnv.moduleGraph.getModuleById(
311
+ "\0" + VIRTUAL_IDS.version
312
+ );
313
+ if (versionMod) {
314
+ rscEnv.moduleGraph.invalidateModule(versionMod);
315
+ }
316
+ }
317
+ }
318
+ }
319
+ },
320
+ };
321
+ }
322
+
323
+ /**
324
+ * Plugin that auto-injects VERSION into custom entry.rsc files.
325
+ * If a custom entry.rsc file uses createRSCHandler but doesn't pass version,
326
+ * this transform adds the import and property automatically.
327
+ * @internal
328
+ */
329
+ function createVersionInjectorPlugin(rscEntryPath: string): Plugin {
330
+ let projectRoot = "";
331
+ let resolvedEntryPath = "";
332
+
333
+ return {
334
+ name: "@rangojs/router:version-injector",
335
+ enforce: "pre",
336
+
337
+ configResolved(config) {
338
+ projectRoot = config.root;
339
+ resolvedEntryPath = resolve(projectRoot, rscEntryPath);
340
+ },
341
+
342
+ transform(code, id) {
343
+ // Only transform the RSC entry file
344
+ const normalizedId = Vite.normalizePath(id);
345
+ const normalizedEntry = Vite.normalizePath(resolvedEntryPath);
346
+
347
+ if (normalizedId !== normalizedEntry) {
348
+ return null;
349
+ }
350
+
351
+ // Check if file uses createRSCHandler
352
+ if (!code.includes("createRSCHandler")) {
353
+ return null;
354
+ }
355
+
356
+ // Check if VERSION is already imported
357
+ if (code.includes("@rangojs/router:version")) {
358
+ return null;
359
+ }
360
+
361
+ // Check if version property is already being passed
362
+ // Look for version: in the createRSCHandler call
363
+ const handlerCallMatch = code.match(/createRSCHandler\s*\(\s*\{/);
364
+ if (!handlerCallMatch) {
365
+ return null;
366
+ }
367
+
368
+ // Add VERSION import after the last import statement
369
+ const lastImportIndex = code.lastIndexOf("import ");
370
+ if (lastImportIndex === -1) {
371
+ return null;
372
+ }
373
+
374
+ // Find the end of the last import statement
375
+ const afterLastImport = code.indexOf("\n", lastImportIndex);
376
+ if (afterLastImport === -1) {
377
+ return null;
378
+ }
379
+
380
+ // Find next line that's not an import continuation
381
+ let insertIndex = afterLastImport + 1;
382
+ while (
383
+ insertIndex < code.length &&
384
+ (code.slice(insertIndex).match(/^\s*(from|import)\s/) ||
385
+ code[insertIndex] === "\n")
386
+ ) {
387
+ const nextNewline = code.indexOf("\n", insertIndex);
388
+ if (nextNewline === -1) break;
389
+ insertIndex = nextNewline + 1;
390
+ }
391
+
392
+ // Insert VERSION import
393
+ const versionImport = `import { VERSION } from "@rangojs/router:version";\n`;
394
+ let newCode = code.slice(0, insertIndex) + versionImport + code.slice(insertIndex);
395
+
396
+ // Add version: VERSION to createRSCHandler call
397
+ // Find createRSCHandler({ and add version: VERSION right after the opening brace
398
+ newCode = newCode.replace(
399
+ /createRSCHandler\s*\(\s*\{/,
400
+ "createRSCHandler({\n version: VERSION,"
401
+ );
402
+
403
+ return {
404
+ code: newCode,
405
+ map: null,
406
+ };
407
+ },
408
+ };
409
+ }
410
+
411
+ /**
412
+ * Vite plugin for rsc-router.
413
+ *
414
+ * Includes @vitejs/plugin-rsc and all necessary transforms for the router
415
+ * to function correctly with React Server Components.
416
+ *
417
+ * @example Node.js (default)
418
+ * ```ts
419
+ * export default defineConfig({
420
+ * plugins: [react(), rscRouter({ router: './src/router.tsx' })],
421
+ * });
422
+ * ```
423
+ *
424
+ * @example Cloudflare Workers
425
+ * ```ts
426
+ * export default defineConfig({
427
+ * plugins: [
428
+ * react(),
429
+ * rscRouter({ preset: 'cloudflare' }),
430
+ * cloudflare({ viteEnvironment: { name: 'rsc' } }),
431
+ * ],
432
+ * });
433
+ * ```
434
+ */
435
+ export async function rscRouter(
436
+ options: RscRouterOptions
437
+ ): Promise<PluginOption[]> {
438
+ const preset = options.preset ?? "node";
439
+ const enableExposeActionId = options.exposeActionId ?? true;
440
+
441
+ const plugins: PluginOption[] = [];
442
+
443
+ // Get package resolution info (workspace vs npm install)
444
+ const rscRouterAliases = getPackageAliases();
445
+ const excludeDeps = getExcludeDeps();
446
+
447
+ // Track RSC entry path for version injection
448
+ let rscEntryPath: string | null = null;
449
+
450
+ if (preset === "cloudflare") {
451
+ // Cloudflare preset: configure entries for cloudflare worker setup
452
+ // Router is not needed here - worker.rsc.tsx imports it directly
453
+
454
+ // Dynamically import @vitejs/plugin-rsc
455
+ const { default: rsc } = await import("@vitejs/plugin-rsc");
456
+
457
+ // Only client and ssr entries - rsc entry is handled by cloudflare plugin
458
+ // Always use virtual modules for cloudflare preset
459
+ const finalEntries: { client: string; ssr: string } = {
460
+ client: VIRTUAL_IDS.browser,
461
+ ssr: VIRTUAL_IDS.ssr,
462
+ };
463
+
464
+ plugins.push({
465
+ name: "@rangojs/router:cloudflare-integration",
466
+ enforce: "pre",
467
+ config() {
468
+ // Configure environments for cloudflare deployment
469
+ return {
470
+ // Exclude rsc-router modules from optimization to prevent module duplication
471
+ // This ensures the same Context instance is used by both browser entry and RSC proxy modules
472
+ optimizeDeps: {
473
+ exclude: excludeDeps,
474
+ esbuildOptions: sharedEsbuildOptions,
475
+ },
476
+ resolve: {
477
+ alias: rscRouterAliases,
478
+ },
479
+ environments: {
480
+ client: {
481
+ build: {
482
+ rollupOptions: {
483
+ output: {
484
+ manualChunks: getManualChunks,
485
+ },
486
+ },
487
+ },
488
+ // Pre-bundle rsc-html-stream to prevent discovery during first request
489
+ // Exclude rsc-router modules to ensure same Context instance
490
+ optimizeDeps: {
491
+ include: ["rsc-html-stream/client"],
492
+ exclude: excludeDeps,
493
+ esbuildOptions: sharedEsbuildOptions,
494
+ },
495
+ },
496
+ ssr: {
497
+ // Build SSR inside RSC directory so wrangler can deploy self-contained dist/rsc
498
+ build: {
499
+ outDir: "./dist/rsc/ssr",
500
+ },
501
+ resolve: {
502
+ // Ensure single React instance in SSR child environment
503
+ dedupe: ["react", "react-dom"],
504
+ },
505
+ // Pre-bundle SSR entry and React for proper module linking with childEnvironments
506
+ // Exclude rsc-router modules to ensure same Context instance
507
+ optimizeDeps: {
508
+ entries: [finalEntries.ssr],
509
+ include: [
510
+ "react",
511
+ "react-dom/server.edge",
512
+ "react/jsx-runtime",
513
+ "rsc-html-stream/server",
514
+ ],
515
+ exclude: excludeDeps,
516
+ esbuildOptions: sharedEsbuildOptions,
517
+ },
518
+ },
519
+ rsc: {
520
+ // RSC environment needs exclude list and esbuild options
521
+ // Exclude rsc-router modules to prevent createContext in RSC environment
522
+ optimizeDeps: {
523
+ exclude: excludeDeps,
524
+ esbuildOptions: sharedEsbuildOptions,
525
+ },
526
+ },
527
+ },
528
+ };
529
+ },
530
+ });
531
+
532
+ plugins.push(createVirtualEntriesPlugin(finalEntries));
533
+
534
+ // Add RSC plugin with cloudflare-specific options
535
+ // Note: loadModuleDevProxy should NOT be used with childEnvironments
536
+ // since SSR runs in workerd alongside RSC
537
+ plugins.push(
538
+ rsc({
539
+ get entries() {
540
+ return finalEntries;
541
+ },
542
+ serverHandler: false,
543
+ }) as PluginOption
544
+ );
545
+ } else {
546
+ // Node preset: full RSC plugin integration
547
+ const nodeOptions = options as RscRouterNodeOptions;
548
+ const routerPath = nodeOptions.router;
549
+ const rscOption = nodeOptions.rsc ?? true;
550
+
551
+ // Add RSC plugin by default (can be disabled with rsc: false)
552
+ if (rscOption !== false) {
553
+ // Dynamically import @vitejs/plugin-rsc
554
+ const { default: rsc } = await import("@vitejs/plugin-rsc");
555
+
556
+ // Resolve entry paths: use explicit config or virtual modules
557
+ const userEntries =
558
+ typeof rscOption === "boolean" ? {} : rscOption.entries || {};
559
+ const finalEntries = {
560
+ client: userEntries.client ?? VIRTUAL_IDS.browser,
561
+ ssr: userEntries.ssr ?? VIRTUAL_IDS.ssr,
562
+ rsc: userEntries.rsc ?? VIRTUAL_IDS.rsc,
563
+ };
564
+
565
+ // Track RSC entry for version injection (only if custom entry provided)
566
+ rscEntryPath = userEntries.rsc ?? null;
567
+
568
+ // Create wrapper plugin that checks for duplicates
569
+ let hasWarnedDuplicate = false;
570
+
571
+ plugins.push({
572
+ name: "@rangojs/router:rsc-integration",
573
+ enforce: "pre",
574
+
575
+ config() {
576
+ // Configure environments for RSC
577
+ // When using virtual entries, we need to explicitly configure optimizeDeps
578
+ // so Vite pre-bundles React before processing the virtual modules.
579
+ // Without this, the dep optimizer may run multiple times with different hashes,
580
+ // causing React instance mismatches.
581
+ const useVirtualClient = finalEntries.client === VIRTUAL_IDS.browser;
582
+ const useVirtualSSR = finalEntries.ssr === VIRTUAL_IDS.ssr;
583
+ const useVirtualRSC = finalEntries.rsc === VIRTUAL_IDS.rsc;
584
+
585
+ return {
586
+ // Exclude rsc-router modules from optimization to prevent module duplication
587
+ // This ensures the same Context instance is used by both browser entry and RSC proxy modules
588
+ optimizeDeps: {
589
+ exclude: excludeDeps,
590
+ esbuildOptions: sharedEsbuildOptions,
591
+ },
592
+ resolve: {
593
+ alias: rscRouterAliases,
594
+ },
595
+ environments: {
596
+ client: {
597
+ build: {
598
+ rollupOptions: {
599
+ output: {
600
+ manualChunks: getManualChunks,
601
+ },
602
+ },
603
+ },
604
+ // Always exclude rsc-router modules, conditionally add virtual entry
605
+ optimizeDeps: {
606
+ exclude: excludeDeps,
607
+ esbuildOptions: sharedEsbuildOptions,
608
+ ...(useVirtualClient && {
609
+ // Tell Vite to scan the virtual entry for dependencies
610
+ entries: [VIRTUAL_IDS.browser],
611
+ }),
612
+ },
613
+ },
614
+ ...(useVirtualSSR && {
615
+ ssr: {
616
+ optimizeDeps: {
617
+ entries: [VIRTUAL_IDS.ssr],
618
+ // Pre-bundle React for SSR to ensure single instance
619
+ include: ["react", "react-dom/server.edge", "react/jsx-runtime"],
620
+ exclude: excludeDeps,
621
+ esbuildOptions: sharedEsbuildOptions,
622
+ },
623
+ },
624
+ }),
625
+ ...(useVirtualRSC && {
626
+ rsc: {
627
+ optimizeDeps: {
628
+ entries: [VIRTUAL_IDS.rsc],
629
+ // Pre-bundle React for RSC to ensure single instance
630
+ include: ["react", "react/jsx-runtime"],
631
+ esbuildOptions: sharedEsbuildOptions,
632
+ },
633
+ },
634
+ }),
635
+ },
636
+ };
637
+ },
638
+
639
+ configResolved(config) {
640
+ // Count how many RSC base plugins there are (rsc:minimal is the main one)
641
+ const rscMinimalCount = config.plugins.filter(
642
+ (p) => p.name === "rsc:minimal"
643
+ ).length;
644
+
645
+ if (rscMinimalCount > 1 && !hasWarnedDuplicate) {
646
+ hasWarnedDuplicate = true;
647
+ console.warn(
648
+ "[rsc-router] Duplicate @vitejs/plugin-rsc detected. " +
649
+ "Remove rsc() from your config or use rscRouter({ rsc: false }) for manual configuration."
650
+ );
651
+ }
652
+ },
653
+ });
654
+
655
+ // Add virtual entries plugin
656
+ plugins.push(createVirtualEntriesPlugin(finalEntries, routerPath));
657
+
658
+ // Add the RSC plugin directly
659
+ // Cast to PluginOption to handle type differences between bundled vite types
660
+ plugins.push(
661
+ rsc({
662
+ entries: finalEntries,
663
+ }) as PluginOption
664
+ );
665
+ }
666
+ }
667
+
668
+ if (enableExposeActionId) {
669
+ plugins.push(exposeActionId());
670
+ }
671
+
672
+ // Always add exposeLoaderId for GET-based loader fetching with useFetchLoader
673
+ plugins.push(exposeLoaderId());
674
+
675
+ // Always add exposeHandleId for auto-generated handle IDs
676
+ plugins.push(exposeHandleId());
677
+
678
+ // Always add exposeLocationStateId for auto-generated location state keys
679
+ plugins.push(exposeLocationStateId());
680
+
681
+ // Add version virtual module plugin for cache invalidation
682
+ plugins.push(createVersionPlugin());
683
+
684
+ // Add version injector for custom entry.rsc files
685
+ if (rscEntryPath) {
686
+ plugins.push(createVersionInjectorPlugin(rscEntryPath));
687
+ }
688
+
689
+ // Transform CJS vendor files to ESM for browser compatibility
690
+ // optimizeDeps.include doesn't work because the file is loaded after initial optimization
691
+ plugins.push(createCjsToEsmPlugin());
692
+
693
+ return plugins;
694
+ }
695
+
696
+ /**
697
+ * Transform CJS vendor files from @vitejs/plugin-rsc to ESM for browser compatibility.
698
+ * The react-server-dom vendor files are shipped as CJS which doesn't work in browsers.
699
+ */
700
+ function createCjsToEsmPlugin(): Plugin {
701
+ return {
702
+ name: "@rangojs/router:cjs-to-esm",
703
+ enforce: "pre",
704
+ transform(code, id) {
705
+ const cleanId = id.split("?")[0];
706
+
707
+ // Transform the client.browser.js entry point to re-export from CJS
708
+ if (
709
+ cleanId.includes("vendor/react-server-dom/client.browser.js") ||
710
+ cleanId.includes("vendor\\react-server-dom\\client.browser.js")
711
+ ) {
712
+ const isProd = process.env.NODE_ENV === "production";
713
+ const cjsFile = isProd
714
+ ? "./cjs/react-server-dom-webpack-client.browser.production.js"
715
+ : "./cjs/react-server-dom-webpack-client.browser.development.js";
716
+
717
+ return {
718
+ code: `export * from "${cjsFile}";`,
719
+ map: null,
720
+ };
721
+ }
722
+
723
+ // Transform the actual CJS files to ESM
724
+ if (
725
+ (cleanId.includes("vendor/react-server-dom/cjs/") ||
726
+ cleanId.includes("vendor\\react-server-dom\\cjs\\")) &&
727
+ cleanId.includes("client.browser")
728
+ ) {
729
+ let transformed = code;
730
+
731
+ // Extract the license comment to preserve it
732
+ const licenseMatch = transformed.match(/^\/\*\*[\s\S]*?\*\//);
733
+ const license = licenseMatch ? licenseMatch[0] : "";
734
+ if (license) {
735
+ transformed = transformed.slice(license.length);
736
+ }
737
+
738
+ // Remove "use strict" (both dev and prod have this)
739
+ transformed = transformed.replace(/^\s*["']use strict["'];\s*/, "");
740
+
741
+ // Remove the conditional IIFE wrapper (development only)
742
+ transformed = transformed.replace(
743
+ /^\s*["']production["']\s*!==\s*process\.env\.NODE_ENV\s*&&\s*\(function\s*\(\)\s*\{/,
744
+ ""
745
+ );
746
+
747
+ // Remove the closing of the conditional IIFE at the end (development only)
748
+ transformed = transformed.replace(/\}\)\(\);?\s*$/, "");
749
+
750
+ // Replace require('react') and require('react-dom') with imports (development)
751
+ transformed = transformed.replace(
752
+ /var\s+React\s*=\s*require\s*\(\s*["']react["']\s*\)\s*,[\s\n]+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
753
+ 'import React from "react";\nimport ReactDOM from "react-dom";\nvar '
754
+ );
755
+
756
+ // Replace require('react-dom') only (production - doesn't import React)
757
+ transformed = transformed.replace(
758
+ /var\s+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
759
+ 'import ReactDOM from "react-dom";\nvar '
760
+ );
761
+
762
+ // Transform exports.xyz = function() to export function xyz()
763
+ transformed = transformed.replace(
764
+ /exports\.(\w+)\s*=\s*function\s*\(/g,
765
+ "export function $1("
766
+ );
767
+
768
+ // Transform exports.xyz = value to export const xyz = value
769
+ transformed = transformed.replace(
770
+ /exports\.(\w+)\s*=/g,
771
+ "export const $1 ="
772
+ );
773
+
774
+ // Reconstruct with license at the top
775
+ transformed = license + "\n" + transformed;
776
+
777
+ return {
778
+ code: transformed,
779
+ map: null,
780
+ };
781
+ }
782
+
783
+ return null;
784
+ },
785
+ };
786
+ }
787
+