vite-plugin-react-server 1.3.6 → 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.
- package/README.md +32 -18
- package/dist/package.json +4 -2
- package/dist/plugin/config/resolveOptions.d.ts.map +1 -1
- package/dist/plugin/config/resolveOptions.js +4 -2
- package/dist/plugin/dev-server/plugin.client.d.ts.map +1 -1
- package/dist/plugin/dev-server/plugin.client.js +13 -3
- package/dist/plugin/dev-server/plugin.server.d.ts.map +1 -1
- package/dist/plugin/dev-server/plugin.server.js +54 -4
- package/dist/plugin/react-static/plugin.server.d.ts.map +1 -1
- package/dist/plugin/react-static/plugin.server.js +9 -1
- package/dist/plugin/react-static/renderPagesBatched.d.ts.map +1 -1
- package/dist/plugin/react-static/renderPagesBatched.js +136 -36
- package/dist/plugin/react-static/types.d.ts +1 -0
- package/dist/plugin/react-static/types.d.ts.map +1 -1
- package/dist/plugin/types.d.ts +15 -0
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/utils/createReactFetcher.js +24 -2
- package/dist/plugin/utils/useRscHmr.js +10 -2
- package/dist/plugin/vendor/register-vendor.js +1 -1
- package/dist/plugin/vendor/vendor-alias.d.ts +5 -2
- package/dist/plugin/vendor/vendor-alias.d.ts.map +1 -1
- package/dist/plugin/vendor/vendor-alias.js +60 -26
- package/dist/plugin/vendor/vendor.server.d.ts.map +1 -1
- package/dist/plugin/vendor/vendor.server.js +2 -2
- package/dist/plugin/vendor/vendor.static.d.ts.map +1 -1
- package/dist/plugin/vendor/vendor.static.js +2 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.development.js +1 -1
- package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.production.js +1 -1
- package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.node.development.js +1 -1
- package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.node.production.js +1 -1
- package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +1 -1
- package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.production.js +1 -1
- package/oss-experimental/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js +1 -1
- package/oss-experimental/react-server-dom-esm/esm/react-server-dom-esm-client.browser.production.js +1 -1
- package/oss-experimental/react-server-dom-esm/package.json +3 -3
- package/package.json +4 -2
- package/plugin/config/resolveOptions.ts +2 -0
- package/plugin/dev-server/plugin.client.ts +13 -2
- package/plugin/dev-server/plugin.server.ts +71 -4
- package/plugin/react-static/plugin.server.ts +11 -1
- package/plugin/react-static/renderPagesBatched.ts +148 -39
- package/plugin/react-static/types.ts +1 -0
- package/plugin/types.ts +15 -0
- package/plugin/vendor/register-vendor.ts +1 -1
- package/plugin/vendor/vendor-alias.ts +61 -39
- package/plugin/vendor/vendor.server.ts +3 -3
- package/plugin/vendor/vendor.static.ts +3 -2
package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.development.js
CHANGED
|
@@ -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";
|
package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.production.js
CHANGED
|
@@ -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";
|
package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.node.development.js
CHANGED
|
@@ -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";
|
package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.node.production.js
CHANGED
|
@@ -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";
|
package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js
CHANGED
|
@@ -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";
|
package/oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.production.js
CHANGED
|
@@ -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";
|
package/oss-experimental/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js
CHANGED
|
@@ -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';
|
package/oss-experimental/react-server-dom-esm/esm/react-server-dom-esm-client.browser.production.js
CHANGED
|
@@ -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
|
+
"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":
|
|
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
|
|
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
|
|
84
|
+
const isSourceFile = normalizedFile.startsWith(moduleBase + '/') &&
|
|
34
85
|
(file.endsWith('.tsx') || file.endsWith('.ts') || file.endsWith('.jsx') || file.endsWith('.js'));
|
|
35
86
|
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
74
|
-
|
|
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 = {
|
|
@@ -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
|
|