@ivogt/rsc-router 0.0.0-experimental.1
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/README.md +19 -0
- package/package.json +131 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/route-definition.test.ts +63 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +891 -0
- package/src/browser/navigation-client.ts +155 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +545 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +228 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +178 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-navigation.ts +150 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +149 -0
- package/src/browser/rsc-router.tsx +310 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/server-action-bridge.ts +747 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +443 -0
- package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
- package/src/cache/__tests__/memory-store.test.ts +484 -0
- package/src/cache/cache-scope.ts +565 -0
- package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
- package/src/cache/cf/cf-cache-store.ts +274 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/index.ts +52 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +366 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +609 -0
- package/src/components/DefaultDocument.tsx +20 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +259 -0
- package/src/handle.ts +120 -0
- package/src/handles/MetaTags.tsx +178 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/href-client.ts +128 -0
- package/src/href.ts +139 -0
- package/src/index.rsc.ts +69 -0
- package/src/index.ts +84 -0
- package/src/loader.rsc.ts +204 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +198 -0
- package/src/route-definition.ts +1333 -0
- package/src/route-map-builder.ts +140 -0
- package/src/route-types.ts +148 -0
- package/src/route-utils.ts +89 -0
- package/src/router/__tests__/match-context.test.ts +104 -0
- package/src/router/__tests__/match-pipelines.test.ts +537 -0
- package/src/router/__tests__/match-result.test.ts +566 -0
- package/src/router/__tests__/on-error.test.ts +935 -0
- package/src/router/__tests__/pattern-matching.test.ts +577 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +60 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +116 -0
- package/src/router/match-context.ts +261 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +261 -0
- package/src/router/match-middleware/cache-store.ts +250 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +268 -0
- package/src/router/match-middleware/segment-resolution.ts +174 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +212 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.test.ts +1355 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +271 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +299 -0
- package/src/router/types.ts +96 -0
- package/src/router.ts +3484 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +942 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +225 -0
- package/src/segment-system.tsx +405 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +340 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +470 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +126 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +215 -0
- package/src/types.ts +1473 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +357 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/index.ts +608 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +109 -0
|
@@ -0,0 +1,608 @@
|
|
|
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
|
+
|
|
16
|
+
// Re-export plugins
|
|
17
|
+
export { exposeActionId } from "./expose-action-id.ts";
|
|
18
|
+
export { exposeLoaderId } from "./expose-loader-id.ts";
|
|
19
|
+
export { exposeHandleId } from "./expose-handle-id.ts";
|
|
20
|
+
export { exposeLocationStateId } from "./expose-location-state-id.ts";
|
|
21
|
+
|
|
22
|
+
// Virtual module type declarations in ./version.d.ts
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* RSC plugin entry points configuration.
|
|
26
|
+
* All entries use virtual modules by default. Specify a path to use a custom entry file.
|
|
27
|
+
*/
|
|
28
|
+
export interface RscEntries {
|
|
29
|
+
/**
|
|
30
|
+
* Path to a custom browser/client entry file.
|
|
31
|
+
* If not specified, a default virtual entry is used.
|
|
32
|
+
*/
|
|
33
|
+
client?: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Path to a custom SSR entry file.
|
|
37
|
+
* If not specified, a default virtual entry is used.
|
|
38
|
+
*/
|
|
39
|
+
ssr?: string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Path to a custom RSC entry file.
|
|
43
|
+
* If not specified, a default virtual entry is used that imports the router from the `entry` option.
|
|
44
|
+
*/
|
|
45
|
+
rsc?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Options for @vitejs/plugin-rsc integration
|
|
50
|
+
*/
|
|
51
|
+
export interface RscPluginOptions {
|
|
52
|
+
/**
|
|
53
|
+
* Entry points for client, ssr, and rsc environments.
|
|
54
|
+
* All entries use virtual modules by default.
|
|
55
|
+
* Specify paths only when you need custom entry files.
|
|
56
|
+
*/
|
|
57
|
+
entries?: RscEntries;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Base options shared by all presets
|
|
62
|
+
*/
|
|
63
|
+
interface RscRouterBaseOptions {
|
|
64
|
+
/**
|
|
65
|
+
* Expose $$id property on server action functions.
|
|
66
|
+
* Required for action-based revalidation to work.
|
|
67
|
+
* @default true
|
|
68
|
+
*/
|
|
69
|
+
exposeActionId?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Options for Node.js deployment (default)
|
|
74
|
+
*/
|
|
75
|
+
export interface RscRouterNodeOptions extends RscRouterBaseOptions {
|
|
76
|
+
/**
|
|
77
|
+
* Deployment preset. Defaults to 'node' when not specified.
|
|
78
|
+
*/
|
|
79
|
+
preset?: "node";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Path to your router configuration file that exports the route tree.
|
|
83
|
+
* This file must export a `router` object created with `createRouter()`.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* rscRouter({ router: './src/router.tsx' })
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
router: string;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* RSC plugin configuration. By default, rsc-router includes @vitejs/plugin-rsc
|
|
94
|
+
* with sensible defaults.
|
|
95
|
+
*
|
|
96
|
+
* Entry files (browser, ssr, rsc) are optional - if they don't exist,
|
|
97
|
+
* virtual defaults are used.
|
|
98
|
+
*
|
|
99
|
+
* - Omit or pass `true`/`{}` to use defaults (recommended)
|
|
100
|
+
* - Pass `{ entries: {...} }` to customize entry paths
|
|
101
|
+
* - Pass `false` to disable (for manual @vitejs/plugin-rsc configuration)
|
|
102
|
+
*
|
|
103
|
+
* @default true
|
|
104
|
+
*/
|
|
105
|
+
rsc?: boolean | RscPluginOptions;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Options for Cloudflare Workers deployment
|
|
110
|
+
*/
|
|
111
|
+
export interface RscRouterCloudflareOptions extends RscRouterBaseOptions {
|
|
112
|
+
/**
|
|
113
|
+
* Deployment preset for Cloudflare Workers.
|
|
114
|
+
* When using cloudflare preset:
|
|
115
|
+
* - @vitejs/plugin-rsc is NOT added (cloudflare plugin adds it)
|
|
116
|
+
* - Your worker entry (e.g., worker.rsc.tsx) imports the router directly
|
|
117
|
+
* - Browser and SSR use virtual entries
|
|
118
|
+
*/
|
|
119
|
+
preset: "cloudflare";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Options for rscRouter plugin
|
|
124
|
+
*/
|
|
125
|
+
export type RscRouterOptions = RscRouterNodeOptions | RscRouterCloudflareOptions;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create a virtual modules plugin for default entry files.
|
|
129
|
+
* Provides virtual module content when entries use VIRTUAL_IDS (no custom entry configured).
|
|
130
|
+
*/
|
|
131
|
+
function createVirtualEntriesPlugin(
|
|
132
|
+
entries: { client: string; ssr: string; rsc?: string },
|
|
133
|
+
routerPath?: string
|
|
134
|
+
): Plugin {
|
|
135
|
+
|
|
136
|
+
// Build virtual modules map based on which entries use virtual IDs
|
|
137
|
+
const virtualModules: Record<string, string> = {};
|
|
138
|
+
|
|
139
|
+
if (entries.client === VIRTUAL_IDS.browser) {
|
|
140
|
+
virtualModules[VIRTUAL_IDS.browser] = VIRTUAL_ENTRY_BROWSER;
|
|
141
|
+
}
|
|
142
|
+
if (entries.ssr === VIRTUAL_IDS.ssr) {
|
|
143
|
+
virtualModules[VIRTUAL_IDS.ssr] = VIRTUAL_ENTRY_SSR;
|
|
144
|
+
}
|
|
145
|
+
if (entries.rsc === VIRTUAL_IDS.rsc && routerPath) {
|
|
146
|
+
// Convert relative path to absolute for virtual module imports
|
|
147
|
+
const absoluteRouterPath = routerPath.startsWith(".")
|
|
148
|
+
? "/" + routerPath.slice(2) // ./src/router.tsx -> /src/router.tsx
|
|
149
|
+
: routerPath;
|
|
150
|
+
virtualModules[VIRTUAL_IDS.rsc] = getVirtualEntryRSC(absoluteRouterPath);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
name: "rsc-router:virtual-entries",
|
|
155
|
+
enforce: "pre",
|
|
156
|
+
|
|
157
|
+
resolveId(id) {
|
|
158
|
+
if (id in virtualModules) {
|
|
159
|
+
return "\0" + id;
|
|
160
|
+
}
|
|
161
|
+
// Handle if the id already has the null prefix (RSC plugin wrapper imports)
|
|
162
|
+
if (id.startsWith("\0") && id.slice(1) in virtualModules) {
|
|
163
|
+
return id;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
load(id) {
|
|
169
|
+
if (id.startsWith("\0virtual:rsc-router/")) {
|
|
170
|
+
const virtualId = id.slice(1);
|
|
171
|
+
if (virtualId in virtualModules) {
|
|
172
|
+
return virtualModules[virtualId];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Manual chunks configuration for client build.
|
|
182
|
+
* Splits React and router packages into separate chunks for better caching.
|
|
183
|
+
*/
|
|
184
|
+
function getManualChunks(id: string): string | undefined {
|
|
185
|
+
const normalized = Vite.normalizePath(id);
|
|
186
|
+
if (
|
|
187
|
+
normalized.includes("node_modules/react/") ||
|
|
188
|
+
normalized.includes("node_modules/react-dom/") ||
|
|
189
|
+
normalized.includes("node_modules/react-server-dom-webpack/") ||
|
|
190
|
+
normalized.includes("node_modules/@vitejs/plugin-rsc/")
|
|
191
|
+
) {
|
|
192
|
+
return "react";
|
|
193
|
+
}
|
|
194
|
+
if (normalized.includes("node_modules/rsc-router/")) {
|
|
195
|
+
return "router";
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Plugin providing rsc-router:version virtual module.
|
|
202
|
+
* Exports VERSION that changes when RSC modules change (dev) or at build time (production).
|
|
203
|
+
*
|
|
204
|
+
* The version is used for:
|
|
205
|
+
* 1. Cache invalidation - CFCacheStore uses VERSION to invalidate stale cache
|
|
206
|
+
* 2. Version mismatch detection - client sends version, server reloads on mismatch
|
|
207
|
+
*
|
|
208
|
+
* In dev mode, the version updates when:
|
|
209
|
+
* - Server starts (initial version)
|
|
210
|
+
* - RSC modules change via HMR (triggers version module invalidation)
|
|
211
|
+
*
|
|
212
|
+
* Client-only HMR changes don't update the version since they don't affect
|
|
213
|
+
* server-rendered content or cached RSC payloads.
|
|
214
|
+
* @internal
|
|
215
|
+
*/
|
|
216
|
+
function createVersionPlugin(): Plugin {
|
|
217
|
+
// Generate version at plugin creation time (build/server start)
|
|
218
|
+
const buildVersion = Date.now().toString(16);
|
|
219
|
+
let currentVersion = buildVersion;
|
|
220
|
+
let isDev = false;
|
|
221
|
+
let server: any = null;
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
name: "rsc-router:version",
|
|
225
|
+
enforce: "pre",
|
|
226
|
+
|
|
227
|
+
configResolved(config) {
|
|
228
|
+
isDev = config.command === "serve";
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
configureServer(devServer) {
|
|
232
|
+
server = devServer;
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
resolveId(id) {
|
|
236
|
+
if (id === VIRTUAL_IDS.version) {
|
|
237
|
+
return "\0" + id;
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
load(id) {
|
|
243
|
+
if (id === "\0" + VIRTUAL_IDS.version) {
|
|
244
|
+
return getVirtualVersionContent(currentVersion);
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
// Track RSC module changes and update version
|
|
250
|
+
hotUpdate(ctx) {
|
|
251
|
+
if (!isDev) return;
|
|
252
|
+
|
|
253
|
+
// Check if this is an RSC environment update (not client/ssr)
|
|
254
|
+
// RSC modules affect server-rendered content and cached payloads
|
|
255
|
+
// In Vite 6, environment is accessed via `this.environment`
|
|
256
|
+
const isRscModule = this.environment?.name === "rsc";
|
|
257
|
+
|
|
258
|
+
if (isRscModule && ctx.modules.length > 0) {
|
|
259
|
+
// Update version when RSC modules change
|
|
260
|
+
currentVersion = Date.now().toString(16);
|
|
261
|
+
console.log(
|
|
262
|
+
`[rsc-router] RSC module changed, version updated: ${currentVersion}`
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Invalidate the version module so it gets reloaded with new version
|
|
266
|
+
if (server) {
|
|
267
|
+
const rscEnv = server.environments?.rsc;
|
|
268
|
+
if (rscEnv?.moduleGraph) {
|
|
269
|
+
const versionMod = rscEnv.moduleGraph.getModuleById(
|
|
270
|
+
"\0" + VIRTUAL_IDS.version
|
|
271
|
+
);
|
|
272
|
+
if (versionMod) {
|
|
273
|
+
rscEnv.moduleGraph.invalidateModule(versionMod);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Plugin that auto-injects VERSION into custom entry.rsc files.
|
|
284
|
+
* If a custom entry.rsc file uses createRSCHandler but doesn't pass version,
|
|
285
|
+
* this transform adds the import and property automatically.
|
|
286
|
+
* @internal
|
|
287
|
+
*/
|
|
288
|
+
function createVersionInjectorPlugin(rscEntryPath: string): Plugin {
|
|
289
|
+
let projectRoot = "";
|
|
290
|
+
let resolvedEntryPath = "";
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
name: "rsc-router:version-injector",
|
|
294
|
+
enforce: "pre",
|
|
295
|
+
|
|
296
|
+
configResolved(config) {
|
|
297
|
+
projectRoot = config.root;
|
|
298
|
+
resolvedEntryPath = resolve(projectRoot, rscEntryPath);
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
transform(code, id) {
|
|
302
|
+
// Only transform the RSC entry file
|
|
303
|
+
const normalizedId = Vite.normalizePath(id);
|
|
304
|
+
const normalizedEntry = Vite.normalizePath(resolvedEntryPath);
|
|
305
|
+
|
|
306
|
+
if (normalizedId !== normalizedEntry) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Check if file uses createRSCHandler
|
|
311
|
+
if (!code.includes("createRSCHandler")) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check if VERSION is already imported
|
|
316
|
+
if (code.includes("rsc-router:version")) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check if version property is already being passed
|
|
321
|
+
// Look for version: in the createRSCHandler call
|
|
322
|
+
const handlerCallMatch = code.match(/createRSCHandler\s*\(\s*\{/);
|
|
323
|
+
if (!handlerCallMatch) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Add VERSION import after the last import statement
|
|
328
|
+
const lastImportIndex = code.lastIndexOf("import ");
|
|
329
|
+
if (lastImportIndex === -1) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Find the end of the last import statement
|
|
334
|
+
const afterLastImport = code.indexOf("\n", lastImportIndex);
|
|
335
|
+
if (afterLastImport === -1) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Find next line that's not an import continuation
|
|
340
|
+
let insertIndex = afterLastImport + 1;
|
|
341
|
+
while (
|
|
342
|
+
insertIndex < code.length &&
|
|
343
|
+
(code.slice(insertIndex).match(/^\s*(from|import)\s/) ||
|
|
344
|
+
code[insertIndex] === "\n")
|
|
345
|
+
) {
|
|
346
|
+
const nextNewline = code.indexOf("\n", insertIndex);
|
|
347
|
+
if (nextNewline === -1) break;
|
|
348
|
+
insertIndex = nextNewline + 1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Insert VERSION import
|
|
352
|
+
const versionImport = `import { VERSION } from "rsc-router:version";\n`;
|
|
353
|
+
let newCode = code.slice(0, insertIndex) + versionImport + code.slice(insertIndex);
|
|
354
|
+
|
|
355
|
+
// Add version: VERSION to createRSCHandler call
|
|
356
|
+
// Find createRSCHandler({ and add version: VERSION right after the opening brace
|
|
357
|
+
newCode = newCode.replace(
|
|
358
|
+
/createRSCHandler\s*\(\s*\{/,
|
|
359
|
+
"createRSCHandler({\n version: VERSION,"
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
code: newCode,
|
|
364
|
+
map: null,
|
|
365
|
+
};
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Vite plugin for rsc-router.
|
|
372
|
+
*
|
|
373
|
+
* Includes @vitejs/plugin-rsc and all necessary transforms for the router
|
|
374
|
+
* to function correctly with React Server Components.
|
|
375
|
+
*
|
|
376
|
+
* @example Node.js (default)
|
|
377
|
+
* ```ts
|
|
378
|
+
* export default defineConfig({
|
|
379
|
+
* plugins: [react(), rscRouter({ router: './src/router.tsx' })],
|
|
380
|
+
* });
|
|
381
|
+
* ```
|
|
382
|
+
*
|
|
383
|
+
* @example Cloudflare Workers
|
|
384
|
+
* ```ts
|
|
385
|
+
* export default defineConfig({
|
|
386
|
+
* plugins: [
|
|
387
|
+
* react(),
|
|
388
|
+
* rscRouter({ preset: 'cloudflare' }),
|
|
389
|
+
* cloudflare({ viteEnvironment: { name: 'rsc' } }),
|
|
390
|
+
* ],
|
|
391
|
+
* });
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
394
|
+
export async function rscRouter(
|
|
395
|
+
options: RscRouterOptions
|
|
396
|
+
): Promise<PluginOption[]> {
|
|
397
|
+
const preset = options.preset ?? "node";
|
|
398
|
+
const enableExposeActionId = options.exposeActionId ?? true;
|
|
399
|
+
|
|
400
|
+
const plugins: PluginOption[] = [];
|
|
401
|
+
|
|
402
|
+
// Track RSC entry path for version injection
|
|
403
|
+
let rscEntryPath: string | null = null;
|
|
404
|
+
|
|
405
|
+
if (preset === "cloudflare") {
|
|
406
|
+
// Cloudflare preset: configure entries for cloudflare worker setup
|
|
407
|
+
// Router is not needed here - worker.rsc.tsx imports it directly
|
|
408
|
+
|
|
409
|
+
// Dynamically import @vitejs/plugin-rsc
|
|
410
|
+
const { default: rsc } = await import("@vitejs/plugin-rsc");
|
|
411
|
+
|
|
412
|
+
// Only client and ssr entries - rsc entry is handled by cloudflare plugin
|
|
413
|
+
// Always use virtual modules for cloudflare preset
|
|
414
|
+
const finalEntries: { client: string; ssr: string } = {
|
|
415
|
+
client: VIRTUAL_IDS.browser,
|
|
416
|
+
ssr: VIRTUAL_IDS.ssr,
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
plugins.push({
|
|
420
|
+
name: "rsc-router:cloudflare-integration",
|
|
421
|
+
enforce: "pre",
|
|
422
|
+
config() {
|
|
423
|
+
// Configure environments for cloudflare deployment
|
|
424
|
+
return {
|
|
425
|
+
environments: {
|
|
426
|
+
client: {
|
|
427
|
+
build: {
|
|
428
|
+
rollupOptions: {
|
|
429
|
+
output: {
|
|
430
|
+
manualChunks: getManualChunks,
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
// Pre-bundle rsc-html-stream to prevent discovery during first request
|
|
435
|
+
optimizeDeps: {
|
|
436
|
+
include: ["rsc-html-stream/client"],
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
ssr: {
|
|
440
|
+
// Build SSR inside RSC directory so wrangler can deploy self-contained dist/rsc
|
|
441
|
+
build: {
|
|
442
|
+
outDir: "./dist/rsc/ssr",
|
|
443
|
+
},
|
|
444
|
+
resolve: {
|
|
445
|
+
// Ensure single React instance in SSR child environment
|
|
446
|
+
dedupe: ["react", "react-dom"],
|
|
447
|
+
},
|
|
448
|
+
// Pre-bundle SSR entry and React for proper module linking with childEnvironments
|
|
449
|
+
optimizeDeps: {
|
|
450
|
+
entries: [finalEntries.ssr],
|
|
451
|
+
include: [
|
|
452
|
+
"react",
|
|
453
|
+
"react-dom/server.edge",
|
|
454
|
+
"react/jsx-runtime",
|
|
455
|
+
"rsc-html-stream/server",
|
|
456
|
+
],
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
plugins.push(createVirtualEntriesPlugin(finalEntries));
|
|
465
|
+
|
|
466
|
+
// Add RSC plugin with cloudflare-specific options
|
|
467
|
+
// Note: loadModuleDevProxy should NOT be used with childEnvironments
|
|
468
|
+
// since SSR runs in workerd alongside RSC
|
|
469
|
+
plugins.push(
|
|
470
|
+
rsc({
|
|
471
|
+
get entries() {
|
|
472
|
+
return finalEntries;
|
|
473
|
+
},
|
|
474
|
+
serverHandler: false,
|
|
475
|
+
}) as PluginOption
|
|
476
|
+
);
|
|
477
|
+
} else {
|
|
478
|
+
// Node preset: full RSC plugin integration
|
|
479
|
+
const nodeOptions = options as RscRouterNodeOptions;
|
|
480
|
+
const routerPath = nodeOptions.router;
|
|
481
|
+
const rscOption = nodeOptions.rsc ?? true;
|
|
482
|
+
|
|
483
|
+
// Add RSC plugin by default (can be disabled with rsc: false)
|
|
484
|
+
if (rscOption !== false) {
|
|
485
|
+
// Dynamically import @vitejs/plugin-rsc
|
|
486
|
+
const { default: rsc } = await import("@vitejs/plugin-rsc");
|
|
487
|
+
|
|
488
|
+
// Resolve entry paths: use explicit config or virtual modules
|
|
489
|
+
const userEntries =
|
|
490
|
+
typeof rscOption === "boolean" ? {} : rscOption.entries || {};
|
|
491
|
+
const finalEntries = {
|
|
492
|
+
client: userEntries.client ?? VIRTUAL_IDS.browser,
|
|
493
|
+
ssr: userEntries.ssr ?? VIRTUAL_IDS.ssr,
|
|
494
|
+
rsc: userEntries.rsc ?? VIRTUAL_IDS.rsc,
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// Track RSC entry for version injection (only if custom entry provided)
|
|
498
|
+
rscEntryPath = userEntries.rsc ?? null;
|
|
499
|
+
|
|
500
|
+
// Create wrapper plugin that checks for duplicates
|
|
501
|
+
let hasWarnedDuplicate = false;
|
|
502
|
+
|
|
503
|
+
plugins.push({
|
|
504
|
+
name: "rsc-router:rsc-integration",
|
|
505
|
+
enforce: "pre",
|
|
506
|
+
|
|
507
|
+
config() {
|
|
508
|
+
// Configure environments for RSC
|
|
509
|
+
// When using virtual entries, we need to explicitly configure optimizeDeps
|
|
510
|
+
// so Vite pre-bundles React before processing the virtual modules.
|
|
511
|
+
// Without this, the dep optimizer may run multiple times with different hashes,
|
|
512
|
+
// causing React instance mismatches.
|
|
513
|
+
const useVirtualClient = finalEntries.client === VIRTUAL_IDS.browser;
|
|
514
|
+
const useVirtualSSR = finalEntries.ssr === VIRTUAL_IDS.ssr;
|
|
515
|
+
const useVirtualRSC = finalEntries.rsc === VIRTUAL_IDS.rsc;
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
environments: {
|
|
519
|
+
client: {
|
|
520
|
+
build: {
|
|
521
|
+
rollupOptions: {
|
|
522
|
+
output: {
|
|
523
|
+
manualChunks: getManualChunks,
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
...(useVirtualClient && {
|
|
528
|
+
optimizeDeps: {
|
|
529
|
+
// Tell Vite to scan the virtual entry for dependencies
|
|
530
|
+
entries: [VIRTUAL_IDS.browser],
|
|
531
|
+
},
|
|
532
|
+
}),
|
|
533
|
+
},
|
|
534
|
+
...(useVirtualSSR && {
|
|
535
|
+
ssr: {
|
|
536
|
+
optimizeDeps: {
|
|
537
|
+
entries: [VIRTUAL_IDS.ssr],
|
|
538
|
+
// Pre-bundle React for SSR to ensure single instance
|
|
539
|
+
include: ["react", "react-dom/server.edge", "react/jsx-runtime"],
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
}),
|
|
543
|
+
...(useVirtualRSC && {
|
|
544
|
+
rsc: {
|
|
545
|
+
optimizeDeps: {
|
|
546
|
+
entries: [VIRTUAL_IDS.rsc],
|
|
547
|
+
// Pre-bundle React for RSC to ensure single instance
|
|
548
|
+
include: ["react", "react/jsx-runtime"],
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
}),
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
configResolved(config) {
|
|
557
|
+
// Count how many RSC base plugins there are (rsc:minimal is the main one)
|
|
558
|
+
const rscMinimalCount = config.plugins.filter(
|
|
559
|
+
(p) => p.name === "rsc:minimal"
|
|
560
|
+
).length;
|
|
561
|
+
|
|
562
|
+
if (rscMinimalCount > 1 && !hasWarnedDuplicate) {
|
|
563
|
+
hasWarnedDuplicate = true;
|
|
564
|
+
console.warn(
|
|
565
|
+
"[rsc-router] Duplicate @vitejs/plugin-rsc detected. " +
|
|
566
|
+
"Remove rsc() from your config or use rscRouter({ rsc: false }) for manual configuration."
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Add virtual entries plugin
|
|
573
|
+
plugins.push(createVirtualEntriesPlugin(finalEntries, routerPath));
|
|
574
|
+
|
|
575
|
+
// Add the RSC plugin directly
|
|
576
|
+
// Cast to PluginOption to handle type differences between bundled vite types
|
|
577
|
+
plugins.push(
|
|
578
|
+
rsc({
|
|
579
|
+
entries: finalEntries,
|
|
580
|
+
}) as PluginOption
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (enableExposeActionId) {
|
|
586
|
+
plugins.push(exposeActionId());
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Always add exposeLoaderId for GET-based loader fetching with useFetchLoader
|
|
590
|
+
plugins.push(exposeLoaderId());
|
|
591
|
+
|
|
592
|
+
// Always add exposeHandleId for auto-generated handle IDs
|
|
593
|
+
plugins.push(exposeHandleId());
|
|
594
|
+
|
|
595
|
+
// Always add exposeLocationStateId for auto-generated location state keys
|
|
596
|
+
plugins.push(exposeLocationStateId());
|
|
597
|
+
|
|
598
|
+
// Add version virtual module plugin for cache invalidation
|
|
599
|
+
plugins.push(createVersionPlugin());
|
|
600
|
+
|
|
601
|
+
// Add version injector for custom entry.rsc files
|
|
602
|
+
if (rscEntryPath) {
|
|
603
|
+
plugins.push(createVersionInjectorPlugin(rscEntryPath));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return plugins;
|
|
607
|
+
}
|
|
608
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for rsc-router:version virtual module.
|
|
3
|
+
* This module is provided by the Vite plugin at build/dev time.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare module "rsc-router:version" {
|
|
7
|
+
/**
|
|
8
|
+
* Auto-generated version string for cache invalidation.
|
|
9
|
+
* Changes on server restart (dev) or build (prod).
|
|
10
|
+
*/
|
|
11
|
+
export const VERSION: string;
|
|
12
|
+
}
|