Skip to content

Commit 36b121b

Browse files
committed
feat: server config hot reload
1 parent af15f53 commit 36b121b

File tree

10 files changed

+173
-21
lines changed

10 files changed

+173
-21
lines changed

.changeset/six-carpets-hope.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@modern-js/app-tools': patch
3+
'@modern-js/server': patch
4+
---
5+
6+
feat: server config hot reload
7+
feat: 支持自定义 web server 热更新

packages/server/server/src/createDevServer.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import {
44
createNodeServer,
55
loadServerRuntimeConfig,
66
} from '@modern-js/server-core/node';
7-
import { devPlugin } from './dev';
7+
import { logger } from '@modern-js/utils';
8+
import { devPlugin, manager } from './dev';
89
import { getDevAssetPrefix, getDevOptions } from './helpers';
10+
import { ResourceType } from './helpers/utils';
911
import type { ApplyPlugins, ModernDevServerOptions } from './types';
1012

1113
export async function createDevServer(
@@ -42,6 +44,7 @@ export async function createDevServer(
4244
};
4345

4446
const server = createServerBase(prodServerOptions);
47+
let currentServer = server;
4548

4649
const devHttpsOption = typeof dev === 'object' && dev.https;
4750
const isHttp2 = devHttpsOption && typeof dev.proxy === 'undefined';
@@ -50,12 +53,14 @@ export async function createDevServer(
5053
const { genHttpsOptions } = await import('./dev-tools/https');
5154
const httpsOptions = await genHttpsOptions(devHttpsOption, pwd);
5255
nodeServer = await createNodeServer(
53-
server.handle.bind(server),
56+
(req, res) => currentServer.handle(req, res),
5457
httpsOptions,
5558
isHttp2,
5659
);
5760
} else {
58-
nodeServer = await createNodeServer(server.handle.bind(server));
61+
nodeServer = await createNodeServer((req, res) =>
62+
currentServer.handle(req, res),
63+
);
5964
}
6065

6166
const promise = getDevAssetPrefix(builder);
@@ -85,8 +90,53 @@ export async function createDevServer(
8590
await builderDevServer?.afterListen();
8691
};
8792

93+
const reload = async () => {
94+
try {
95+
const updatedServerConfig =
96+
(await loadServerRuntimeConfig(
97+
distDir,
98+
serverConfigFile,
99+
serverConfigPath,
100+
metaName,
101+
)) || {};
102+
103+
const updatedProdServerOptions = {
104+
...options,
105+
pwd: distDir,
106+
serverConfig: {
107+
...updatedServerConfig,
108+
...options.serverConfig,
109+
},
110+
plugins: [
111+
...(updatedServerConfig.plugins || []),
112+
...(options.plugins || []),
113+
],
114+
};
115+
116+
const newServer = createServerBase(updatedProdServerOptions);
117+
118+
await manager.close(ResourceType.Watcher);
119+
120+
newServer.addPlugins([
121+
devPlugin({
122+
...options,
123+
}),
124+
]);
125+
126+
await applyPlugins(newServer, updatedProdServerOptions, nodeServer);
127+
128+
await newServer.init();
129+
130+
currentServer = newServer;
131+
logger.info(`Custom Web Server HMR succeeded`);
132+
} catch (e) {
133+
logger.error('[Custom Web Server HMR failed]:', e);
134+
}
135+
};
136+
88137
return {
89138
server: nodeServer,
90139
afterListen,
140+
reload,
91141
};
92142
}

packages/server/server/src/dev.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
onRepack,
1414
startWatcher,
1515
} from './helpers';
16+
import { ResourceManager, ResourceType } from './helpers/utils';
1617
import type { ModernDevServerOptions } from './types';
1718

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

