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.
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/dist/build/createBuildConfig.d.ts +12 -0
- package/dist/build/createBuildConfig.d.ts.map +1 -0
- package/dist/build/createBuildConfig.js +55 -0
- package/dist/build/createBuildConfig.js.map +1 -0
- package/dist/checkFilesExist.d.ts +8 -0
- package/dist/checkFilesExist.d.ts.map +1 -0
- package/dist/checkFilesExist.js +61 -0
- package/dist/checkFilesExist.js.map +1 -0
- package/dist/collect-css-manifest.d.ts +4 -0
- package/dist/collect-css-manifest.d.ts.map +1 -0
- package/dist/collect-css-manifest.js +57 -0
- package/dist/collect-css-manifest.js.map +1 -0
- package/dist/components.d.ts +13 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +13 -0
- package/dist/components.js.map +1 -0
- package/dist/copy-dir.d.ts +4 -0
- package/dist/copy-dir.d.ts.map +1 -0
- package/dist/getEnv.d.ts +19 -0
- package/dist/getEnv.d.ts.map +1 -0
- package/dist/getEnv.js +76 -0
- package/dist/getEnv.js.map +1 -0
- package/dist/helpers/normalizedRelativePath.d.ts +9 -0
- package/dist/helpers/normalizedRelativePath.d.ts.map +1 -0
- package/dist/helpers/normalizedRelativePath.js +31 -0
- package/dist/helpers/normalizedRelativePath.js.map +1 -0
- package/dist/helpers/tryManifest.d.ts +8 -0
- package/dist/helpers/tryManifest.d.ts.map +1 -0
- package/dist/html/createPageLoader.d.ts +26 -0
- package/dist/html/createPageLoader.d.ts.map +1 -0
- package/dist/html/createPageLoader.js +70 -0
- package/dist/html/createPageLoader.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +6 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/module-graph.d.ts +10 -0
- package/dist/module-graph.d.ts.map +1 -0
- package/dist/options.d.ts +86 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +251 -0
- package/dist/options.js.map +1 -0
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +31 -0
- package/dist/plugin.js.map +1 -0
- package/dist/react-client/plugin.d.ts +4 -0
- package/dist/react-client/plugin.d.ts.map +1 -0
- package/dist/react-client/plugin.js +28 -0
- package/dist/react-client/plugin.js.map +1 -0
- package/dist/react-server/createDevMiddleware.d.ts +8 -0
- package/dist/react-server/createDevMiddleware.d.ts.map +1 -0
- package/dist/react-server/createDevServer.d.ts +4 -0
- package/dist/react-server/createDevServer.d.ts.map +1 -0
- package/dist/react-server/createHandler.d.ts +23 -0
- package/dist/react-server/createHandler.d.ts.map +1 -0
- package/dist/react-server/createHandler.js +110 -0
- package/dist/react-server/createHandler.js.map +1 -0
- package/dist/react-server/createReactNodeStreamer.d.ts +10 -0
- package/dist/react-server/createReactNodeStreamer.d.ts.map +1 -0
- package/dist/react-server/createRscStream.d.ts +4 -0
- package/dist/react-server/createRscStream.d.ts.map +1 -0
- package/dist/react-server/createRscStream.js +47 -0
- package/dist/react-server/createRscStream.js.map +1 -0
- package/dist/react-server/createSsrHandler.d.ts +4 -0
- package/dist/react-server/createSsrHandler.d.ts.map +1 -0
- package/dist/react-server/plugin.d.ts +8 -0
- package/dist/react-server/plugin.d.ts.map +1 -0
- package/dist/react-server/plugin.js +298 -0
- package/dist/react-server/plugin.js.map +1 -0
- package/dist/resolvePage.d.ts +19 -0
- package/dist/resolvePage.d.ts.map +1 -0
- package/dist/resolvePage.js +44 -0
- package/dist/resolvePage.js.map +1 -0
- package/dist/resolveProps.d.ts +19 -0
- package/dist/resolveProps.d.ts.map +1 -0
- package/dist/resolveProps.js +90 -0
- package/dist/resolveProps.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/transformer/index.d.ts +28 -0
- package/dist/transformer/index.d.ts.map +1 -0
- package/dist/transformer/index.js +54 -0
- package/dist/transformer/index.js.map +1 -0
- package/dist/transformer/preserveDirectives.d.ts +4 -0
- package/dist/transformer/preserveDirectives.d.ts.map +1 -0
- package/dist/transformer/preserveDirectives.js +72 -0
- package/dist/transformer/preserveDirectives.js.map +1 -0
- package/dist/transformer/preserver.d.ts +2 -0
- package/dist/transformer/preserver.d.ts.map +1 -0
- package/dist/transformer/transformer.d.ts +30 -0
- package/dist/transformer/transformer.d.ts.map +1 -0
- package/dist/transformer/transformer.js +80 -0
- package/dist/transformer/transformer.js.map +1 -0
- package/dist/transformer/types.d.ts +15 -0
- package/dist/transformer/types.d.ts.map +1 -0
- package/dist/types.d.ts +197 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/worker/createHtmlStream.d.ts +7 -0
- package/dist/worker/createHtmlStream.d.ts.map +1 -0
- package/dist/worker/createWorker.d.ts +3 -0
- package/dist/worker/createWorker.d.ts.map +1 -0
- package/dist/worker/createWorker.js +33 -0
- package/dist/worker/createWorker.js.map +1 -0
- package/dist/worker/loader.d.ts +15 -0
- package/dist/worker/loader.d.ts.map +1 -0
- package/dist/worker/renderPages.d.ts +18 -0
- package/dist/worker/renderPages.d.ts.map +1 -0
- package/dist/worker/renderPages.js +99 -0
- package/dist/worker/renderPages.js.map +1 -0
- package/dist/worker/types.d.ts +31 -0
- package/dist/worker/types.d.ts.map +1 -0
- package/dist/worker/worker.d.ts +7 -0
- package/dist/worker/worker.d.ts.map +1 -0
- package/package.json +116 -0
- package/src/build/createBuildConfig.ts +74 -0
- package/src/checkFilesExist.ts +67 -0
- package/src/collect-css-manifest.ts +76 -0
- package/src/components.tsx +14 -0
- package/src/copy-dir.ts +27 -0
- package/src/getEnv.ts +135 -0
- package/src/helpers/normalizedRelativePath.ts +59 -0
- package/src/helpers/tryManifest.ts +23 -0
- package/src/html/createPageLoader.ts +99 -0
- package/src/index.ts +4 -0
- package/src/manifest.ts +24 -0
- package/src/module-graph.ts +48 -0
- package/src/options.ts +351 -0
- package/src/plugin.ts +31 -0
- package/src/react-client/plugin.ts +34 -0
- package/src/react-server/createDevMiddleware.ts +75 -0
- package/src/react-server/createDevServer.ts +10 -0
- package/src/react-server/createHandler.ts +144 -0
- package/src/react-server/createReactNodeStreamer.ts +25 -0
- package/src/react-server/createRscStream.ts +52 -0
- package/src/react-server/createSsrHandler.ts +147 -0
- package/src/react-server/plugin.ts +349 -0
- package/src/resolvePage.ts +65 -0
- package/src/resolveProps.ts +122 -0
- package/src/server.tsx +0 -0
- package/src/transformer/README.md +44 -0
- package/src/transformer/index.ts +112 -0
- package/src/transformer/preserveDirectives.ts +100 -0
- package/src/transformer/preserver.ts +47 -0
- package/src/transformer/transformer.ts +123 -0
- package/src/transformer/types.ts +15 -0
- package/src/types.ts +245 -0
- package/src/worker/createHtmlStream.ts +76 -0
- package/src/worker/createWorker.ts +39 -0
- package/src/worker/loader.ts +16 -0
- package/src/worker/renderPages.ts +144 -0
- package/src/worker/types.ts +38 -0
- package/src/worker/worker.tsx +136 -0
- 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);
|