Skip to content

Commit e9b2f3e

Browse files
committed
feat: rebase main
1 parent 085cfed commit e9b2f3e

File tree

10 files changed

+206
-18
lines changed

10 files changed

+206
-18
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: 48 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(
@@ -35,6 +37,7 @@ export async function createDevServer(
3537
};
3638

3739
const server = createServerBase(prodServerOptions);
40+
let currentServer = server;
3841

3942
const devHttpsOption = typeof dev === 'object' && dev.https;
4043
const isHttp2 = !!devHttpsOption;
@@ -43,12 +46,14 @@ export async function createDevServer(
4346
const { genHttpsOptions } = await import('./dev-tools/https');
4447
const httpsOptions = await genHttpsOptions(devHttpsOption, pwd);
4548
nodeServer = await createNodeServer(
46-
server.handle.bind(server),
49+
(req, res) => currentServer.handle(req, res),
4750
httpsOptions,
4851
isHttp2,
4952
);
5053
} else {
51-
nodeServer = await createNodeServer(server.handle.bind(server));
54+
nodeServer = await createNodeServer((req, res) =>
55+
currentServer.handle(req, res),
56+
);
5257
}
5358

5459
const promise = getDevAssetPrefix(builder);
@@ -78,8 +83,48 @@ export async function createDevServer(
7883
await builderDevServer?.afterListen();
7984
};
8085

86+
const reload = async () => {
87+
try {
88+
const updatedServerConfig =
89+
(await loadServerRuntimeConfig(serverConfigPath)) || {};
90+
91+
const updatedProdServerOptions = {
92+
...options,
93+
pwd: distDir,
94+
serverConfig: {
95+
...updatedServerConfig,
96+
...options.serverConfig,
97+
},
98+
plugins: [
99+
...(updatedServerConfig.plugins || []),
100+
...(options.plugins || []),
101+
],
102+
};
103+
104+
const newServer = createServerBase(updatedProdServerOptions);
105+
106+
await manager.close(ResourceType.Watcher);
107+
108+
newServer.addPlugins([
109+
devPlugin({
110+
...options,
111+
}),
112+
]);
113+
114+
await applyPlugins(newServer, updatedProdServerOptions, nodeServer);
115+
116+
await newServer.init();
117+
118+
currentServer = newServer;
119+
logger.info(`Custom Web Server HMR succeeded`);
120+
} catch (e) {
121+
logger.error('[Custom Web Server HMR failed]:', e);
122+
}
123+
};
124+
81125
return {
82126
server: nodeServer,
83127
afterListen,
128+
reload,
84129
};
85130
}

packages/server/server/src/dev.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
onRepack,
1111
startWatcher,
1212
} from './helpers';
13+
import { ResourceManager, ResourceType } from './helpers/utils';
1314
import type { ModernDevServerOptions } from './types';
1415

1516
type BuilderDevServer = Awaited<ReturnType<BuilderInstance['createDevServer']>>;
@@ -18,14 +19,14 @@ export type DevPluginOptions = ModernDevServerOptions<ServerBaseOptions> & {
1819
builderDevServer?: BuilderDevServer;
1920
};
2021

