vite-plugin-react-server 1.3.5 → 1.4.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 (52) hide show
  1. package/README.md +32 -18
  2. package/dist/package.json +4 -2
  3. package/dist/plugin/config/resolveOptions.d.ts.map +1 -1
  4. package/dist/plugin/config/resolveOptions.js +4 -2
  5. package/dist/plugin/dev-server/plugin.client.d.ts.map +1 -1
  6. package/dist/plugin/dev-server/plugin.client.js +13 -3
  7. package/dist/plugin/dev-server/plugin.server.d.ts.map +1 -1
  8. package/dist/plugin/dev-server/plugin.server.js +54 -4
  9. package/dist/plugin/react-static/plugin.server.d.ts.map +1 -1
  10. package/dist/plugin/react-static/plugin.server.js +9 -1
  11. package/dist/plugin/react-static/renderPagesBatched.d.ts.map +1 -1
  12. package/dist/plugin/react-static/renderPagesBatched.js +136 -36
  13. package/dist/plugin/react-static/types.d.ts +1 -0
  14. package/dist/plugin/react-static/types.d.ts.map +1 -1
  15. package/dist/plugin/types.d.ts +15 -0
  16. package/dist/plugin/types.d.ts.map +1 -1
  17. package/dist/plugin/utils/createReactFetcher.js +24 -2
  18. package/dist/plugin/utils/index.server.d.ts +0 -1
  19. package/dist/plugin/utils/index.server.d.ts.map +1 -1
  20. package/dist/plugin/utils/index.server.js +3 -4
  21. package/dist/plugin/utils/useRscHmr.js +10 -2
  22. package/dist/plugin/vendor/register-vendor.js +1 -1
  23. package/dist/plugin/vendor/vendor-alias.d.ts +5 -2
  24. package/dist/plugin/vendor/vendor-alias.d.ts.map +1 -1
  25. package/dist/plugin/vendor/vendor-alias.js +60 -26
  26. package/dist/plugin/vendor/vendor.server.d.ts.map +1 -1
  27. package/dist/plugin/vendor/vendor.server.js +2 -2
  28. package/dist/plugin/vendor/vendor.static.d.ts.map +1 -1
  29. package/dist/plugin/vendor/vendor.static.js +2 -2
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.development.js +1 -1
  32. package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.production.js +1 -1
  33. package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.node.development.js +1 -1
  34. package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.node.production.js +1 -1
  35. package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +1 -1
  36. package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.production.js +1 -1
  37. package/oss-experimental/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js +1 -1
  38. package/oss-experimental/react-server-dom-esm/esm/react-server-dom-esm-client.browser.production.js +1 -1
  39. package/oss-experimental/react-server-dom-esm/package.json +3 -3
  40. package/package.json +4 -2
  41. package/plugin/config/resolveOptions.ts +2 -0
  42. package/plugin/dev-server/plugin.client.ts +13 -2
  43. package/plugin/dev-server/plugin.server.ts +71 -4
  44. package/plugin/react-static/plugin.server.ts +11 -1
  45. package/plugin/react-static/renderPagesBatched.ts +148 -39
  46. package/plugin/react-static/types.ts +1 -0
  47. package/plugin/types.ts +15 -0
  48. package/plugin/utils/index.server.ts +3 -4
  49. package/plugin/vendor/register-vendor.ts +1 -1
  50. package/plugin/vendor/vendor-alias.ts +61 -39
  51. package/plugin/vendor/vendor.server.ts +3 -3
  52. package/plugin/vendor/vendor.static.ts +3 -2
@@ -50,7 +50,7 @@
50
50
  var existingPromise = asyncModuleCache.get(metadata.specifier);
51
51
  if (existingPromise)
52
52
  return "fulfilled" === existingPromise.status ? null : existingPromise;