27+
export const manager = new ResourceManager();
28+
2629
export const devPlugin = (options: DevPluginOptions): ServerPluginLegacy => ({
2730
name: '@modern-js/plugin-dev',
2831

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

32-
const closeCb: Array<(...args: []) => any> = [];
33-
3435
const dev = getDevOptions(options);
3536

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

45-
close && closeCb.push(close);
46+
if (close) {
47+
manager.register(ResourceType.Builder, close);
48+
}
4649

4750
const {
4851
middlewares,
@@ -75,15 +78,12 @@ export const devPlugin = (options: DevPluginOptions): ServerPluginLegacy => ({
7578
watchOptions,
7679
server: serverBase!,
7780
});
78-
closeCb.push(watcher.close.bind(watcher));
81+
manager.register(ResourceType.Watcher, watcher.close.bind(watcher));
7982
}
8083

81-
closeCb.length > 0 &&
82-
nodeServer?.on('close', () => {
83-
closeCb.forEach(cb => {
84-
cb();
85-
});
86-
});
84+
nodeServer?.on('close', () => {
85+
manager.closeAll();
86+
});
8787

8888
const before: RequestHandler[] = [];
8989

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,33 @@
11
import { createDebugger } from '@modern-js/utils';
22

33
export const debug = createDebugger('server');
4+
5+
export enum ResourceType {
6+
Builder = 'builder',
7+
Watcher = 'watcher',
8+
}
9+
10+
export class ResourceManager {
11+
private resources: Record<ResourceType, (() => Promise<void>) | null> = {
12+
[ResourceType.Builder]: null,
13+
[ResourceType.Watcher]: null,
14+
};
15+
16+
register(type: ResourceType, cb: () => Promise<void>) {
17+
this.resources[type] = cb;
18+
}
19+
20+
async close(type: ResourceType) {
21+
await this.resources[type]?.();
22+
this.resources[type] = null;
23+
}
24+
25+
async closeAll() {
26+
await Promise.allSettled([
27+
this.resources[ResourceType.Builder]?.() || Promise.resolve(),
28+
this.resources[ResourceType.Watcher]?.() || Promise.resolve(),
29+
]);
30+
this.resources[ResourceType.Builder] = null;
31+
this.resources[ResourceType.Watcher] = null;
32+
}
33+
}

packages/solutions/app-tools/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@
5353
"types": "./dist/types/exports/server.d.ts",
5454
"jsnext:source": "./src/exports/server.ts",
5555
"default": "./dist/cjs/exports/server.js"
56+
},
57+
"./server/hmr": {
58+
"types": "./dist/types/plugins/serverHmr.d.ts",
59+
"jsnext:source": "./src/plugins/serverHmr.ts",
60+
"default": "./dist/cjs/plugins/serverHmr.js"
5661
}
5762
},
5863
"engines": {

packages/solutions/app-tools/src/commands/dev.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import type { ConfigChain } from '@rsbuild/core';
1313
import type { AppNormalizedConfig, AppTools } from '../types';
1414
import { buildServerConfig } from '../utils/config';
15-
import { setServer } from '../utils/createServer';
15+
import { createServer, setServer } from '../utils/createServer';
1616
import { loadServerPlugins } from '../utils/loadPlugins';
1717
import { printInstructions } from '../utils/printInstructions';
1818
import { registerCompiler } from '../utils/register';
@@ -128,7 +128,7 @@ export const dev = async (
128128
const host = normalizedConfig.dev?.host || DEFAULT_DEV_HOST;
129129

130130
if (apiOnly) {
131-
const { server } = await createDevServer(
131+
const { server } = await createServer(
132132
{
133133
...serverOptions,
134134
runCompile: false,
@@ -151,7 +151,7 @@ export const dev = async (
151151
);
152152
setServer(server);
153153
} else {
154-
const { server, afterListen } = await createDevServer(
154+
const { server, afterListen } = await createServer(
155155
{
156156
...serverOptions,
157157
builder: appContext.builder,

packages/solutions/app-tools/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import analyzePlugin from './plugins/analyze';
2828
import deployPlugin from './plugins/deploy';
2929
import initializePlugin from './plugins/initialize';
3030
import serverBuildPlugin from './plugins/serverBuild';
31+
import { serverHmrPlugin } from './plugins/serverHmr';
3132
import serverRuntimePlugin from './plugins/serverRuntime';
3233
import type { AppTools, AppToolsOptions, CliPluginFuture } from './types';
3334
import type {
@@ -75,6 +76,7 @@ export const appTools = (
7576
}),
7677
serverBuildPlugin(),
7778
deployPlugin(),
79+
serverHmrPlugin(),
7880
],
7981
post: [
8082
'@modern-js/plugin-initialize',
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import path from 'path';
2+
import type { ServerPluginLegacy } from '@modern-js/server-core';
3+
import { reloadServer } from '../utils/createServer';
4+
5+
import type { AppTools, CliPluginFuture } from '../types';
6+
7+
export const serverHmrPlugin = (): CliPluginFuture<AppTools<'shared'>> => ({
8+
name: '@modern-js/server-hmr-plugin',
9+
setup(api) {
10+
api._internalServerPlugins(({ plugins }) => {
11+
if (process.env.NODE_ENV === 'development') {
12+
plugins.push({
13+
name: '@modern-js/app-tools/server/hmr',
14+
});
15+
}
16+
return { plugins };
17+
});
18+
},
19+
});
20+
21+
export default (): ServerPluginLegacy => ({
22+
name: '@modern-js/server-hmr-plugin',
23+
setup: api => {
24+
return {
25+
async reset({ event }) {
26+
if (event.type === 'file-change') {
27+
const { appDirectory } = api.useAppContext();
28+
const serverPath = path.join(appDirectory, 'server');
29+
const indexPath = path.join(serverPath, 'index');
30+
const isServerFileChanged = event.payload.some(
31+
({ filename }) =>
32+
filename.startsWith(serverPath) &&
33+
!filename.startsWith(indexPath),
34+
);
35+
if (isServerFileChanged) {
36+
await reloadServer?.();
37+
}
38+
}
39+
},
40+
};
41+
},
42+
});

packages/solutions/app-tools/src/utils/createServer.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import type { Server } from 'node:http';
22
import type { Http2SecureServer } from 'node:http2';
33
import { applyPlugins } from '@modern-js/prod-server';
44
import {
5+
type ApplyPlugins,
56
type ModernDevServerOptions,
67
createDevServer,
78
} from '@modern-js/server';
89

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

1113
export const getServer = () => server;
1214

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

2426
export const createServer = async (
2527
options: ModernDevServerOptions,
26-
): Promise<Server | Http2SecureServer> => {
28+
applyPluginsFn?: ApplyPlugins,
29+
): Promise<{
30+
server: Server | Http2SecureServer;
31+
afterListen: () => Promise<void>;
32+
}> => {
2733
if (server) {
2834
server.close();
2935
}
30-
server = (await createDevServer(options, applyPlugins)).server;
36+
const {
37+
server: newServer,
38+
afterListen,
39+
reload: reloadDevServer,
40+
} = await createDevServer(options, applyPluginsFn || applyPlugins);
3141

32-
return server;
42+
reloadServer = reloadDevServer;
43+
setServer(newServer);
44+
return { server: newServer, afterListen };
3345
};

packages/solutions/app-tools/tests/utils.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
closeServer,
55
createServer,
66
getServer,
7+
reloadServer,
78
} from '../src/utils/createServer';
89
import { getSelectedEntries } from '../src/utils/getSelectedEntries';
910

@@ -81,8 +82,11 @@ describe('test app-tools utils', () => {
8182
dev: {},
8283
});
8384

84-
expect(app instanceof Server).toBe(true);
85-
expect(getServer()).toBe(app);
85+
expect(app.server instanceof Server).toBe(true);
86+
expect(getServer()).toBe(app.server);
87+
88+
await reloadServer?.();
89+
expect(getServer()).toBe(app.server);
8690

8791
await closeServer();
8892
expect(getServer()).toBeNull();

0 commit comments

Comments
 (0)