Skip to content

Commit 1041e46

Browse files
authored
Merge branch 'main' into renovate/main-rsbuild
2 parents c0d4d78 + 00e5d60 commit 1041e46

File tree

9 files changed

+134
-53
lines changed

9 files changed

+134
-53
lines changed

packages/cli/plugin-ssg/src/server/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export const createServer = async (
120120
method: 'GET',
121121
headers: {
122122
host: 'localhost',
123+
'x-modern-ssg-render': 'true',
123124
},
124125
});
125126

packages/runtime/plugin-runtime/src/cli/code.ts

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import path from 'path';
22
import type {
3-
AppNormalizedConfig,
43
AppToolsContext,
54
AppToolsFeatureHooks,
65
AppToolsNormalizedConfig,
@@ -17,34 +16,10 @@ import {
1716
INDEX_FILE_NAME,
1817
SERVER_ENTRY_POINT_FILE_NAME,
1918
} from './constants';
19+
import { resolveSSRMode } from './ssr/mode';
2020
import * as template from './template';
2121
import * as serverTemplate from './template.server';
2222

23-
function getSSRMode(
24-
entry: string,
25-
config: AppToolsNormalizedConfig,
26-
): 'string' | 'stream' | false {
27-
const { ssr, ssrByEntries } = config.server;
28-
29-
if (config.output.ssg || config.output.ssgByEntries) {
30-
return 'string';
31-
}
32-
33-
return checkSSRMode(ssrByEntries?.[entry] || ssr);
34-
35-
function checkSSRMode(ssr: AppNormalizedConfig['server']['ssr']) {
36-
if (!ssr) {
37-
return false;
38-
}
39-
40-
if (typeof ssr === 'boolean') {
41-
return ssr ? 'string' : false;
42-
}
43-
44-
return ssr.mode === 'stream' ? 'stream' : 'string';
45-
}
46-
}
47-
4823
export const generateCode = async (
4924
entrypoints: Entrypoint[],
5025
appContext: AppToolsContext,
@@ -63,15 +38,25 @@ export const generateCode = async (
6338
} = appContext;
6439
await Promise.all(
6540
entrypoints.map(async entrypoint => {
66-
const { entryName, isAutoMount, entry, customEntry, customServerEntry } =
67-
entrypoint;
41+
const {
42+
entryName,
43+
isAutoMount,
44+
entry,
45+
customEntry,
46+
customServerEntry,
47+
nestedRoutesEntry,
48+
} = entrypoint;
6849
const { plugins: runtimePlugins } =
6950
await hooks._internalRuntimePlugins.call({
7051
entrypoint,
7152
plugins: [],
7253
});
7354
if (isAutoMount) {
74-
const ssrMode = getSSRMode(entryName, config);
55+
const ssrMode = resolveSSRMode({
56+
entry: entryName,
57+
config,
58+
nestedRoutesEntry,
59+
});
7560
let indexCode = '';
7661
// index.jsx
7762
if (!ssrMode && config.server.rsc) {

packages/runtime/plugin-runtime/src/cli/ssr/index.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import type {
66
ServerUserConfig,
77
} from '@modern-js/app-tools';
88
import type { CLIPluginAPI } from '@modern-js/plugin';
9+
import type { Entrypoint } from '@modern-js/types';
910
import { LOADABLE_STATS_FILE, isUseSSRBundle } from '@modern-js/utils';
1011
import type { RsbuildPlugin } from '@rsbuild/core';
12+
import { resolveSSRMode } from './mode';
1113

1214
const hasStringSSREntry = (userConfig: AppToolsNormalizedConfig): boolean => {
1315
const isStreaming = (ssr: ServerUserConfig['ssr']) =>
@@ -42,16 +44,32 @@ const hasStringSSREntry = (userConfig: AppToolsNormalizedConfig): boolean => {
4244
return false;
4345
};
4446

45-
const checkUseStringSSR = (config: AppToolsNormalizedConfig): boolean => {
46-
const { output } = config;
47-
48-
if (output?.ssg) {
49-
return true;
50-
}
51-
if (output?.ssgByEntries && Object.keys(output.ssgByEntries).length > 0) {
52-
return true;
47+
/**
48+
* Check if any entry uses string SSR mode.
49+
* Returns true if at least one entry uses 'string' SSR mode.
50+
*/
51+
const checkUseStringSSR = (
52+
config: AppToolsNormalizedConfig,
53+
appDirectory?: string,
54+
entrypoints?: Entrypoint[],
55+
): boolean => {
56+
// If entrypoints are provided, check each entry
57+
if (entrypoints && entrypoints.length > 0) {
58+
for (const entrypoint of entrypoints) {
59+
const ssrMode = resolveSSRMode({
60+
entry: entrypoint.entryName,
61+
config,
62+
appDirectory,
63+
nestedRoutesEntry: entrypoint.nestedRoutesEntry,
64+
});
65+
if (ssrMode === 'string') {
66+
return true;
67+
}
68+
}
69+
return false;
5370
}
54-
return hasStringSSREntry(config);
71+
72+
return true;
5573
};
5674

5775
const ssrBuilderPlugin = (
@@ -72,10 +90,13 @@ const ssrBuilderPlugin = (
7290
? 'edge'
7391
: 'node';
7492

93+
const appContext = modernAPI.getAppContext();
94+
const { appDirectory, entrypoints } = appContext;
95+
7596
const useLoadablePlugin =
7697
isUseSSRBundle(userConfig) &&
7798
!isServerEnvironment &&
78-
checkUseStringSSR(userConfig);
99+
checkUseStringSSR(userConfig, appDirectory, entrypoints);
79100

80101
return mergeEnvironmentConfig(config, {
81102
source: {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type {
2+
AppNormalizedConfig,
3+
AppToolsNormalizedConfig,
4+
} from '@modern-js/app-tools';
5+
import { isReact18, isUseRsc } from '@modern-js/utils';
6+
7+
export type SSRMode = 'string' | 'stream' | false;
8+
9+
/**
10+
* Unified SSR mode resolution function.
11+
* Priority:
12+
* 1. If SSG is enabled, use SSG configuration (SSG takes precedence over SSR when both are configured)
13+
* 2. User's explicit server.ssr/server.ssrByEntries config
14+
* 3. Otherwise return false (no SSR)
15+
*/
16+
export function resolveSSRMode(params: {
17+
entry?: string;
18+
config: AppToolsNormalizedConfig;
19+
appDirectory?: string;
20+
nestedRoutesEntry?: string;
21+
}): SSRMode {
22+
const { entry, config, appDirectory, nestedRoutesEntry } = params;
23+
24+
// 1. Check if SSG is enabled first (SSG takes precedence over SSR when both are configured)
25+
const isSsgEnabled =
26+
config.output?.ssg ||
27+
(config.output?.ssgByEntries &&
28+
(entry
29+
? !!config.output.ssgByEntries[entry]
30+
: Object.keys(config.output.ssgByEntries).length > 0));
31+
32+
if (isSsgEnabled) {
33+
if (nestedRoutesEntry) {
34+
return 'stream';
35+
} else {
36+
return 'string';
37+
}
38+
if (appDirectory) {
39+
return isReact18(appDirectory) ? 'stream' : 'string';
40+
}
41+
return 'stream';
42+
}
43+
44+
// 2. Check user's explicit SSR config (server.ssr or server.ssrByEntries)
45+
const ssr = entry
46+
? config.server?.ssrByEntries?.[entry] || config.server?.ssr
47+
: config.server?.ssr;
48+
49+
if (ssr !== undefined) {
50+
if (!ssr) {
51+
return false;
52+
}
53+
if (typeof ssr === 'boolean') {
54+
return ssr ? 'string' : false;
55+
}
56+
return ssr.mode === 'stream' ? 'stream' : 'string';
57+
}
58+
59+
// 3. No SSR
60+
return false;
61+
}

packages/runtime/plugin-runtime/src/core/server/requestHandler.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
parseHeaders,
1111
parseQuery,
1212
} from '@modern-js/runtime-utils/universal/request';
13-
import type React from 'react';
13+
import React from 'react';
1414
import { Fragment } from 'react';
1515
import {
1616
type TInternalRuntimeContext,
@@ -141,7 +141,13 @@ function createSSRContext(
141141
config.ssr,
142142
config.ssrByEntries,
143143
);
144-
const ssrMode = getSSRMode(ssrConfig);
144+
let ssrMode = getSSRMode(ssrConfig);
145+
146+
const isSsgRender = headers.get('x-modern-ssg-render') === 'true';
147+
if (isSsgRender) {
148+
const reactMajor = Number((React.version || '0').split('.')[0]);
149+
ssrMode = reactMajor >= 18 ? 'stream' : 'string';
150+
}
145151

146152
const loaderFailureMode =
147153
typeof ssrConfig === 'object' ? ssrConfig.loaderFailureMode : undefined;

packages/runtime/plugin-runtime/src/core/server/stream/createReadableStream.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ export const createReadableStreamFromElement: CreateReadableStreamFromElement =
3636
// When a crawler visit the page, we should waiting for entrie content of page
3737

3838
const isbot = checkIsBot(request.headers.get('user-agent'));
39-
const onReady = isbot || forceStream2String ? 'onAllReady' : 'onShellReady';
39+
const isSsgRender = request.headers.get('x-modern-ssg-render') === 'true';
40+
const onReady =
41+
isbot || isSsgRender || forceStream2String
42+
? 'onAllReady'
43+
: 'onShellReady';
4044

4145
const internalRuntimeContext = getGlobalInternalRuntimeContext();
4246
const hooks = internalRuntimeContext.hooks;

packages/runtime/plugin-runtime/src/core/server/stream/createReadableStream.worker.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ export const createReadableStreamFromElement: CreateReadableStreamFromElement =
5656
});
5757

5858
const isbot = checkIsBot(request.headers.get('user-agent'));
59-
if (isbot) {
60-
// However, when a crawler visits your page, or if you’re generating the pages at the build time,
59+
const isSsgRender = request.headers.get('x-modern-ssg-render') === 'true';
60+
if (isbot || isSsgRender) {
61+
// However, when a crawler visits your page, or if you're generating the pages at the build time,
6162
// you might want to let all of the content load first and then produce the final HTML output instead of revealing it progressively.
6263
// from: https://react.dev/reference/react-dom/server/renderToReadableStream#handling-different-errors-in-different-ways
6364
await readableOriginal.allReady;

packages/runtime/plugin-runtime/src/router/cli/code/index.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
} from '@modern-js/utils';
2929
import { cloneDeep } from '@modern-js/utils/lodash';
3030
import { ENTRY_POINT_RUNTIME_GLOBAL_CONTEXT_FILE_NAME } from '../../../cli/constants';
31+
import { resolveSSRMode } from '../../../cli/ssr/mode';
3132
import { FILE_SYSTEM_ROUTES_FILE_NAME } from '../constants';
3233
import { walk } from './nestedRoutes';
3334
import * as templates from './templates';
@@ -165,14 +166,15 @@ export const generateCode = async (
165166
config.server.ssrByEntries,
166167
packageName,
167168
);
168-
const useSSG = isSSGEntry(config, entryName, entrypoints);
169169

170-
let mode: SSRMode | undefined;
171-
if (ssr) {
172-
mode = typeof ssr === 'object' ? ssr.mode || 'string' : 'string';
173-
}
170+
const ssrMode = resolveSSRMode({
171+
entry: entrypoint.entryName,
172+
config,
173+
appDirectory: appContext.appDirectory,
174+
nestedRoutesEntry: entrypoint.nestedRoutesEntry,
175+
});
174176

175-
if (mode === 'stream') {
177+
if (ssrMode === 'stream') {
176178
const hasPageRoute = routes.some(
177179
route => 'type' in route && route.type === 'page',
178180
);
@@ -189,7 +191,7 @@ export const generateCode = async (
189191
code: await templates.fileSystemRoutes({
190192
metaName,
191193
routes: routes,
192-
ssrMode: useSSG ? 'string' : isUseRsc(config) ? 'stream' : mode,
194+
ssrMode: isUseRsc(config) ? 'stream' : ssrMode,
193195
nestedRoutesEntry: entrypoint.nestedRoutesEntry,
194196
entryName: entrypoint.entryName,
195197
internalDirectory,
@@ -225,7 +227,7 @@ export const generateCode = async (
225227
const serverRoutesCode = await templates.fileSystemRoutes({
226228
metaName,
227229
routes: filtedRoutesForServer,
228-
ssrMode: useSSG ? 'string' : mode,
230+
ssrMode,
229231
nestedRoutesEntry: entrypoint.nestedRoutesEntry,
230232
entryName: entrypoint.entryName,
231233
internalDirectory,

packages/toolkit/types/common/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ export type ServerPlugin = {
1212
options?: Record<string, any>;
1313
};
1414

15-
export type SSRMode = 'string' | 'stream';
15+
export type SSRMode = 'string' | 'stream' | false;

0 commit comments

Comments
 (0)