Skip to content
Closed
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
3 changes: 1 addition & 2 deletions packages/cli/plugin-ssg/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@
"@modern-js/utils": "workspace:*",
"@swc/helpers": "^0.5.17",
"node-mocks-http": "^1.11.0",
"normalize-path": "3.0.0",
"portfinder": "^1.0.37"
"normalize-path": "3.0.0"
},
"peerDependencies": {
"react-router-dom": ">=7.0.0"
Expand Down
212 changes: 134 additions & 78 deletions packages/cli/plugin-ssg/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,151 @@
import childProcess from 'child_process';
import { IncomingMessage, ServerResponse } from 'node:http';
import path from 'path';
import type {
AppNormalizedConfig,
AppToolsExtendAPI,
} from '@modern-js/app-tools';
import type { ServerRoute as ModernRoute } from '@modern-js/types';
import { logger } from '@modern-js/utils';
import { openRouteSSR } from '../libs/util';
import {
type ProdServerOptions,
createProdServer,
loadServerPlugins,
} from '@modern-js/prod-server';
import type {
ServerRoute as ModernRoute,
ServerPlugin,
} from '@modern-js/types';
import { SERVER_DIR, createLogger, getMeta, logger } from '@modern-js/utils';
import { chunkArray, openRouteSSR } from '../libs/util';
import type { SsgRoute } from '../types';
import { CLOSE_SIGN } from './consts';

export const createServer = (
// SSG only interrupt when stderror, so we need to override the rslog's error to console.error
function getLogger() {
const l = createLogger({
level: 'verbose',
});
return {
...l,
error: (...args: any[]) => {
console.error(...args);
},
};
}

const MAX_CONCURRENT_REQUESTS = 10;

function createMockIncomingMessage(
url: string,
headers: Record<string, string> = {},
): IncomingMessage {
const urlObj = new URL(url);
const mockReq = new IncomingMessage({} as any);

// Set basic properties that createWebRequest uses
mockReq.url = urlObj.pathname + urlObj.search;
mockReq.method = 'GET';
mockReq.headers = {
host: urlObj.host,
'user-agent': 'SSG-Renderer/1.0',
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'accept-language': 'en-US,en;q=0.5',
'accept-encoding': 'gzip, deflate',
connection: 'keep-alive',
...headers,
};

// Set other required properties for IncomingMessage
mockReq.httpVersion = '1.1';
mockReq.httpVersionMajor = 1;
mockReq.httpVersionMinor = 1;
mockReq.complete = true;
mockReq.rawHeaders = [];
mockReq.socket = {} as any;
mockReq.connection = mockReq.socket;

return mockReq;
}

function createMockServerResponse(): ServerResponse {
const mockRes = new ServerResponse({} as any);
return mockRes;
}

export const createServer = async (
api: AppToolsExtendAPI,
ssgRoutes: SsgRoute[],
pageRoutes: ModernRoute[],
apiRoutes: ModernRoute[],
options: AppNormalizedConfig,
appDirectory: string,
): Promise<string[]> =>
new Promise((resolve, reject) => {
// this side of the shallow copy of a route for subsequent render processing, to prevent the modification of the current field
// manually enable the server-side rendering configuration for all routes that require SSG
const entries = ssgRoutes.map(route => route.entryName!);
const backup: ModernRoute[] = openRouteSSR(pageRoutes, entries);
): Promise<string[]> => {
// this side of the shallow copy of a route for subsequent render processing, to prevent the modification of the current field
// manually enable the server-side rendering configuration for all routes that require SSG
const entries = ssgRoutes.map(route => route.entryName!);
const backup: ModernRoute[] = openRouteSSR(pageRoutes, entries);
const total = backup.concat(apiRoutes);

try {
const appContext = api.useAppContext();
const meta = getMeta(appContext.metaName);

const total = backup.concat(apiRoutes);
const distDirectory = appContext.distDirectory;
const serverConfigPath = path.resolve(
distDirectory,
SERVER_DIR,
`${meta}.server`,
);

const cp = childProcess.fork(path.join(__dirname, 'process'), {
cwd: appDirectory,
silent: true,
});
const plugins: ServerPlugin[] = appContext.serverPlugins;

const appContext = api.useAppContext();
// Todo: need use collect server plugins
// maybe build command need add collect, or just call collectServerPlugin hooks
const plugins = appContext.serverPlugins;

cp.send(
JSON.stringify({
options,
renderRoutes: ssgRoutes,
routes: total,
appContext: {
// Make sure that bff runs the product of the dist directory, because we dont register ts-node in the child process
apiDirectory: path.join(
appContext.distDirectory,
path.relative(appContext.appDirectory, appContext.apiDirectory),
),
lambdaDirectory: path.join(
appContext.distDirectory,
path.relative(appContext.appDirectory, appContext.lambdaDirectory),
),
appDirectory: appContext.appDirectory,
metaName: appContext.metaName,
},
const serverOptions: ProdServerOptions = {
pwd: distDirectory,
config: options as any,
appContext,
serverConfigPath,
routes: total,
plugins: await loadServerPlugins(
plugins,
distDirectory: appContext.distDirectory,
}),
);
appContext.appDirectory || distDirectory,
),
staticGenerate: true,
logger: getLogger(),
};

const htmlChunks: string[] = [];
const htmlAry: string[] = [];

cp.on('message', (chunk: string) => {
if (chunk !== null) {
htmlChunks.push(chunk);
} else {
const html = htmlChunks.join('');
htmlAry.push(html);
htmlChunks.length = 0;
}

if (htmlAry.length === ssgRoutes.length) {
cp.send(CLOSE_SIGN);
resolve(htmlAry);
}
});

cp.stderr?.on('data', chunk => {
const str = chunk.toString();
if (str.includes('Error')) {
logger.error(str);
reject(new Error('ssg render failed'));
cp.kill('SIGKILL');
} else {
logger.info(str.replace(/[^\S\n]+/g, ' '));
}
});

cp.stdout?.on('data', chunk => {
const str = chunk.toString();
logger.info(str.replace(/[^\S\n]+/g, ' '));
});
});
const nodeServer = await createProdServer(serverOptions);
const requestHandler = nodeServer.getRequestHandler();

const chunkedRoutes = chunkArray(ssgRoutes, MAX_CONCURRENT_REQUESTS);
const results: string[] = [];

for (const routes of chunkedRoutes) {
const promises = routes.map(async route => {
const url = `http://localhost${route.urlPath}`;
const request = new Request(url, {
method: 'GET',
headers: {
host: 'localhost',
},
});

const mockReq = createMockIncomingMessage(url);
const mockRes = createMockServerResponse();

const response = await requestHandler(request, {
// It is mainly for the enableHandleWeb scenario; the req is useless for other scenarios.
node: {
req: mockReq,
res: mockRes,
},
});

return await response.text();
});

const batch = await Promise.all(promises);
results.push(...batch);
}

return results;
} catch (e) {
logger.error(e instanceof Error ? e.stack : (e as any).toString());
throw new Error('ssg render failed');
}
};
147 changes: 0 additions & 147 deletions packages/cli/plugin-ssg/src/server/process.ts

This file was deleted.

Loading
Loading