Skip to content

Commit 0fa1a07

Browse files
committed
feat: improve the support for ssg and unify the calculation rules of SSR mode
1 parent c8f93d5 commit 0fa1a07

File tree

9 files changed

+108
-45
lines changed

9 files changed

+108
-45
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: 2 additions & 20 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,32 +16,15 @@ 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

2323
function getSSRMode(
2424
entry: string,
2525
config: AppToolsNormalizedConfig,
2626
): '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-
}
27+
return resolveSSRMode({ entry, config });
4628
}
4729

4830
export const generateCode = async (

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
import type { CLIPluginAPI } from '@modern-js/plugin';
99
import { LOADABLE_STATS_FILE, isUseSSRBundle } from '@modern-js/utils';
1010
import type { RsbuildPlugin } from '@rsbuild/core';
11+
import { resolveSSRMode } from './mode';
1112

1213
const hasStringSSREntry = (userConfig: AppToolsNormalizedConfig): boolean => {
1314
const isStreaming = (ssr: ServerUserConfig['ssr']) =>
@@ -42,16 +43,12 @@ const hasStringSSREntry = (userConfig: AppToolsNormalizedConfig): boolean => {
4243
return false;
4344
};
4445

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;
53-
}
54-
return hasStringSSREntry(config);
46+
const checkUseStringSSR = (
47+
config: AppToolsNormalizedConfig,
48+
appDirectory?: string,
49+
): boolean => {
50+
const ssrMode = resolveSSRMode({ config, appDirectory });
51+
return ssrMode === 'string';
5552
};
5653

5754
const ssrBuilderPlugin = (
@@ -72,10 +69,13 @@ const ssrBuilderPlugin = (
7269
? 'edge'
7370
: 'node';
7471

72+
const appContext = modernAPI.getAppContext();
73+
const { appDirectory } = appContext;
74+
7575
const useLoadablePlugin =
7676
isUseSSRBundle(userConfig) &&
7777
!isServerEnvironment &&
78-
checkUseStringSSR(userConfig);
78+
checkUseStringSSR(userConfig, appDirectory);
7979

8080
return mergeEnvironmentConfig(config, {
8181
source: {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
}): SSRMode {
21+
const { entry, config, appDirectory } = params;
22+
23+
// 1. Check if SSG is enabled first (SSG takes precedence over SSR when both are configured)
24+
const isSsgEnabled =
25+
config.output?.ssg ||
26+
(config.output?.ssgByEntries &&
27+
(entry
28+
? !!config.output.ssgByEntries[entry]
29+
: Object.keys(config.output.ssgByEntries).length > 0));
30+
31+
if (isSsgEnabled) {
32+
// If user explicitly disables conventional routing (non-conventional routing), force 'string'
33+
const entryRouterConfig = entry
34+
? config.runtimeByEntries?.[entry]?.router
35+
: undefined;
36+
const routerConfig =
37+
entryRouterConfig !== undefined
38+
? entryRouterConfig
39+
: config.runtime?.router;
40+
41+
if (!routerConfig) {
42+
return 'string';
43+
}
44+
45+
if (appDirectory) {
46+
return isReact18(appDirectory) ? 'stream' : 'string';
47+
}
48+
return 'stream';
49+
}
50+
51+
// 2. Check user's explicit SSR config (server.ssr or server.ssrByEntries)
52+
const ssr = entry
53+
? config.server?.ssrByEntries?.[entry] || config.server?.ssr
54+
: config.server?.ssr;
55+
56+
if (ssr !== undefined) {
57+
if (!ssr) {
58+
return false;
59+
}
60+
if (typeof ssr === 'boolean') {
61+
return ssr ? 'string' : false;
62+
}
63+
return ssr.mode === 'stream' ? 'stream' : 'string';
64+
}
65+
66+
// 3. No SSR
67+
return false;
68+
}

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 RuntimeContext,
@@ -143,7 +143,13 @@ function createSSRContext(
143143
config.ssr,
144144
config.ssrByEntries,
145145
);
146-
const ssrMode = getSSRMode(ssrConfig);
146+
let ssrMode = getSSRMode(ssrConfig);
147+
148+
const isSsgRender = headers.get('x-modern-ssg-render') === 'true';
149+
if (isSsgRender) {
150+
const reactMajor = Number((React.version || '0').split('.')[0]);
151+
ssrMode = reactMajor >= 18 ? 'stream' : 'string';
152+
}
147153

148154
const loaderFailureMode =
149155
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: 9 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';
@@ -173,14 +174,14 @@ export const generateCode = async (
173174
config.server.ssrByEntries,
174175
packageName,
175176
);
176-
const useSSG = isSSGEntry(config, entryName, entrypoints);
177177

178-
let mode: SSRMode | undefined;
179-
if (ssr) {
180-
mode = typeof ssr === 'object' ? ssr.mode || 'string' : 'string';
181-
}
178+
const ssrMode = resolveSSRMode({
179+
entry: entrypoint.entryName,
180+
config,
181+
appDirectory: appContext.appDirectory,
182+
});
182183

183-
if (mode === 'stream') {
184+
if (ssrMode === 'stream') {
184185
const hasPageRoute = routes.some(
185186
route => 'type' in route && route.type === 'page',
186187
);
@@ -197,7 +198,7 @@ export const generateCode = async (
197198
code: await templates.fileSystemRoutes({
198199
metaName,
199200
routes: routes,
200-
ssrMode: useSSG ? 'string' : isUseRsc(config) ? 'stream' : mode,
201+
ssrMode: isUseRsc(config) ? 'stream' : ssrMode,
201202
nestedRoutesEntry: entrypoint.nestedRoutesEntry,
202203
entryName: entrypoint.entryName,
203204
internalDirectory,
@@ -233,7 +234,7 @@ export const generateCode = async (
233234
const serverRoutesCode = await templates.fileSystemRoutes({
234235
metaName,
235236
routes: filtedRoutesForServer,
236-
ssrMode: useSSG ? 'string' : mode,
237+
ssrMode,
237238
nestedRoutesEntry: entrypoint.nestedRoutesEntry,
238239
entryName: entrypoint.entryName,
239240
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)