vite-plugin-react-server 0.1.0

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 (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +289 -0
  3. package/dist/build/createBuildConfig.d.ts +12 -0
  4. package/dist/build/createBuildConfig.d.ts.map +1 -0
  5. package/dist/build/createBuildConfig.js +55 -0
  6. package/dist/build/createBuildConfig.js.map +1 -0
  7. package/dist/checkFilesExist.d.ts +8 -0
  8. package/dist/checkFilesExist.d.ts.map +1 -0
  9. package/dist/checkFilesExist.js +61 -0
  10. package/dist/checkFilesExist.js.map +1 -0
  11. package/dist/collect-css-manifest.d.ts +4 -0
  12. package/dist/collect-css-manifest.d.ts.map +1 -0
  13. package/dist/collect-css-manifest.js +57 -0
  14. package/dist/collect-css-manifest.js.map +1 -0
  15. package/dist/components.d.ts +13 -0
  16. package/dist/components.d.ts.map +1 -0
  17. package/dist/components.js +13 -0
  18. package/dist/components.js.map +1 -0
  19. package/dist/copy-dir.d.ts +4 -0
  20. package/dist/copy-dir.d.ts.map +1 -0
  21. package/dist/getEnv.d.ts +19 -0
  22. package/dist/getEnv.d.ts.map +1 -0
  23. package/dist/getEnv.js +76 -0
  24. package/dist/getEnv.js.map +1 -0
  25. package/dist/helpers/normalizedRelativePath.d.ts +9 -0
  26. package/dist/helpers/normalizedRelativePath.d.ts.map +1 -0
  27. package/dist/helpers/normalizedRelativePath.js +31 -0
  28. package/dist/helpers/normalizedRelativePath.js.map +1 -0
  29. package/dist/helpers/tryManifest.d.ts +8 -0
  30. package/dist/helpers/tryManifest.d.ts.map +1 -0
  31. package/dist/html/createPageLoader.d.ts +26 -0
  32. package/dist/html/createPageLoader.d.ts.map +1 -0
  33. package/dist/html/createPageLoader.js +70 -0
  34. package/dist/html/createPageLoader.js.map +1 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +5 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/manifest.d.ts +6 -0
  40. package/dist/manifest.d.ts.map +1 -0
  41. package/dist/module-graph.d.ts +10 -0
  42. package/dist/module-graph.d.ts.map +1 -0
  43. package/dist/options.d.ts +86 -0
  44. package/dist/options.d.ts.map +1 -0
  45. package/dist/options.js +251 -0
  46. package/dist/options.js.map +1 -0
  47. package/dist/plugin.d.ts +8 -0
  48. package/dist/plugin.d.ts.map +1 -0
  49. package/dist/plugin.js +31 -0
  50. package/dist/plugin.js.map +1 -0
  51. package/dist/react-client/plugin.d.ts +4 -0
  52. package/dist/react-client/plugin.d.ts.map +1 -0
  53. package/dist/react-client/plugin.js +28 -0
  54. package/dist/react-client/plugin.js.map +1 -0
  55. package/dist/react-server/createDevMiddleware.d.ts +8 -0
  56. package/dist/react-server/createDevMiddleware.d.ts.map +1 -0
  57. package/dist/react-server/createDevServer.d.ts +4 -0
  58. package/dist/react-server/createDevServer.d.ts.map +1 -0
  59. package/dist/react-server/createHandler.d.ts +23 -0
  60. package/dist/react-server/createHandler.d.ts.map +1 -0
  61. package/dist/react-server/createHandler.js +110 -0
  62. package/dist/react-server/createHandler.js.map +1 -0
  63. package/dist/react-server/createReactNodeStreamer.d.ts +10 -0
  64. package/dist/react-server/createReactNodeStreamer.d.ts.map +1 -0
  65. package/dist/react-server/createRscStream.d.ts +4 -0
  66. package/dist/react-server/createRscStream.d.ts.map +1 -0
  67. package/dist/react-server/createRscStream.js +47 -0
  68. package/dist/react-server/createRscStream.js.map +1 -0
  69. package/dist/react-server/createSsrHandler.d.ts +4 -0
  70. package/dist/react-server/createSsrHandler.d.ts.map +1 -0
  71. package/dist/react-server/plugin.d.ts +8 -0
  72. package/dist/react-server/plugin.d.ts.map +1 -0
  73. package/dist/react-server/plugin.js +298 -0
  74. package/dist/react-server/plugin.js.map +1 -0
  75. package/dist/resolvePage.d.ts +19 -0
  76. package/dist/resolvePage.d.ts.map +1 -0
  77. package/dist/resolvePage.js +44 -0
  78. package/dist/resolvePage.js.map +1 -0
  79. package/dist/resolveProps.d.ts +19 -0
  80. package/dist/resolveProps.d.ts.map +1 -0
  81. package/dist/resolveProps.js +90 -0
  82. package/dist/resolveProps.js.map +1 -0
  83. package/dist/server.d.ts +2 -0
  84. package/dist/server.d.ts.map +1 -0
  85. package/dist/transformer/index.d.ts +28 -0
  86. package/dist/transformer/index.d.ts.map +1 -0
  87. package/dist/transformer/index.js +54 -0
  88. package/dist/transformer/index.js.map +1 -0
  89. package/dist/transformer/preserveDirectives.d.ts +4 -0
  90. package/dist/transformer/preserveDirectives.d.ts.map +1 -0
  91. package/dist/transformer/preserveDirectives.js +72 -0
  92. package/dist/transformer/preserveDirectives.js.map +1 -0
  93. package/dist/transformer/preserver.d.ts +2 -0
  94. package/dist/transformer/preserver.d.ts.map +1 -0
  95. package/dist/transformer/transformer.d.ts +30 -0
  96. package/dist/transformer/transformer.d.ts.map +1 -0
  97. package/dist/transformer/transformer.js +80 -0
  98. package/dist/transformer/transformer.js.map +1 -0
  99. package/dist/transformer/types.d.ts +15 -0
  100. package/dist/transformer/types.d.ts.map +1 -0
  101. package/dist/types.d.ts +197 -0
  102. package/dist/types.d.ts.map +1 -0
  103. package/dist/worker/createHtmlStream.d.ts +7 -0
  104. package/dist/worker/createHtmlStream.d.ts.map +1 -0
  105. package/dist/worker/createWorker.d.ts +3 -0
  106. package/dist/worker/createWorker.d.ts.map +1 -0
  107. package/dist/worker/createWorker.js +33 -0
  108. package/dist/worker/createWorker.js.map +1 -0
  109. package/dist/worker/loader.d.ts +15 -0
  110. package/dist/worker/loader.d.ts.map +1 -0
  111. package/dist/worker/renderPages.d.ts +18 -0
  112. package/dist/worker/renderPages.d.ts.map +1 -0
  113. package/dist/worker/renderPages.js +99 -0
  114. package/dist/worker/renderPages.js.map +1 -0
  115. package/dist/worker/types.d.ts +31 -0
  116. package/dist/worker/types.d.ts.map +1 -0
  117. package/dist/worker/worker.d.ts +7 -0
  118. package/dist/worker/worker.d.ts.map +1 -0
  119. package/package.json +116 -0
  120. package/src/build/createBuildConfig.ts +74 -0
  121. package/src/checkFilesExist.ts +67 -0
  122. package/src/collect-css-manifest.ts +76 -0
  123. package/src/components.tsx +14 -0
  124. package/src/copy-dir.ts +27 -0
  125. package/src/getEnv.ts +135 -0
  126. package/src/helpers/normalizedRelativePath.ts +59 -0
  127. package/src/helpers/tryManifest.ts +23 -0
  128. package/src/html/createPageLoader.ts +99 -0
  129. package/src/index.ts +4 -0
  130. package/src/manifest.ts +24 -0
  131. package/src/module-graph.ts +48 -0
  132. package/src/options.ts +351 -0
  133. package/src/plugin.ts +31 -0
  134. package/src/react-client/plugin.ts +34 -0
  135. package/src/react-server/createDevMiddleware.ts +75 -0
  136. package/src/react-server/createDevServer.ts +10 -0
  137. package/src/react-server/createHandler.ts +144 -0
  138. package/src/react-server/createReactNodeStreamer.ts +25 -0
  139. package/src/react-server/createRscStream.ts +52 -0
  140. package/src/react-server/createSsrHandler.ts +147 -0
  141. package/src/react-server/plugin.ts +349 -0
  142. package/src/resolvePage.ts +65 -0
  143. package/src/resolveProps.ts +122 -0
  144. package/src/server.tsx +0 -0
  145. package/src/transformer/README.md +44 -0
  146. package/src/transformer/index.ts +112 -0
  147. package/src/transformer/preserveDirectives.ts +100 -0
  148. package/src/transformer/preserver.ts +47 -0
  149. package/src/transformer/transformer.ts +123 -0
  150. package/src/transformer/types.ts +15 -0
  151. package/src/types.ts +245 -0
  152. package/src/worker/createHtmlStream.ts +76 -0
  153. package/src/worker/createWorker.ts +39 -0
  154. package/src/worker/loader.ts +16 -0
  155. package/src/worker/renderPages.ts +144 -0
  156. package/src/worker/types.ts +38 -0
  157. package/src/worker/worker.tsx +136 -0
  158. package/tsconfig.json +79 -0
@@ -0,0 +1,349 @@
1
+ import { readFileSync } from "node:fs";
2
+ import type { ServerResponse } from "node:http";
3
+ import { resolve as resolvePath } from "node:path";
4
+ import { performance } from "node:perf_hooks";
5
+ import { Worker } from "node:worker_threads";
6
+ import type { Plugin as RollupPlugin } from "rollup";
7
+ import type { Manifest, Plugin as VitePlugin } from "vite";
8
+ import {
9
+ createLogger,
10
+ type ResolvedConfig,
11
+ type UserConfig,
12
+ type ViteDevServer,
13
+ } from "vite";
14
+ import { createBuildConfig } from "../build/createBuildConfig.js";
15
+ import { checkFilesExist } from "../checkFilesExist.js";
16
+ import { getEnv } from "../getEnv.js";
17
+ import { createPageLoader } from "../html/createPageLoader.js";
18
+ import { renderPages } from "../worker/renderPages.js";
19
+ import { resolveOptions, resolvePages, resolveUserConfig } from "../options.js";
20
+ import type { BuildTiming, ReactStreamPluginMeta } from "../types.js";
21
+ import { type StreamPluginOptions } from "../types.js";
22
+ import { createWorker } from "../worker/createWorker.js";
23
+ import { createHandler } from "./createHandler.js";
24
+
25
+ let pageSet: Set<string>;
26
+ let pageMap: Map<string, string>;
27
+ let worker: Worker;
28
+ let config: ResolvedConfig;
29
+ let cssModules = new Set<string>();
30
+ let clientComponents = new Map<string, string>();
31
+ let define: Record<string, string>;
32
+ let buildCssFiles = new Set<string>();
33
+
34
+ interface BuildStats {
35
+ htmlFiles: number;
36
+ clientComponents: number;
37
+ cssFiles: number;
38
+ totalRoutes: number;
39
+ timing: {
40
+ config: number;
41
+ build: number;
42
+ render: number;
43
+ total: number;
44
+ };
45
+ }
46
+
47
+ export async function reactStreamPlugin(
48
+ options: StreamPluginOptions = {} as StreamPluginOptions
49
+ ): Promise<VitePlugin & RollupPlugin & { meta: ReactStreamPluginMeta }> {
50
+ const timing: BuildTiming = {
51
+ start: performance.now(),
52
+ };
53
+ const resolvedOptions = resolveOptions(options);
54
+ if (resolvedOptions.type === "error") {
55
+ console.error(
56
+ "[vite-react-stream:server] Error resolving userOptions. Please check your userOptions."
57
+ );
58
+ throw resolvedOptions.error;
59
+ }
60
+ const { userOptions } = resolvedOptions;
61
+ return {
62
+ name: "vite:react-stream",
63
+ meta: {
64
+ timing,
65
+ } as ReactStreamPluginMeta,
66
+ api: {
67
+ addCssFile(path: string) {
68
+ buildCssFiles.add(path);
69
+ },
70
+ },
71
+ configResolved(resolvedConfig) {
72
+ if (resolvedConfig.command === "build") {
73
+ timing.configResolved = performance.now();
74
+ console.log("[vite-react-stream] Starting build...");
75
+ }
76
+ config = resolvedConfig;
77
+ },
78
+ async configureServer(server: ViteDevServer) {
79
+ if (server.config.root) {
80
+ console.log(
81
+ "[vite-react-stream] Root dir changed",
82
+ server.config.root,
83
+ server.config.root
84
+ );
85
+ }
86
+
87
+ const activeStreams = new Set<ServerResponse>();
88
+
89
+ // Handle Vite server restarts
90
+ server.ws.on("restart", (path) => {
91
+ console.log(
92
+ "[vite-react-stream] 🔧 Plugin changed, preparing for restart:",
93
+ path
94
+ );
95
+
96
+ // Close streams with restart message
97
+ for (const res of activeStreams) {
98
+ res.writeHead(503, {
99
+ "Content-Type": "text/x-component",
100
+ "Retry-After": "1",
101
+ });
102
+ res.end('{"error":"Server restarting..."}');
103
+ }
104
+ activeStreams.clear();
105
+ });
106
+
107
+ server.ws.on("connection", (socket, req) => {
108
+ console.log("[vite-react-stream] hooking up ws connection");
109
+ });
110
+
111
+ server.ws.on("listening", () => {
112
+ console.log("[vite-react-stream] hooking up ws listening");
113
+ });
114
+
115
+ server.middlewares.use(async (req, res, next) => {
116
+ if (req.headers.accept !== "text/x-component") return next();
117
+ console.log("[vite-react-stream] middleware called");
118
+ try {
119
+ const handler = await createHandler(
120
+ req.url ?? "",
121
+ {
122
+ Page: userOptions.Page,
123
+ props: userOptions.props,
124
+ build: userOptions.build,
125
+ Html: ({ children }) => children,
126
+ pageExportName: userOptions.pageExportName,
127
+ propsExportName: userOptions.propsExportName,
128
+ moduleBase: userOptions.moduleBase,
129
+ moduleBasePath: userOptions.moduleBasePath,
130
+ projectRoot: server.config.root,
131
+ },
132
+ {
133
+ cssFiles: Array.from(cssModules),
134
+ logger: createLogger(),
135
+ loader: server.ssrLoadModule,
136
+ moduleGraph: server.moduleGraph,
137
+ }
138
+ );
139
+ handler?.stream?.pipe(res);
140
+ } finally {
141
+ res.on("close", () => {
142
+ console.log("[vite-react-stream] ➖ Stream closed for:", req.url);
143
+ activeStreams.delete(res);
144
+ });
145
+ }
146
+ });
147
+ },
148
+
149
+ async config(config, configEnv): Promise<UserConfig> {
150
+ const resolvedPages = await resolvePages(userOptions.build.pages);
151
+ if(resolvedPages.type === 'error') {
152
+ throw resolvedPages.error;
153
+ }
154
+ const { pages } = resolvedPages;
155
+ const resolvedConfig = resolveUserConfig(
156
+ "react-server",
157
+ pages,
158
+ config,
159
+ configEnv,
160
+ userOptions
161
+ );
162
+ if(resolvedConfig.type === "error") {
163
+ throw resolvedConfig.error;
164
+ }
165
+ const { userConfig } = resolvedConfig;
166
+
167
+ const envResult = getEnv(userConfig, configEnv);
168
+ const files = await checkFilesExist(pages, userOptions, userConfig.root);
169
+ pageSet = files.pageSet;
170
+ pageMap = files.pageMap;
171
+ define = envResult.define;
172
+ const entries = Array.from(
173
+ new Set([
174
+ ...Array.from(files.pageSet.values()),
175
+ ...Array.from(files.propsSet.values()),
176
+ ]).values()
177
+ );
178
+
179
+ const buildConfig = createBuildConfig({
180
+ root: config.root ?? process.cwd(),
181
+ base: config.base ?? envResult.publicUrl,
182
+ outDir: config.build?.outDir ?? "dist/server",
183
+ entries,
184
+ });
185
+ return {
186
+ ...buildConfig,
187
+ define,
188
+ plugins: config.plugins,
189
+ };
190
+ },
191
+ async buildStart() {
192
+ timing.buildStart = performance.now();
193
+ },
194
+ async closeBundle() {
195
+ if(!config) return;
196
+ console.log("RSC CLOSE BUNDLE CALLED");
197
+ if (!pageSet?.size) return;
198
+ timing.renderStart = performance.now();
199
+
200
+ try {
201
+ const manifest = JSON.parse(
202
+ readFileSync(
203
+ resolvePath(
204
+ config.root,
205
+ config.build.outDir,
206
+ ".vite",
207
+ "manifest.json"
208
+ ),
209
+ "utf-8"
210
+ )
211
+ ) as Manifest;
212
+ const clientManifest = JSON.parse(
213
+ readFileSync(
214
+ resolvePath(
215
+ config.root,
216
+ userOptions.build.client,
217
+ ".vite",
218
+ "manifest.json"
219
+ ),
220
+ "utf-8"
221
+ )
222
+ ) as Manifest;
223
+ // Create a single worker for all routes
224
+ if (!worker)
225
+ worker = await createWorker(
226
+ config.root,
227
+ config.build.outDir,
228
+ "worker.js",
229
+ process.env["NODE_ENV"] === "development" ? "development" : "production"
230
+ );
231
+ // this is based on the user config - the routes should lead to a page and props but the rendering is agnostic of that
232
+ const routes = Array.from(pageMap.keys());
233
+ const indexEntry = clientManifest["index.html"];
234
+ if (!indexEntry) {
235
+ throw new Error("root /index.html not found");
236
+ }
237
+ await renderPages(routes, {
238
+ pipableStreamOptions: {
239
+ bootstrapModules: ["/" + indexEntry.file],
240
+ },
241
+ outDir: config.build.outDir,
242
+ clientCss: indexEntry.css?.map((css) => "/" + css) ?? [],
243
+ pluginOptions: {
244
+ Page: userOptions.Page,
245
+ props: userOptions.props,
246
+ build: userOptions.build,
247
+ Html: userOptions.Html,
248
+ pageExportName: userOptions.pageExportName,
249
+ propsExportName: userOptions.propsExportName,
250
+ moduleBase: userOptions.moduleBase,
251
+ moduleBasePath: userOptions.moduleBasePath,
252
+ moduleBaseURL: userOptions.moduleBaseURL,
253
+ projectRoot: config.root,
254
+ },
255
+ worker: worker,
256
+ manifest: clientManifest as Manifest,
257
+ loader: createPageLoader({
258
+ manifest,
259
+ root: config.root,
260
+ outDir: config.build.outDir,
261
+ moduleBase: userOptions.moduleBase,
262
+ alwaysRegisterServer: false,
263
+ alwaysRegisterClient: false,
264
+ registerServer: [],
265
+ registerClient: Object.keys(clientManifest).filter(
266
+ (key) =>
267
+ key.endsWith(".client.tsx") && clientManifest[key].isEntry
268
+ ),
269
+ }),
270
+ onCssFile: (path) => buildCssFiles.add(path),
271
+ });
272
+ console.log("[vite-react-stream] Render complete");
273
+ console.log("[vite-react-stream] Terminating worker");
274
+ if (worker) await worker.terminate();
275
+
276
+ timing.renderEnd = performance.now();
277
+ timing.total = (timing.renderEnd - timing.start) / 1000;
278
+
279
+ // Collect stats
280
+ const stats: BuildStats = {
281
+ htmlFiles: routes.length,
282
+ clientComponents: clientComponents.size,
283
+ cssFiles: cssModules.size,
284
+ totalRoutes: routes.length,
285
+ timing: {
286
+ config: ((timing.configResolved ?? 0) - timing.start) / 1000,
287
+ build:
288
+ ((timing.buildStart ?? 0) - (timing.configResolved ?? 0)) / 1000,
289
+ render:
290
+ ((timing.renderEnd ?? 0) - (timing.renderStart ?? 0)) / 1000,
291
+ total: (timing.renderEnd ?? 0 - timing.start) / 1000,
292
+ },
293
+ };
294
+
295
+ // Format duration helper
296
+ const formatDuration = (seconds: number) => {
297
+ if (seconds < 0.001) {
298
+ return `${(seconds * 1000000).toFixed(0)}μs`;
299
+ }
300
+ if (seconds < 1) {
301
+ return `${(seconds * 1000).toFixed(0)}ms`;
302
+ }
303
+ return `${seconds.toFixed(2)}s`;
304
+ };
305
+
306
+ console.log("\n[vite-react-stream] Build Summary:");
307
+ console.log("─".repeat(50));
308
+ console.log(`📄 Generated ${stats.htmlFiles} HTML files`);
309
+ console.log(`🎯 Processed ${stats.clientComponents} client components`);
310
+ console.log(`🎨 Included ${stats.cssFiles} CSS files`);
311
+ console.log(`🛣️ Total routes: ${stats.totalRoutes}`);
312
+ console.log("─".repeat(50));
313
+ console.log("⏱️ Timing:");
314
+ console.log(` Config: ${formatDuration(stats.timing.config)}`);
315
+ console.log(` Build: ${formatDuration(stats.timing.build)}`);
316
+ console.log(` Render: ${formatDuration(stats.timing.render)}`);
317
+ console.log(" ".repeat(12));
318
+ console.log(` Total: ${formatDuration(stats.timing.total)}`);
319
+ console.log("─".repeat(50));
320
+ } catch (error) {
321
+ console.error("[vite-react-stream] Build failed:", error);
322
+ throw error;
323
+ }
324
+ },
325
+ async buildEnd(error) {
326
+ if (error) {
327
+ console.error("[vite-react-stream] Build error:", error);
328
+ }
329
+ if (worker) await worker.terminate();
330
+ },
331
+ handleHotUpdate({ file }) {
332
+ if (file.endsWith(".css")) {
333
+ cssModules.add(file);
334
+ }
335
+ },
336
+ transform(code: string, id: string) {
337
+ if (
338
+ (id.includes(".client") ||
339
+ code.startsWith('"use client"') ||
340
+ code.startsWith("use client")) &&
341
+ !id.includes("node_modules")
342
+ ) {
343
+ console.log("[vite-react-stream] Client component added", id);
344
+ clientComponents.set(id, code);
345
+ }
346
+ return { code };
347
+ },
348
+ };
349
+ }
@@ -0,0 +1,65 @@
1
+ type ResolvePageOptions = {
2
+ pageModule: Record<string, any>;
3
+ path: string;
4
+ url: string;
5
+ exportName: string;
6
+ };
7
+
8
+ type ResolvePageResult =
9
+ | { type: "success"; key: string; Page: any }
10
+ | { type: "error"; error: Error }
11
+ | { type: "skip" };
12
+
13
+ export async function resolvePage({
14
+ pageModule,
15
+ path,
16
+ url,
17
+ exportName,
18
+ }: ResolvePageOptions): Promise<ResolvePageResult> {
19
+ if (!pageModule) {
20
+ return {
21
+ type: "error",
22
+ error: new Error(`pageModule is ${typeof pageModule}`),
23
+ };
24
+ }
25
+ const keys =
26
+ typeof pageModule === "object" && pageModule != null
27
+ ? Object.keys(pageModule)
28
+ : [];
29
+ const found = keys.find((v) => v === exportName || v === url || v === path);
30
+ if (found) {
31
+ if (typeof pageModule[found] === "function") {
32
+ return {
33
+ type: "success",
34
+ key: found,
35
+ Page: pageModule[found],
36
+ };
37
+ } else {
38
+ if (
39
+ typeof pageModule === "object" &&
40
+ pageModule != null &&
41
+ Object.keys(pageModule).includes("type")
42
+ )
43
+ return pageModule as ResolvePageResult;
44
+ return {
45
+ type: "error",
46
+ [exportName]: () => found,
47
+ error: pageModule[found]["error"],
48
+ };
49
+ }
50
+ }
51
+ if (keys.includes("type")) return pageModule as ResolvePageResult;
52
+ return {
53
+ type: "error",
54
+ error: new Error(
55
+ `Could not find Page export "${exportName}" in "${path}". ${
56
+ typeof pageModule === "object" && pageModule != null
57
+ ? keys.length
58
+ ? "Available exports: " + keys.join(", ")
59
+ : "The object was defined but has no properties."
60
+ : "typeof pageModule =" + typeof pageModule
61
+ }`,
62
+ { cause: pageModule }
63
+ ),
64
+ };
65
+ }
@@ -0,0 +1,122 @@
1
+ type ResolvePropsOptions = {
2
+ propsModule: Record<string, any>;
3
+ path: string;
4
+ exportName: string;
5
+ url: string;
6
+ };
7
+
8
+ type ResolvePropsResult =
9
+ | { type: "success"; key: string; props: any }
10
+ | { type: "error"; error: Error }
11
+ | { type: "skip" };
12
+
13
+ function isFunction(value: any) {
14
+ return typeof value === "function";
15
+ }
16
+
17
+ export async function resolveProps({
18
+ propsModule,
19
+ path,
20
+ exportName,
21
+ url,
22
+ }: ResolvePropsOptions): Promise<ResolvePropsResult> {
23
+ if (!propsModule) {
24
+ return {
25
+ type: "error",
26
+ error: new Error(`propsModule is ${typeof propsModule}`),
27
+ };
28
+ }
29
+
30
+ if (typeof propsModule !== "object") {
31
+ return {
32
+ type: "error",
33
+ error: new Error(
34
+ `propsModule must be an object, got ${typeof propsModule}`
35
+ ),
36
+ };
37
+ }
38
+
39
+ const keys = Object.keys(propsModule);
40
+ const found = keys.find((v) => v === exportName || v === url || v === path);
41
+ if (found) {
42
+ const value = propsModule[found];
43
+
44
+ try {
45
+ // If it's a function, call it with the URL
46
+ if (isFunction(value)) {
47
+ const props = await value(url);
48
+ return {
49
+ type: "success",
50
+ key: found,
51
+ props,
52
+ };
53
+ }
54
+
55
+ // If it's a promise, await it
56
+ if (value && typeof value.then === "function") {
57
+ const props = await value;
58
+ return {
59
+ type: "success",
60
+ key: found,
61
+ props,
62
+ };
63
+ }
64
+
65
+ // If it's a plain object, use it directly
66
+ if (typeof value === "object" && value !== null) {
67
+ return {
68
+ type: "success",
69
+ key: found,
70
+ props: value,
71
+ };
72
+ }
73
+
74
+ console.warn(found, "error in resolveProps", propsModule, url, path);
75
+ return {
76
+ type: "error",
77
+ error: new Error(
78
+ `Expected props export "${exportName}" in "${path}" to be a function, promise, or object that resolves to props, instead got typeof ${typeof value}.`
79
+ ),
80
+ };
81
+ } catch (error) {
82
+ console.warn(found, "error in resolveProps", propsModule, url, path);
83
+ return {
84
+ type: "error",
85
+ error: error as Error,
86
+ };
87
+ }
88
+ }
89
+ const commonjs = keys.find((v) => v === "exports");
90
+
91
+ if (!!commonjs) {
92
+ const exportKeys = (commonjs as unknown as { exports: any })["exports"]
93
+ ? Object.keys((commonjs as unknown as { exports: any })["exports"])
94
+ : [];
95
+ const foundCommonJS = exportKeys.find(
96
+ (v) => v === exportName || v === url || v === path
97
+ );
98
+ return {
99
+ type: "error",
100
+ error: new Error(
101
+ `Expected props export "${exportName}" in "${path}", but instead got "exports" with ${
102
+ !!foundCommonJS
103
+ ? foundCommonJS.toString()
104
+ : exportKeys.length
105
+ ? exportKeys.join(", ")
106
+ : "no keys"
107
+ }, this will not work. Make sure to set esModule: true in rollupOptions.output`
108
+ ),
109
+ };
110
+ }
111
+
112
+ return {
113
+ type: "error",
114
+ error: new Error(
115
+ `Could not find props export "${exportName}" in "${path}". ${
116
+ keys.length
117
+ ? "Available exports: " + keys.join(", ")
118
+ : "The object was defined but has no properties."
119
+ }`
120
+ ),
121
+ };
122
+ }
package/src/server.tsx ADDED
File without changes
@@ -0,0 +1,44 @@
1
+ # Vite React Client Transform plugin
2
+
3
+ When consuming such a client file, you need to call vite with the react-server condition.
4
+
5
+ ```json
6
+ {
7
+ "scripts":{
8
+ "build": "NODE_OPTIONS='--conditions react-server' vite build",
9
+ "dev": "NODE_OPTIONS='--conditions react-server' vite",
10
+ }
11
+ }
12
+ ```
13
+
14
+ ## Key Concepts
15
+
16
+ 1. **Environment Separation**
17
+ - The main thread runs with Vite's `react-server` condition
18
+ - HTML rendering must happen in a clean Node environment (worker thread)
19
+ - Worker needs `NODE_OPTIONS=''` to avoid inheriting Vite's conditions
20
+
21
+ 2. **RSC Stream Flow**
22
+ ```
23
+ Main Thread (Vite) Worker Thread (Clean Node) Vite Dev Thread (Browser)
24
+ --------------- --------------------- ---------------------
25
+ RSC Stream → HTML Rendering React Client
26
+ (react-server) → (react-dom/server.node) (react-dom/client)
27
+ → (react-server-dom-esm/client.node) (react-server-dom-esm/client.browser)
28
+ ```
29
+
30
+ 3. **Request Handling**
31
+ - Skip Vite's internal requests (`/@vite/client`, etc.)
32
+ - Handle HTML and directory requests
33
+ - Pass through static files
34
+
35
+ 4. **Module Resolution**
36
+ - Client components need proper import maps
37
+ - Use Vite's module resolution for dependencies
38
+ - Bootstrap modules must be properly resolved
39
+
40
+ 5. **Common Pitfalls**
41
+ - Don't try to use `react-dom/server.node` in the main thread
42
+ - Don't mix React server/client conditions in the same environment
43
+ - Be careful with stream handling between threads
44
+ - Watch for proper module resolution in both environments
@@ -0,0 +1,112 @@
1
+ import type { Plugin } from "vite";
2
+ import { normalizePath } from "vite";
3
+ import { DEFAULT_CONFIG } from "../options.js";
4
+ import { createRscTransformer } from "./transformer.js";
5
+ import type { ViteReactClientTransformOptions } from "./types.js";
6
+
7
+
8
+ /**
9
+ * Plugin for transforming React Client Components.
10
+ *
11
+ * Core responsibilities:
12
+ * 1. Detects "use client" directives
13
+ * 2. Transforms client components for RSC boundaries
14
+ * 3. Adds client reference metadata for RSC
15
+ *
16
+ * When a component is marked with "use client", it:
17
+ * - Gets transformed into a client reference
18
+ * - Maintains module ID for RSC boundaries
19
+ * - Preserves class/function behavior
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * export default defineConfig({
24
+ * plugins: [
25
+ * viteReactClientTransformPlugin({
26
+ * projectRoot: process.cwd(),
27
+ * })
28
+ * ]
29
+ * });
30
+ * ```
31
+ */
32
+
33
+ export function viteReactClientTransformPlugin(
34
+ options?: ViteReactClientTransformOptions
35
+ ): Plugin {
36
+ if(process.env['NODE_OPTIONS']?.match(/--conditions=react-server/)) {
37
+ console.log('react-server')
38
+ } else {
39
+ throw new Error('react-server condition not found, set NODE_OPTIONS="--conditions react-server"')
40
+ }
41
+ const projectRoot = options?.projectRoot || process.cwd();
42
+ const include = options?.include || DEFAULT_CONFIG.FILE_REGEX;
43
+ const exclude = options?.exclude;
44
+ let transform: any;
45
+ // get the file we are imported from (parent)
46
+
47
+ return {
48
+ name: "vite:react-stream-transformer",
49
+ enforce: "pre",
50
+
51
+ configResolved(config) {
52
+ transform = createRscTransformer({
53
+ moduleId:
54
+ options?.moduleId ||
55
+ moduleIdDefault({
56
+ projectRoot: projectRoot,
57
+ output: {
58
+ dir: config.build?.outDir ?? DEFAULT_CONFIG.BUILD.server,
59
+ },
60
+ isProduction: config.isProduction,
61
+ }),
62
+ }).transform;
63
+ },
64
+
65
+ transform(code: string, id: string, opts) {
66
+ // Skip if file doesn't match patterns
67
+ if (
68
+ !matchPattern(id, include) ||
69
+ (exclude && matchPattern(id, exclude))
70
+ ) {
71
+ return null;
72
+ }
73
+
74
+ // Look for use client directive at start of file (no exceptions)
75
+ const directiveMatch =
76
+ code.startsWith('"use client"') || code.startsWith("'use client'");
77
+ if (!directiveMatch) return null;
78
+
79
+ // Transform client components
80
+ return transform(code, id, opts);
81
+ },
82
+ };
83
+ }
84
+
85
+ const moduleIdDefault =
86
+ ({
87
+ projectRoot,
88
+ output: _,
89
+ isProduction,
90
+ }: {
91
+ isProduction: boolean;
92
+ projectRoot: string;
93
+ output: { dir: string };
94
+ }) =>
95
+ (moduleId: string) => {
96
+ const normalized = normalizePath(moduleId);
97
+ const noRoot = normalized.startsWith(projectRoot)
98
+ ? normalized.slice(projectRoot.length)
99
+ : normalized;
100
+ if (!isProduction) {
101
+ return noRoot;
102
+ }
103
+ return noRoot.replace(DEFAULT_CONFIG.FILE_REGEX, ".js");
104
+ };
105
+
106
+ const matchPattern = (
107
+ file: string,
108
+ pattern: string | RegExp | (string | RegExp)[]
109
+ ) =>
110
+ Array.isArray(pattern)
111
+ ? pattern.some((p) => file.match(p as RegExp))
112
+ : file.match(pattern as RegExp);