Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cli/plugin-ssg/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export const createServer = async (
method: 'GET',
headers: {
host: 'localhost',
'x-modern-ssg-render': 'true',
},
});

Expand Down
22 changes: 2 additions & 20 deletions packages/runtime/plugin-runtime/src/cli/code.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import path from 'path';
import type {
AppNormalizedConfig,
AppToolsContext,
AppToolsFeatureHooks,
AppToolsNormalizedConfig,
Expand All @@ -17,32 +16,15 @@ import {
INDEX_FILE_NAME,
SERVER_ENTRY_POINT_FILE_NAME,
} from './constants';
import { resolveSSRMode } from './ssr/mode';
import * as template from './template';
import * as serverTemplate from './template.server';

function getSSRMode(
entry: string,
config: AppToolsNormalizedConfig,
): 'string' | 'stream' | false {
const { ssr, ssrByEntries } = config.server;

if (config.output.ssg || config.output.ssgByEntries) {
return 'string';
}

return checkSSRMode(ssrByEntries?.[entry] || ssr);

function checkSSRMode(ssr: AppNormalizedConfig['server']['ssr']) {
if (!ssr) {
return false;
}

if (typeof ssr === 'boolean') {
return ssr ? 'string' : false;
}

return ssr.mode === 'stream' ? 'stream' : 'string';
}
return resolveSSRMode({ entry, config });
}

export const generateCode = async (
Expand Down
22 changes: 11 additions & 11 deletions packages/runtime/plugin-runtime/src/cli/ssr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
import type { CLIPluginAPI } from '@modern-js/plugin';
import { LOADABLE_STATS_FILE, isUseSSRBundle } from '@modern-js/utils';
import type { RsbuildPlugin } from '@rsbuild/core';
import { resolveSSRMode } from './mode';

const hasStringSSREntry = (userConfig: AppToolsNormalizedConfig): boolean => {
const isStreaming = (ssr: ServerUserConfig['ssr']) =>
Expand Down Expand Up @@ -42,16 +43,12 @@ const hasStringSSREntry = (userConfig: AppToolsNormalizedConfig): boolean => {
return false;
};

const checkUseStringSSR = (config: AppToolsNormalizedConfig): boolean => {
const { output } = config;

if (output?.ssg) {
return true;
}
if (output?.ssgByEntries && Object.keys(output.ssgByEntries).length > 0) {
return true;
}
return hasStringSSREntry(config);
const checkUseStringSSR = (
config: AppToolsNormalizedConfig,
appDirectory?: string,
): boolean => {
const ssrMode = resolveSSRMode({ config, appDirectory });
return ssrMode === 'string';
};

const ssrBuilderPlugin = (
Expand All @@ -72,10 +69,13 @@ const ssrBuilderPlugin = (
? 'edge'
: 'node';

const appContext = modernAPI.getAppContext();
const { appDirectory } = appContext;

const useLoadablePlugin =
isUseSSRBundle(userConfig) &&
!isServerEnvironment &&
checkUseStringSSR(userConfig);
checkUseStringSSR(userConfig, appDirectory);

return mergeEnvironmentConfig(config, {
source: {
Expand Down
68 changes: 68 additions & 0 deletions packages/runtime/plugin-runtime/src/cli/ssr/mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type {
AppNormalizedConfig,
AppToolsNormalizedConfig,
} from '@modern-js/app-tools';
import { isReact18, isUseRsc } from '@modern-js/utils';

export type SSRMode = 'string' | 'stream' | false;

/**
* Unified SSR mode resolution function.
* Priority:
* 1. If SSG is enabled, use SSG configuration (SSG takes precedence over SSR when both are configured)
* 2. User's explicit server.ssr/server.ssrByEntries config
* 3. Otherwise return false (no SSR)
*/
export function resolveSSRMode(params: {
entry?: string;
config: AppToolsNormalizedConfig;
appDirectory?: string;
}): SSRMode {
const { entry, config, appDirectory } = params;

// 1. Check if SSG is enabled first (SSG takes precedence over SSR when both are configured)
const isSsgEnabled =
config.output?.ssg ||
(config.output?.ssgByEntries &&
(entry
? !!config.output.ssgByEntries[entry]
: Object.keys(config.output.ssgByEntries).length > 0));

if (isSsgEnabled) {
// If user explicitly disables conventional routing (non-conventional routing), force 'string'
const entryRouterConfig = entry
? config.runtimeByEntries?.[entry]?.router
: undefined;
const routerConfig =
entryRouterConfig !== undefined
? entryRouterConfig
: config.runtime?.router;

if (!routerConfig) {
return 'string';
}

if (appDirectory) {
return isReact18(appDirectory) ? 'stream' : 'string';
}
return 'stream';
}

// 2. Check user's explicit SSR config (server.ssr or server.ssrByEntries)
const ssr = entry
? config.server?.ssrByEntries?.[entry] || config.server?.ssr
: config.server?.ssr;

if (ssr !== undefined) {
if (!ssr) {
return false;
}
if (typeof ssr === 'boolean') {
return ssr ? 'string' : false;
}
return ssr.mode === 'stream' ? 'stream' : 'string';
}

// 3. No SSR
return false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
parseHeaders,
parseQuery,
} from '@modern-js/runtime-utils/universal/request';
import type React from 'react';
import React from 'react';
import { Fragment } from 'react';
import {
type RuntimeContext,
Expand Down Expand Up @@ -143,7 +143,13 @@ function createSSRContext(
config.ssr,
config.ssrByEntries,
);
const ssrMode = getSSRMode(ssrConfig);
let ssrMode = getSSRMode(ssrConfig);

const isSsgRender = headers.get('x-modern-ssg-render') === 'true';
if (isSsgRender) {
const reactMajor = Number((React.version || '0').split('.')[0]);
ssrMode = reactMajor >= 18 ? 'stream' : 'string';
}

const loaderFailureMode =
typeof ssrConfig === 'object' ? ssrConfig.loaderFailureMode : undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ export const createReadableStreamFromElement: CreateReadableStreamFromElement =
// When a crawler visit the page, we should waiting for entrie content of page

const isbot = checkIsBot(request.headers.get('user-agent'));
const onReady = isbot || forceStream2String ? 'onAllReady' : 'onShellReady';
const isSsgRender = request.headers.get('x-modern-ssg-render') === 'true';
const onReady =
isbot || isSsgRender || forceStream2String
? 'onAllReady'
: 'onShellReady';

const internalRuntimeContext = getGlobalInternalRuntimeContext();
const hooks = internalRuntimeContext.hooks;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ export const createReadableStreamFromElement: CreateReadableStreamFromElement =
});

const isbot = checkIsBot(request.headers.get('user-agent'));
if (isbot) {
// However, when a crawler visits your page, or if you’re generating the pages at the build time,
const isSsgRender = request.headers.get('x-modern-ssg-render') === 'true';
if (isbot || isSsgRender) {
// However, when a crawler visits your page, or if you're generating the pages at the build time,
// you might want to let all of the content load first and then produce the final HTML output instead of revealing it progressively.
// from: https://react.dev/reference/react-dom/server/renderToReadableStream#handling-different-errors-in-different-ways
await readableOriginal.allReady;
Expand Down
17 changes: 9 additions & 8 deletions packages/runtime/plugin-runtime/src/router/cli/code/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from '@modern-js/utils';
import { cloneDeep } from '@modern-js/utils/lodash';
import { ENTRY_POINT_RUNTIME_GLOBAL_CONTEXT_FILE_NAME } from '../../../cli/constants';
import { resolveSSRMode } from '../../../cli/ssr/mode';
import { FILE_SYSTEM_ROUTES_FILE_NAME } from '../constants';
import { walk } from './nestedRoutes';
import * as templates from './templates';
Expand Down Expand Up @@ -173,14 +174,14 @@ export const generateCode = async (
config.server.ssrByEntries,
packageName,
);
const useSSG = isSSGEntry(config, entryName, entrypoints);

let mode: SSRMode | undefined;
if (ssr) {
mode = typeof ssr === 'object' ? ssr.mode || 'string' : 'string';
}
const ssrMode = resolveSSRMode({
entry: entrypoint.entryName,
config,
appDirectory: appContext.appDirectory,
});

if (mode === 'stream') {
if (ssrMode === 'stream') {
const hasPageRoute = routes.some(
route => 'type' in route && route.type === 'page',
);
Expand All @@ -197,7 +198,7 @@ export const generateCode = async (
code: await templates.fileSystemRoutes({
metaName,
routes: routes,
ssrMode: useSSG ? 'string' : isUseRsc(config) ? 'stream' : mode,
ssrMode: isUseRsc(config) ? 'stream' : ssrMode,
nestedRoutesEntry: entrypoint.nestedRoutesEntry,
entryName: entrypoint.entryName,
internalDirectory,
Expand Down Expand Up @@ -233,7 +234,7 @@ export const generateCode = async (
const serverRoutesCode = await templates.fileSystemRoutes({
metaName,
routes: filtedRoutesForServer,
ssrMode: useSSG ? 'string' : mode,
ssrMode,
nestedRoutesEntry: entrypoint.nestedRoutesEntry,
entryName: entrypoint.entryName,
internalDirectory,
Expand Down
2 changes: 1 addition & 1 deletion packages/toolkit/types/common/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ export type ServerPlugin = {
options?: Record<string, any>;
};

export type SSRMode = 'string' | 'stream';
export type SSRMode = 'string' | 'stream' | false;