Skip to content

feat: server config hot reload #7339

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
7 changes: 7 additions & 0 deletions .changeset/six-carpets-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@modern-js/app-tools': patch
'@modern-js/server': patch
---

feat: server config hot reload
feat: 支持自定义 web server 热更新
56 changes: 53 additions & 3 deletions packages/server/server/src/createDevServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import {
createNodeServer,
loadServerRuntimeConfig,
} from '@modern-js/server-core/node';
import { devPlugin } from './dev';
import { logger } from '@modern-js/utils';
import { devPlugin, manager } from './dev';
import { getDevAssetPrefix, getDevOptions } from './helpers';
import { ResourceType } from './helpers/utils';
import type { ApplyPlugins, ModernDevServerOptions } from './types';

export async function createDevServer(
Expand Down Expand Up @@ -42,6 +44,7 @@ export async function createDevServer(
};

const server = createServerBase(prodServerOptions);
let currentServer = server;

const devHttpsOption = typeof dev === 'object' && dev.https;
const isHttp2 = devHttpsOption && typeof dev.proxy === 'undefined';
Expand All @@ -50,12 +53,14 @@ export async function createDevServer(
const { genHttpsOptions } = await import('./dev-tools/https');
const httpsOptions = await genHttpsOptions(devHttpsOption, pwd);
nodeServer = await createNodeServer(
server.handle.bind(server),
(req, res) => currentServer.handle(req, res),
httpsOptions,
isHttp2,
);
} else {
nodeServer = await createNodeServer(server.handle.bind(server));
nodeServer = await createNodeServer((req, res) =>
currentServer.handle(req, res),
);
}

const promise = getDevAssetPrefix(builder);
Expand Down Expand Up @@ -85,8 +90,53 @@ export async function createDevServer(
await builderDevServer?.afterListen();
};

const reload = async () => {
try {
const updatedServerConfig =
(await loadServerRuntimeConfig(
distDir,
serverConfigFile,
serverConfigPath,
metaName,
)) || {};

const updatedProdServerOptions = {
...options,
pwd: distDir,
serverConfig: {
...updatedServerConfig,
...options.serverConfig,
},
plugins: [
...(updatedServerConfig.plugins || []),
...(options.plugins || []),
],
};

const newServer = createServerBase(updatedProdServerOptions);

await manager.close(ResourceType.Watcher);

newServer.addPlugins([
devPlugin({
...options,
}),
]);

await applyPlugins(newServer, updatedProdServerOptions, nodeServer);

await newServer.init();

currentServer = newServer;
logger.info(`Custom Web Server HMR succeeded`);
} catch (e) {
logger.error('[Custom Web Server HMR failed]:', e);
}
};

return {
server: nodeServer,
afterListen,
reload,
};
}
20 changes: 10 additions & 10 deletions packages/server/server/src/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
onRepack,
startWatcher,
} from './helpers';
import { ResourceManager, ResourceType } from './helpers/utils';
import type { ModernDevServerOptions } from './types';

type BuilderDevServer = Awaited<
Expand All @@ -23,14 +24,14 @@ export type DevPluginOptions = ModernDevServerOptions<ServerBaseOptions> & {
builderDevServer?: BuilderDevServer;
};

export const manager = new ResourceManager();