53
- var modulePromise = import(metadata.specifier);
53
+ var modulePromise = import(/* @vite-ignore */ metadata.specifier);
54
54
  modulePromise.then(
55
55
  function (value) {
56
56
  modulePromise.status = "fulfilled";
@@ -26,7 +26,7 @@ function preloadModule(metadata) {
26
26
  var existingPromise = asyncModuleCache.get(metadata.specifier);
27
27
  if (existingPromise)
28
28
  return "fulfilled" === existingPromise.status ? null : existingPromise;
29
- var modulePromise = import(metadata.specifier);
29
+ var modulePromise = import(/* @vite-ignore */ metadata.specifier);
30
30
  modulePromise.then(
31
31
  function (value) {
32
32
  modulePromise.status = "fulfilled";
@@ -50,7 +50,7 @@
50
50
  var existingPromise = asyncModuleCache.get(metadata.specifier);
51
51
  if (existingPromise)
52
52
  return "fulfilled" === existingPromise.status ? null : existingPromise;
53
- var modulePromise = import(metadata.specifier);
53
+ var modulePromise = import(/* @vite-ignore */ metadata.specifier);
54
54
  modulePromise.then(
55
55
  function (value) {
56
56
  modulePromise.status = "fulfilled";
@@ -27,7 +27,7 @@ function preloadModule(metadata) {
27
27
  var existingPromise = asyncModuleCache.get(metadata.specifier);
28
28
  if (existingPromise)
29
29
  return "fulfilled" === existingPromise.status ? null : existingPromise;
30
- var modulePromise = import(metadata.specifier);
30
+ var modulePromise = import(/* @vite-ignore */ metadata.specifier);
31
31
  modulePromise.then(
32
32
  function (value) {
33
33
  modulePromise.status = "fulfilled";
@@ -2854,7 +2854,7 @@
2854
2854
  var existingPromise = asyncModuleCache.get(metadata.specifier);
2855
2855
  if (existingPromise)
2856
2856
  return "fulfilled" === existingPromise.status ? null : existingPromise;
2857
- var modulePromise = import(metadata.specifier);
2857
+ var modulePromise = import(/* @vite-ignore */ metadata.specifier);
2858
2858
  modulePromise.then(
2859
2859
  function (value) {
2860
2860
  modulePromise.status = "fulfilled";
@@ -1952,7 +1952,7 @@ function preloadModule(metadata) {
1952
1952
  var existingPromise = asyncModuleCache.get(metadata.specifier);
1953
1953
  if (existingPromise)
1954
1954
  return "fulfilled" === existingPromise.status ? null : existingPromise;
1955
- var modulePromise = import(metadata.specifier);
1955
+ var modulePromise = import(/* @vite-ignore */ metadata.specifier);
1956
1956
  modulePromise.then(
1957
1957
  function (value) {
1958
1958
  modulePromise.status = "fulfilled";
@@ -122,7 +122,7 @@ function preloadModule(metadata) {
122
122
  return existingPromise;
123
123
  } else {
124
124
  // $FlowFixMe[unsupported-syntax]
125
- var modulePromise = import(metadata.specifier);
125
+ var modulePromise = import(/* @vite-ignore */ metadata.specifier);
126
126
  modulePromise.then(function (value) {
127
127
  var fulfilledThenable = modulePromise;
128
128
  fulfilledThenable.status = 'fulfilled';
@@ -55,7 +55,7 @@ function preloadModule(metadata) {
55
55
  return existingPromise;
56
56
  } else {
57
57
  // $FlowFixMe[unsupported-syntax]
58
- const modulePromise = import(metadata.specifier);
58
+ const modulePromise = import(/* @vite-ignore */ metadata.specifier);
59
59
  modulePromise.then(value => {
60
60
  const fulfilledThenable = modulePromise;
61
61
  fulfilledThenable.status = 'fulfilled';
@@ -32,12 +32,12 @@
32
32
  "./client.node": "./client.node.js",
33
33
  "./server": {
34
34
  "react-server": "./server.node.js",
35
- "default": "./server.js"
35
+ "default": "./server.node.js"
36
36
  },
37
37
  "./server.node": "./server.node.js",
38
38
  "./static": {
39
39
  "react-server": "./static.node.js",
40
- "default": "./static.js"
40
+ "default": "./static.node.js"
41
41
  },
42
42
  "./static.node": "./static.node.js",
43
43
  "./node-loader": "./esm/react-server-dom-esm-node-loader.production.js",
@@ -60,4 +60,4 @@
60
60
  "acorn-loose": "^8.3.0",
61
61
  "webpack-sources": "^3.2.0"
62
62
  }
63
- }
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-react-server",
3
- "version": "1.3.5",
3
+ "version": "1.4.0",
4
4
  "description": "Vite plugin for React Server Components (RSC)",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin/index.js",
@@ -43,7 +43,9 @@
43
43
  "./stream": "./dist/plugin/stream/index.js",
44
44
  "./stream/client": "./dist/plugin/stream/index.client.js",
45
45
  "./stream/server": "./dist/plugin/stream/index.server.js",
46
- "./env": "./dist/plugin/env/plugin.js",
46
+ "./env": {
47
+ "types": "./env.d.ts"
48
+ },
47
49
  "./config": "./dist/plugin/config/index.js",
48
50
  "./error": "./dist/plugin/error/index.js",
49
51
  "./vendor": "./dist/plugin/vendor/index.js",
@@ -668,6 +668,8 @@ export const resolveOptions: ResolveOptionsFn = function _resolveOptions(
668
668
  useHtmlWorker: options.build?.useHtmlWorker ??
669
669
  // Force useHtmlWorker to true when build.pages is explicitly configured, regardless of default logic
670
670
  (options.build?.pages && (Array.isArray(options.build.pages) || typeof options.build.pages === 'function')) ? true : DEFAULT_CONFIG.BUILD.useHtmlWorker,
671
+ renderMode: options.build?.renderMode ?? "parallel",
672
+ batchSize: options.build?.batchSize ?? 8,
671
673
  } satisfies ResolvedUserOptions["build"];
672
674
 
673
675
  // Development configuration
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import type { VitePluginFn } from "../../types.js";
2
3
  import { configureReactServer } from "./configureReactServer.client.js";
3
4
  import { resolveOptions } from "../config/resolveOptions.js";
@@ -84,11 +85,21 @@ export const vitePluginReactDevServer: VitePluginFn = function _vitePluginReactS
84
85
 
85
86
  // Normalize paths for comparison (handle both absolute and relative)
86
87
  const normalizedFile = file.replace(projectRoot, '').replace(/^\/+/, '');
87
- const isServerFile = normalizedFile.startsWith(moduleBase + '/') &&
88
+ const isSourceFile = normalizedFile.startsWith(moduleBase + '/') &&
88
89
  (file.endsWith('.tsx') || file.endsWith('.ts') || file.endsWith('.jsx') || file.endsWith('.js'));
89
90
 
91
+ // Skip client components — let @vitejs/plugin-react handle them with Fast Refresh
92
+ const isClientFile = isSourceFile && (() => {
93
+ try {
94
+ const head = readFileSync(file, 'utf-8').slice(0, 200);
95
+ return /^\s*["']use client["']/.test(head.split('\n')[0]);
96
+ } catch { return false; }
97
+ })();
98
+
99
+ const isServerFile = isSourceFile && !isClientFile;
100
+
90
101
  // Always log for debugging
91
- server.config.logger.info(`[vite-plugin-react-server] handleHotUpdate: file=${file}, normalized=${normalizedFile}, isServerFile=${isServerFile}, hasHandler=${!!hmrHandler}`);
102
+ server.config.logger.info(`[vite-plugin-react-server] handleHotUpdate: file=${file}, normalized=${normalizedFile}, isServerFile=${isServerFile}, isClientFile=${isClientFile}, hasHandler=${!!hmrHandler}`);
92
103
 
93
104
  if (isServerFile && hmrHandler) {
94
105
  isProcessingHmr = true;
@@ -2,6 +2,7 @@ import type { StreamPluginOptions } from "../../types.js";
2
2
  import { configureReactServer } from "./configureReactServer.server.js";
3
3
  import { resolveOptions } from "../config/resolveOptions.js";
4
4
  import type { Plugin, ViteDevServer } from "vite";
5
+ import { readFileSync } from "node:fs";
5
6
 
6
7
  /**
7
8
  * Dev server plugin for server environment.
@@ -26,15 +27,74 @@ export const vitePluginReactDevServer = function _vitePluginReactServerDevServer
26
27
  name: "vite-plugin-react-server:server-hmr",
27
28
  apply: "serve" as const,
28
29
  // Server-level handleHotUpdate — sends custom WS event to client
30
+ // Vite 6 Environment API: hotUpdate runs per-environment.
31
+ // Prevent server/ssr environments from triggering page reload for client components.
32
+ hotUpdate(ctx: any) {
33
+ const { file, server } = ctx;
34
+ const envName = ctx.environment?.name ?? 'unknown';
35
+
36
+ const moduleBase = userOptions.moduleBase || "src";
37
+ const projectRoot = userOptions.projectRoot || server?.config?.root || '';
38
+ const normalizedFile = file.replace(projectRoot, '').replace(/^\/+/, '');
39
+ const isSourceFile = normalizedFile.startsWith(moduleBase + '/');
40
+
41
+ if (!isSourceFile) return;
42
+
43
+ // Client environment: let Vite/@vitejs/plugin-react handle it
44
+ if (envName === 'client') {
45
+ // Check if it's a client component — let Fast Refresh handle it
46
+ const isClient = (file.endsWith('.tsx') || file.endsWith('.ts') || file.endsWith('.jsx') || file.endsWith('.js')) && (() => {
47
+ try {
48
+ const head = readFileSync(file, 'utf-8').slice(0, 200);
49
+ return /^\s*["']use client["']/.test(head.split('\n')[0]);
50
+ } catch { return false; }
51
+ })();
52
+
53
+ if (isClient) return; // Let Fast Refresh handle client components
54
+
55
+ // Server component changed — send RSC refetch event to client
56
+ // Only do this once (from client env) to avoid duplicate events
57
+ server.config.logger.info(`[vite-plugin-react-server] File changed (RSC refetch): ${normalizedFile}`);
58
+ server.ws.send({
59
+ type: 'custom',
60
+ event: 'vite-plugin-react-server:server-component-update',
61
+ data: { file: normalizedFile, path: file },
62
+ });
63
+
64
+ return []; // Don't trigger client-side page reload
65
+ }
66
+
67
+ // Server/SSR environments: suppress page reload for all source files
68
+ // Server components are handled by the RSC refetch event sent above
69
+ // Invalidate the server module so next RSC request gets fresh content
70
+ if (envName === 'server') {
71
+ const mod = ctx.environment?.moduleGraph?.getModulesByFile(file);
72
+ if (mod) {
73
+ for (const m of mod) {
74
+ ctx.environment.moduleGraph.invalidateModule(m);
75
+ }
76
+ }
77
+ }
78
+ return [];
79
+ },
29
80
  handleHotUpdate({ file, server }: { file: string; server: ViteDevServer }) {
30
81
  const moduleBase = userOptions.moduleBase || "src";
31
82
  const projectRoot = userOptions.projectRoot || server.config.root;
32
83
  const normalizedFile = file.replace(projectRoot, '').replace(/^\/+/, '');
33
- const isServerFile = normalizedFile.startsWith(moduleBase + '/') &&
84
+ const isSourceFile = normalizedFile.startsWith(moduleBase + '/') &&
34
85
  (file.endsWith('.tsx') || file.endsWith('.ts') || file.endsWith('.jsx') || file.endsWith('.js'));
35
86
 
36
- if (isServerFile) {
37
- server.config.logger.info(`[vite-plugin-react-server] Server component changed: ${normalizedFile}`);
87
+ // Skip client components — let @vitejs/plugin-react handle them
88
+ // with Fast Refresh (preserves component-level state).
89
+ const isClientFile = isSourceFile && (() => {
90
+ try {
91
+ const head = readFileSync(file, 'utf-8').slice(0, 200);
92
+ return /^\s*["']use client["']/.test(head.split('\n')[0]);
93
+ } catch { return false; }
94
+ })();
95
+
96
+ if (isSourceFile && !isClientFile) {
97
+ server.config.logger.info(`[vite-plugin-react-server] File changed (RSC refetch): ${normalizedFile}`);
38
98
 
39
99
  // Send custom HMR event so client can refetch RSC stream
40
100
  server.ws.send({
@@ -58,13 +118,20 @@ export const vitePluginReactDevServer = function _vitePluginReactServerDevServer
58
118
  // The client will refetch the RSC stream via the custom event
59
119
  return [];
60
120
  }
121
+
122
+ if (isClientFile) {
123
+ // Client components are handled by @vitejs/plugin-react (Fast Refresh)
124
+ // or Vite's client-side HMR. Return empty to prevent the server
125
+ // environment from triggering a full page reload.
126
+ return [];
127
+ }
61
128
  },
62
129
  };
63
130
 
64
131
  const serverPlugin = {
65
132
  name: "vite-plugin-react-server:dev-server-server",
66
133
  apply: "serve" as const,
67
- applyToEnvironment(partialEnvironment: any) {
134
+ applyToEnvironment(partialEnvironment: any) {
68
135
  return partialEnvironment?.consumer === 'server';
69
136
  },
70
137
  configureServer(server: ViteDevServer) {
@@ -31,7 +31,8 @@ import type {
31
31
  AutoDiscoveredFiles,
32
32
  VitePluginFn,
33
33
  } from "../types.js";
34
- import { renderPages } from "./renderPages.js";
34
+ import { renderPagesBatched } from "./renderPagesBatched.js";
35
+ import { renderPages as renderPagesSequential } from "./renderPages.js";
35
36
  import { getBundleManifest } from "../helpers/getBundleManifest.js";
36
37
  import { createWorker } from "../worker/createWorker.js";
37
38
  import {
@@ -445,6 +446,14 @@ export const reactStaticPlugin: VitePluginFn = function _reactStaticPlugin(
445
446
  }
446
447
  }
447
448
 
449
+ // Select render mode based on build config
450
+ const renderMode = userOptions.build?.renderMode ?? "parallel";
451
+ const renderPages = renderMode === "sequential" ? renderPagesSequential : renderPagesBatched;
452
+
453
+ if (userOptions.verbose) {
454
+ logger.info(`[static] Using ${renderMode} rendering${renderMode === "parallel" ? ` (batch size: ${userOptions.build?.batchSize ?? 8})` : ""}`);
455
+ }
456
+
448
457
  // this will render the routes
449
458
  const renderPagesGenerator = renderPages(
450
459
  routes,
@@ -465,6 +474,7 @@ export const reactStaticPlugin: VitePluginFn = function _reactStaticPlugin(
465
474
  staticManifest: staticManifest, // Pass static manifest for path resolution
466
475
  autoDiscoveredFiles: autoDiscoveredFiles!,
467
476
  cssFilesByPage: cssFilesByPage,
477
+ batchSize: userOptions.build?.batchSize,
468
478
  },
469
479
  renderPage
470
480
  );
@@ -10,6 +10,8 @@ import type { RenderPagesFn, RenderPageFn, RenderPagesHandlerOptions } from "./t
10
10
  import { handleError } from "../error/handleError.js";
11
11
  import { fileWriter } from "./fileWriter.js";
12
12
  import type { Manifest } from "vite";
13
+ import { createRenderMetrics } from "../metrics/createRenderMetrics.js";
14
+ import { createStreamMetrics } from "../metrics/createStreamMetrics.js";
13
15
 
14
16
  const DEFAULT_BATCH_SIZE = 8;
15
17
 
@@ -23,13 +25,15 @@ function resolvePathWithManifest(path: string, manifest: Manifest): string {
23
25
 
24
26
  /**
25
27
  * Renders a single route completely, consuming all yields from renderPage
26
- * and writing the RSC and HTML files
28
+ * and writing the RSC and HTML files. Collects metrics and handles events
29
+ * identically to the sequential renderPages.
27
30
  */
28
31
  async function renderSingleRoute(
29
32
  route: string,
30
33
  handlerOptions: RenderPagesHandlerOptions,
31
34
  renderPage: RenderPageFn,
32
35
  manifest: Manifest,
36
+ failedRoutes: Map<string, unknown>,
33
37
  ): Promise<{ route: string; results: RenderPageResult[]; error?: Error }> {
34
38
  const { autoDiscoveredFiles, cssFilesByPage, ...options } = handlerOptions;
35
39
  const { page, props, root, html } = autoDiscoveredFiles.urlMap?.get(route) || {};
@@ -44,6 +48,131 @@ async function renderSingleRoute(
44
48
  const resolvedRootPath = root ? resolvePathWithManifest(root, manifest) : undefined;
45
49
  const resolvedHtmlPath = html ? resolvePathWithManifest(html, manifest) : undefined;
46
50
 
51
+ // Store results for metrics tracking
52
+ const routeResults = new Map<string, RenderPageResult>();
53
+
54
+ // Create onEvent wrapper that handles route.error and metrics collection
55
+ // This mirrors the sequential renderPages behavior exactly
56
+ const wrapperOnEvent = (event: any) => {
57
+ // Call the original onEvent first
58
+ if (options.onEvent) {
59
+ options.onEvent(event);
60
+ }
61
+
62
+ // Handle route.error events
63
+ if (event.type === "route.error") {
64
+ const detectedPanicError = handleError({
65
+ error: event.data.error,
66
+ logger: options.logger,
67
+ panicThreshold: event.data.panicThreshold,
68
+ context: `route.error (${event.data.route})`,
69
+ });
70
+
71
+ if (detectedPanicError != null) {
72
+ options.logger?.error(
73
+ `[renderPagesBatched] Panic error for route ${event.data.route}: ${event.data.error.message}`
74
+ );
75
+ failedRoutes.set(event.data.route, event.data.error);
76
+ } else {
77
+ options.logger?.warn(
78
+ `[renderPagesBatched] Non-panic error for route ${event.data.route}: ${event.data.error.message}`
79
+ );
80
+ }
81
+ }
82
+
83
+ // Handle metrics collection on file.write.done
84
+ if (event.type === "file.write.done" && event.data.route === route) {
85
+ const routeResult = routeResults.get(route);
86
+ if (routeResult && routeResult.type === "success") {
87
+ if (event.data.fileType === "html") {
88
+ const endTime = performance.now();
89
+ const htmlMetrics = createRenderMetrics({
90
+ route: route,
91
+ type: routeResult.metrics.html.type,
92
+ fromMainThread: routeResult.metrics.html.fromMainThread,
93
+ fromRscWorker: routeResult.metrics.html.fromRscWorker,
94
+ fromHtmlWorker: routeResult.metrics.html.fromHtmlWorker,
95
+ fileSize: event.data.content.length,
96
+ chunks: event.data.chunks || 0,
97
+ processingTime: endTime - routeResult.metrics.html.streamMetrics.startTime,
98
+ chunkRate: (event.data.chunks || 0) / ((endTime - routeResult.metrics.html.streamMetrics.startTime) / 1000),
99
+ fileName: event.data.fileName,
100
+ outputPath: event.data.path,
101
+ baseDir: event.data.baseDir,
102
+ routePath: event.data.routePath,
103
+ streamMetrics: createStreamMetrics({
104
+ ...routeResult.metrics.html.streamMetrics,
105
+ chunks: event.data.chunks || 0,
106
+ bytes: event.data.content.length,
107
+ duration: endTime - routeResult.metrics.html.streamMetrics.startTime,
108
+ endTime: endTime,
109
+ }),
110
+ });
111
+
112
+ if (options.onMetrics) {
113
+ options.onMetrics(htmlMetrics);
114
+ }
115
+
116
+ // Also emit RSC Full metrics if available
117
+ if (routeResult.metrics?.rscFull) {
118
+ const rscFullEndTime = performance.now();
119
+ const rscFullMetrics = createRenderMetrics({
120
+ route: route,
121
+ type: routeResult.metrics.rscFull.type,
122
+ fromMainThread: routeResult.metrics.rscFull.fromMainThread,
123
+ fromRscWorker: routeResult.metrics.rscFull.fromRscWorker,
124
+ fromHtmlWorker: routeResult.metrics.rscFull.fromHtmlWorker,
125
+ processingTime: rscFullEndTime - routeResult.metrics.rscFull.streamMetrics.startTime,
126
+ chunks: routeResult.metrics.rscFull.streamMetrics.chunks,
127
+ chunkRate: routeResult.metrics.rscFull.streamMetrics.chunks / ((rscFullEndTime - routeResult.metrics.rscFull.streamMetrics.startTime) / 1000),
128
+ fileName: event.data.fileName,
129
+ outputPath: event.data.path,
130
+ baseDir: event.data.baseDir,
131
+ routePath: event.data.routePath,
132
+ streamMetrics: createStreamMetrics({
133
+ ...routeResult.metrics.rscFull.streamMetrics,
134
+ duration: rscFullEndTime - routeResult.metrics.rscFull.streamMetrics.startTime,
135
+ endTime: rscFullEndTime,
136
+ }),
137
+ });
138
+
139
+ if (options.onMetrics) {
140
+ options.onMetrics(rscFullMetrics);
141
+ }
142
+ }
143
+ } else if (event.data.fileType === "rsc") {
144
+ const rscEndTime = performance.now();
145
+ const rscMetrics = createRenderMetrics({
146
+ route: route,
147
+ type: routeResult.metrics.rscHeadless.type,
148
+ fromMainThread: routeResult.metrics.rscHeadless.fromMainThread,
149
+ fromRscWorker: routeResult.metrics.rscHeadless.fromRscWorker,
150
+ fromHtmlWorker: routeResult.metrics.rscHeadless.fromHtmlWorker,
151
+ fileSize: event.data.content.length,
152
+ chunks: event.data.chunks || 0,
153
+ processingTime: rscEndTime - routeResult.metrics.rscHeadless.streamMetrics.startTime,
154
+ chunkRate: (event.data.chunks || 0) / ((rscEndTime - routeResult.metrics.rscHeadless.streamMetrics.startTime) / 1000),
155
+ fileName: event.data.fileName,
156
+ outputPath: event.data.path,
157
+ baseDir: event.data.baseDir,
158
+ routePath: event.data.routePath,
159
+ streamMetrics: createStreamMetrics({
160
+ ...routeResult.metrics.rscHeadless.streamMetrics,
161
+ chunks: event.data.chunks || 0,
162
+ bytes: event.data.content.length,
163
+ duration: rscEndTime - routeResult.metrics.rscHeadless.streamMetrics.startTime,
164
+ endTime: rscEndTime,
165
+ }),
166
+ });
167
+
168
+ if (options.onMetrics) {
169
+ options.onMetrics(rscMetrics);
170
+ }
171
+ }
172
+ }
173
+ }
174
+ };
175
+
47
176
  const routeHandlerOptions = {
48
177
  ...options,
49
178
  manifest,
@@ -55,6 +184,7 @@ async function renderSingleRoute(
55
184
  cssFiles: cssFilesByPage?.get(route) ?? new Map(),
56
185
  globalCss: options.globalCss ?? new Map(),
57
186
  id: `${route}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
187
+ onEvent: wrapperOnEvent,
58
188
  };
59
189
 
60
190
  const pageRenderer = renderPage(routeHandlerOptions);
@@ -65,24 +195,25 @@ async function renderSingleRoute(
65
195
  for await (const result of pageRenderer) {
66
196
  results.push(result);
67
197
 
68
- // Track error results
69
198
  if (result.type === "error" && result.error) {
70
199
  routeError = result.error instanceof Error ? result.error : new Error(String(result.error));
71
200
  }
72
201
 
73
- // Write files for success and skip results
74
- if ((result.type === "success" || result.type === "skip") && result.html && result.rsc) {
202
+ if (result.type === "success" || result.type === "skip") {
203
+ // Store result for metrics tracking (wrapperOnEvent needs this)
204
+ routeResults.set(route, result);
205
+
75
206
  const rscWritePromise = fileWriter(
76
207
  result.rsc as any,
77
208
  "rsc",
78
- { ...options, route, logger: options.logger },
209
+ { ...options, route, onEvent: wrapperOnEvent, logger: options.logger },
79
210
  options.signal
80
211
  );
81
212
 
82
213
  const htmlWritePromise = fileWriter(
83
214
  result.html as any,
84
215
  "html",
85
- { ...options, route, logger: options.logger },
216
+ { ...options, route, onEvent: wrapperOnEvent, logger: options.logger },
86
217
  options.signal
87
218
  );
88
219
 
@@ -90,7 +221,6 @@ async function renderSingleRoute(
90
221
  }
91
222
  }
92
223
 
93
- // Return error if any result was an error
94
224
  if (routeError) {
95
225
  return { route, results, error: routeError };
96
226
  }
@@ -173,7 +303,7 @@ export const renderPagesBatched: RenderPagesFn = (
173
303
 
174
304
  // Render all pages in this batch concurrently
175
305
  const batchPromises = batch.map(route =>
176
- renderSingleRoute(route, handlerOptions, renderPage, manifest)
306
+ renderSingleRoute(route, handlerOptions, renderPage, manifest, failedRoutes)
177
307
  );
178
308
 
179
309
  const batchResults = await Promise.all(batchPromises);
@@ -191,13 +321,22 @@ export const renderPagesBatched: RenderPagesFn = (
191
321
  if (panicError != null) {
192
322
  failedRoutes.set(route, error);
193
323
  options.logger?.error(`[renderPagesBatched] Panic error for route ${route}: ${error.message}`);
324
+ const errorResult: RenderPagesResult = {
325
+ type: "error",
326
+ error,
327
+ route,
328
+ failedRoutes,
329
+ completedRoutes,
330
+ results,
331
+ };
332
+ yield errorResult;
333
+ return errorResult;
194
334
  } else {
195
335
  options.logger?.warn(`[renderPagesBatched] Non-panic error for route ${route}: ${error.message}`);
196
336
  }
197
337
  } else {
198
338
  completedRoutes.add(route);
199
339
 
200
- // Yield each result from this page
201
340
  for (const result of pageResults) {
202
341
  if (result.type === "success" || result.type === "skip") {
203
342
  results.set(route, result);
@@ -220,36 +359,6 @@ export const renderPagesBatched: RenderPagesFn = (
220
359
  }
221
360
  }
222
361
 
223
- // Check if we should panic based on failed routes
224
- if (failedRoutes.size > 0) {
225
- const firstError = Array.from(failedRoutes.values())[0];
226
- const panicError = handleError({
227
- error: firstError,
228
- logger: options.logger,
229
- panicThreshold: options.panicThreshold,
230
- context: `renderPagesBatched final check`,
231
- });
232
-
233
- if (panicError != null) {
234
- if (options.verbose) {
235
- options.logger?.error(
236
- `[renderPagesBatched] Build failed due to panic threshold: ${failedRoutes.size} routes failed`
237
- );
238
- }
239
- // Yield error before returning
240
- const errorResult: RenderPagesResult = {
241
- type: "error",
242
- error: panicError,
243
- route: "",
244
- failedRoutes,
245
- completedRoutes,
246
- results,
247
- };
248
- yield errorResult;
249
- return errorResult;
250
- }
251
- }
252
-
253
362
  // Final success result
254
363
  const finalResult: RenderPagesResult = {
255
364
  type: "success",
@@ -133,6 +133,7 @@ export type RenderPagesHandlerOptions = Omit<
133
133
  cssFilesByPage: Map<string, Map<string, CssContent>>;
134
134
  serverPipeableStreamOptions: any;
135
135
  staticManifest?: Manifest; // Static manifest for consistent module IDs
136
+ batchSize?: number; // Concurrency for parallel rendering
136
137
  };
137
138
 
138
139
  export type RenderPagesFn = (
package/plugin/types.ts CHANGED
@@ -1102,6 +1102,21 @@ export type BuildConfig = {
1102
1102
  rscExtension?: string;
1103
1103
  cssModuleExtension?: string;
1104
1104
  nodeExtension?: string;
1105
+ /**
1106
+ * Controls how pages are rendered during static generation.
1107
+ *
1108
+ * - `"parallel"` (default): Renders pages in concurrent batches for faster builds.
1109
+ * Use `batchSize` to control concurrency (default: 8).
1110
+ * - `"sequential"`: Renders pages one at a time. Slower but uses less memory
1111
+ * and produces deterministic output order.
1112
+ */
1113
+ renderMode?: "parallel" | "sequential";
1114
+ /**
1115
+ * Number of pages to render concurrently when `renderMode` is `"parallel"`.
1116
+ * Higher values use more memory but build faster.
1117
+ * @default 8
1118
+ */
1119
+ batchSize?: number;
1105
1120
  };
1106
1121
 
1107
1122
  export type DevConfig = {
@@ -1,7 +1,6 @@
1
- // Server barrel: excludes browser-only modules (createReactFetcher, useRscHmr,
2
- // createCallServer) that import from react-server-dom-esm/client.browser or
3
- // use React hooks which fail under react-server condition (React CJS exports)
4
- export * from "./callServer.js";
1
+ // Server barrel: excludes browser-only modules that import from
2
+ // react-server-dom-esm/client.browser or use React hooks:
3
+ // - createReactFetcher, useRscHmr, createCallServer, callServer
5
4
  export * from "./urls.js";
6
5
  export * from "./env.js";
7
6
  export * from "./routeToURL.js";
@@ -35,7 +35,7 @@ register("data:text/javascript," + encodeURIComponent(`
35
35
  "react-server-dom-esm/client.node": join(ossDir, "client.node.js"),
36
36
  "react-server-dom-esm/server": join(ossDir, "esm", "react-server-dom-esm-server.node.js"),
37
37
  "react-server-dom-esm/server.node": join(ossDir, "esm", "react-server-dom-esm-server.node.js"),
38
- "react-server-dom-esm/static": join(ossDir, "static.js"),
38
+ "react-server-dom-esm/static": join(ossDir, "static.node.js"),
39
39
  "react-server-dom-esm/static.node": join(ossDir, "static.node.js"),
40
40
  };
41
41