22+
export const manager = new ResourceManager();
23+
2124
export const devPlugin = (options: DevPluginOptions): ServerPlugin => ({
2225
name: '@modern-js/plugin-dev',
2326

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

27-
const closeCb: Array<(...args: []) => any> = [];
28-
2930
const dev = getDevOptions(options.dev);
3031

3132
api.onPrepare(async () => {
@@ -36,7 +37,9 @@ export const devPlugin = (options: DevPluginOptions): ServerPlugin => ({
3637
connectWebSocket,
3738
} = builderDevServer || {};
3839

39-
close && closeCb.push(close);
40+
if (close) {
41+
manager.register(ResourceType.Builder, close);
42+
}
4043

4144
const {
4245
middlewares,
@@ -53,14 +56,12 @@ export const devPlugin = (options: DevPluginOptions): ServerPlugin => ({
5356
// TODO: remove any
5457
const hooks = (api as any).getHooks();
5558

56-
// Handle webpack rebuild
5759
builder?.onDevCompileDone(({ stats }) => {
5860
if (stats.toJson({ all: false }).name !== 'server') {
5961
onRepack(distDirectory!, hooks);
6062
}
6163
});
6264

63-
// Handle watch
6465
const { watchOptions } = config.server;
6566
const watcher = startWatcher({
6667
pwd,
@@ -70,13 +71,11 @@ export const devPlugin = (options: DevPluginOptions): ServerPlugin => ({
7071
watchOptions,
7172
server: serverBase!,
7273
});
73-
closeCb.push(watcher.close.bind(watcher));
74-
closeCb.length > 0 &&
75-
nodeServer?.on('close', () => {
76-
closeCb.forEach(cb => {
77-
cb();
78-
});
79-
});
74+
manager.register(ResourceType.Watcher, watcher.close.bind(watcher));
75+
76+
nodeServer?.on('close', () => {
77+
manager.closeAll();
78+
});
8079

8180
// Handle setupMiddlewares
8281
const before: RequestHandler[] = [];
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
@@ -16,7 +16,7 @@ import {
1616
import type { ConfigChain } from '@rsbuild/core';
1717
import type { AppNormalizedConfig, AppTools } from '../types';
1818
import { buildServerConfig } from '../utils/config';
19-
import { setServer } from '../utils/createServer';
19+
import { createServer, setServer } from '../utils/createServer';
2020
import { loadServerPlugins } from '../utils/loadPlugins';
2121
import { printInstructions } from '../utils/printInstructions';
2222
import { registerCompiler } from '../utils/register';
@@ -103,7 +103,7 @@ export const dev = async (
103103
const host = normalizedConfig.dev?.host || DEFAULT_DEV_HOST;
104104

105105
if (apiOnly) {
106-
const { server } = await createDevServer(
106+
const { server } = await createServer(
107107
{
108108
...serverOptions,
109109
runCompile: false,
@@ -126,7 +126,7 @@ export const dev = async (
126126
);
127127
setServer(server);
128128
} else {
129-
const { server, afterListen } = await createDevServer(
129+
const { server, afterListen } = await createServer(
130130
{
131131
...serverOptions,
132132
builder: appContext.builder,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import analyzePlugin from './plugins/analyze';
2525
import deployPlugin from './plugins/deploy';
2626
import initializePlugin from './plugins/initialize';
2727
import serverBuildPlugin from './plugins/serverBuild';
28+
import { serverHmrPlugin } from './plugins/serverHmr';
2829
import serverRuntimePlugin from './plugins/serverRuntime';
2930
import type { AppTools, CliPlugin } from './types';
3031
import type {
@@ -53,6 +54,7 @@ export const appTools = (): CliPlugin<AppTools> => ({
5354
analyzePlugin(),
5455
serverBuildPlugin(),
5556
deployPlugin(),
57+
serverHmrPlugin(),
5658
],
5759
post: [
5860
'@modern-js/plugin-initialize',
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import path from 'path';
2+
import type { ServerPlugin } from '@modern-js/server-core';
3+
import { reloadServer } from '../utils/createServer';
4+
5+
import type { AppTools, CliPlugin } from '../types';
6+
7+
export const serverHmrPlugin = (): CliPlugin<AppTools> => ({
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 (): ServerPlugin => ({
22+
name: '@modern-js/server-hmr-plugin',
23+
setup: api => {
24+
api.onReset(async ({ event }) => {
25+
if (event.type === 'file-change') {
26+
const { appDirectory } = api.getServerContext();
27+
const serverPath = path.join(appDirectory, 'server');
28+
const indexPath = path.join(serverPath, 'index');
29+
const isServerFileChanged = event.payload.some(
30+
({ filename }) =>
31+
filename.startsWith(serverPath) && !filename.startsWith(indexPath),
32+
);
33+
if (isServerFileChanged) {
34+
await reloadServer?.();
35+
}
36+
}
37+
});
38+
},
39+
});

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import type { Server } from 'node:http';
22
import type { Http2SecureServer } from 'node:http2';
3+
import { applyPlugins } from '@modern-js/prod-server';
4+
import {
5+
type ApplyPlugins,
6+
type ModernDevServerOptions,
7+
createDevServer,
8+
} from '@modern-js/server';
39

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

613
export const getServer = () => server;
714

@@ -15,3 +22,24 @@ export const closeServer = async () => {
1522
server = null;
1623
}
1724
};
25+
26+
export const createServer = async (
27+
options: ModernDevServerOptions,
28+
applyPluginsFn?: ApplyPlugins,
29+
): Promise<{
30+
server: Server | Http2SecureServer;
31+
afterListen: () => Promise<void>;
32+
}> => {
33+
if (server) {
34+
server.close();
35+
}
36+
const {
37+
server: newServer,
38+
afterListen,
39+
reload: reloadDevServer,
40+
} = await createDevServer(options, applyPluginsFn || applyPlugins);
41+
42+
reloadServer = reloadDevServer;
43+
setServer(newServer);
44+
return { server: newServer, afterListen };
45+
};

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

Lines changed: 33 additions & 0 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

@@ -58,4 +59,36 @@ describe('test app-tools utils', () => {
5859
resolve();
5960
});
6061
});
62+
63+
it('should create and close server correctly', async () => {
64+
const app = await createServer({
65+
pwd: __dirname,
66+
config: {
67+
html: {},
68+
output: {
69+
distPath: {
70+
root: 'dist',
71+
},
72+
},
73+
source: {},
74+
tools: {},
75+
server: {},
76+
runtime: {},
77+
bff: {},
78+
dev: {},
79+
security: {},
80+
},
81+
appContext: {},
82+
dev: {},
83+
});
84+
85+
expect(app.server instanceof Server).toBe(true);
86+
expect(getServer()).toBe(app.server);
87+
88+
await reloadServer?.();
89+
expect(getServer()).toBe(app.server);
90+
91+
await closeServer();
92+
expect(getServer()).toBeNull();
93+
});
6194
});

0 commit comments

Comments
 (0)