export const devPlugin = (options: DevPluginOptions): ServerPluginLegacy => ({
name: '@modern-js/plugin-dev',

setup(api) {
const { config, pwd, builder, builderDevServer } = options;

const closeCb: Array<(...args: []) => any> = [];

const dev = getDevOptions(options);

return {
Expand All @@ -42,7 +43,9 @@ export const devPlugin = (options: DevPluginOptions): ServerPluginLegacy => ({
connectWebSocket,
} = builderDevServer || {};

close && closeCb.push(close);
if (close) {
manager.register(ResourceType.Builder, close);
}

const {
middlewares,
Expand Down Expand Up @@ -75,15 +78,12 @@ export const devPlugin = (options: DevPluginOptions): ServerPluginLegacy => ({
watchOptions,
server: serverBase!,
});
closeCb.push(watcher.close.bind(watcher));
manager.register(ResourceType.Watcher, watcher.close.bind(watcher));
}

closeCb.length > 0 &&
nodeServer?.on('close', () => {
closeCb.forEach(cb => {
cb();
});
});
nodeServer?.on('close', () => {
manager.closeAll();
});

const before: RequestHandler[] = [];

Expand Down
30 changes: 30 additions & 0 deletions packages/server/server/src/helpers/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
import { createDebugger } from '@modern-js/utils';

export const debug = createDebugger('server');

export enum ResourceType {
Builder = 'builder',
Watcher = 'watcher',
}

export class ResourceManager {
private resources: Record<ResourceType, (() => Promise<void>) | null> = {
[ResourceType.Builder]: null,
[ResourceType.Watcher]: null,
};

register(type: ResourceType, cb: () => Promise<void>) {
this.resources[type] = cb;
}

async close(type: ResourceType) {
await this.resources[type]?.();
this.resources[type] = null;
}

async closeAll() {
await Promise.allSettled([
this.resources[ResourceType.Builder]?.() || Promise.resolve(),
this.resources[ResourceType.Watcher]?.() || Promise.resolve(),
]);
this.resources[ResourceType.Builder] = null;
this.resources[ResourceType.Watcher] = null;
}
}
5 changes: 5 additions & 0 deletions packages/solutions/app-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@
"types": "./dist/types/exports/server.d.ts",
"jsnext:source": "./src/exports/server.ts",
"default": "./dist/cjs/exports/server.js"
},
"./server/hmr": {
"types": "./dist/types/plugins/serverHmr.d.ts",
"jsnext:source": "./src/plugins/serverHmr.ts",
"default": "./dist/cjs/plugins/serverHmr.js"
}
},
"engines": {
Expand Down
6 changes: 3 additions & 3 deletions packages/solutions/app-tools/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import type { ConfigChain } from '@rsbuild/core';
import type { AppNormalizedConfig, AppTools } from '../types';
import { buildServerConfig } from '../utils/config';
import { setServer } from '../utils/createServer';
import { createServer, setServer } from '../utils/createServer';
import { loadServerPlugins } from '../utils/loadPlugins';
import { printInstructions } from '../utils/printInstructions';
import { registerCompiler } from '../utils/register';
Expand Down Expand Up @@ -128,7 +128,7 @@ export const dev = async (
const host = normalizedConfig.dev?.host || DEFAULT_DEV_HOST;

if (apiOnly) {
const { server } = await createDevServer(
const { server } = await createServer(
{
...serverOptions,
runCompile: false,
Expand All @@ -151,7 +151,7 @@ export const dev = async (
);
setServer(server);
} else {
const { server, afterListen } = await createDevServer(
const { server, afterListen } = await createServer(
{
...serverOptions,
builder: appContext.builder,
Expand Down
2 changes: 2 additions & 0 deletions packages/solutions/app-tools/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import analyzePlugin from './plugins/analyze';
import deployPlugin from './plugins/deploy';
import initializePlugin from './plugins/initialize';
import serverBuildPlugin from './plugins/serverBuild';
import { serverHmrPlugin } from './plugins/serverHmr';
import serverRuntimePlugin from './plugins/serverRuntime';
import type { AppTools, AppToolsOptions, CliPluginFuture } from './types';
import type {
Expand Down Expand Up @@ -75,6 +76,7 @@ export const appTools = (
}),
serverBuildPlugin(),
deployPlugin(),
serverHmrPlugin(),
],
post: [
'@modern-js/plugin-initialize',
Expand Down
42 changes: 42 additions & 0 deletions packages/solutions/app-tools/src/plugins/serverHmr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import path from 'path';
import type { ServerPluginLegacy } from '@modern-js/server-core';
import { reloadServer } from '../utils/createServer';

import type { AppTools, CliPluginFuture } from '../types';

export const serverHmrPlugin = (): CliPluginFuture<AppTools<'shared'>> => ({
name: '@modern-js/server-hmr-plugin',
setup(api) {
api._internalServerPlugins(({ plugins }) => {
if (process.env.NODE_ENV === 'development') {
plugins.push({
name: '@modern-js/app-tools/server/hmr',
});
}
return { plugins };
});
},
});

export default (): ServerPluginLegacy => ({
name: '@modern-js/server-hmr-plugin',
setup: api => {
return {
async reset({ event }) {
if (event.type === 'file-change') {
const { appDirectory } = api.useAppContext();
const serverPath = path.join(appDirectory, 'server');
const indexPath = path.join(serverPath, 'index');
const isServerFileChanged = event.payload.some(
({ filename }) =>
filename.startsWith(serverPath) &&
!filename.startsWith(indexPath),
);
if (isServerFileChanged) {
await reloadServer?.();
}
}
},
};
},
});
18 changes: 15 additions & 3 deletions packages/solutions/app-tools/src/utils/createServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import type { Server } from 'node:http';
import type { Http2SecureServer } from 'node:http2';
import { applyPlugins } from '@modern-js/prod-server';
import {
type ApplyPlugins,
type ModernDevServerOptions,
createDevServer,
} from '@modern-js/server';

let server: Server | Http2SecureServer | null = null;
export let reloadServer: (() => Promise<void>) | null = null;

export const getServer = () => server;

Expand All @@ -23,11 +25,21 @@ export const closeServer = async () => {

export const createServer = async (
options: ModernDevServerOptions,
): Promise<Server | Http2SecureServer> => {
applyPluginsFn?: ApplyPlugins,
): Promise<{
server: Server | Http2SecureServer;
afterListen: () => Promise<void>;
}> => {
if (server) {
server.close();
}
server = (await createDevServer(options, applyPlugins)).server;
const {
server: newServer,
afterListen,
reload: reloadDevServer,
} = await createDevServer(options, applyPluginsFn || applyPlugins);

return server;
reloadServer = reloadDevServer;
setServer(newServer);
return { server: newServer, afterListen };
};
8 changes: 6 additions & 2 deletions packages/solutions/app-tools/tests/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
closeServer,
createServer,
getServer,
reloadServer,
} from '../src/utils/createServer';
import { getSelectedEntries } from '../src/utils/getSelectedEntries';

Expand Down Expand Up @@ -81,8 +82,11 @@ describe('test app-tools utils', () => {
dev: {},
});

expect(app instanceof Server).toBe(true);
expect(getServer()).toBe(app);
expect(app.server instanceof Server).toBe(true);
expect(getServer()).toBe(app.server);

await reloadServer?.();
expect(getServer()).toBe(app.server);

await closeServer();
expect(getServer()).toBeNull();
Expand Down
Loading