diff --git a/.changeset/six-carpets-hope.md b/.changeset/six-carpets-hope.md new file mode 100644 index 000000000000..1e7e26dad5c0 --- /dev/null +++ b/.changeset/six-carpets-hope.md @@ -0,0 +1,7 @@ +--- +'@modern-js/app-tools': patch +'@modern-js/server': patch +--- + +feat: server config hot reload +feat: 支持自定义 web server 热更新 diff --git a/packages/server/server/src/createDevServer.ts b/packages/server/server/src/createDevServer.ts index 126639734cbd..27c02ee0576e 100644 --- a/packages/server/server/src/createDevServer.ts +++ b/packages/server/server/src/createDevServer.ts @@ -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( @@ -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'; @@ -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); @@ -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, }; } diff --git a/packages/server/server/src/dev.ts b/packages/server/server/src/dev.ts index 46345020b924..e8a4907c8b42 100644 --- a/packages/server/server/src/dev.ts +++ b/packages/server/server/src/dev.ts @@ -13,6 +13,7 @@ import { onRepack, startWatcher, } from './helpers'; +import { ResourceManager, ResourceType } from './helpers/utils'; import type { ModernDevServerOptions } from './types'; type BuilderDevServer = Awaited< @@ -23,14 +24,14 @@ export type DevPluginOptions = ModernDevServerOptions & { 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 { @@ -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, @@ -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[] = []; diff --git a/packages/server/server/src/helpers/utils.ts b/packages/server/server/src/helpers/utils.ts index 4142e7cc52c4..ec0b039fed49 100644 --- a/packages/server/server/src/helpers/utils.ts +++ b/packages/server/server/src/helpers/utils.ts @@ -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 Promise) | null> = { + [ResourceType.Builder]: null, + [ResourceType.Watcher]: null, + }; + + register(type: ResourceType, cb: () => Promise) { + 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; + } +} diff --git a/packages/solutions/app-tools/package.json b/packages/solutions/app-tools/package.json index ce773da67de2..42b4d050bd2d 100644 --- a/packages/solutions/app-tools/package.json +++ b/packages/solutions/app-tools/package.json @@ -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": { diff --git a/packages/solutions/app-tools/src/commands/dev.ts b/packages/solutions/app-tools/src/commands/dev.ts index c30649b4febe..c497b2f4dfe1 100644 --- a/packages/solutions/app-tools/src/commands/dev.ts +++ b/packages/solutions/app-tools/src/commands/dev.ts @@ -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'; @@ -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, @@ -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, diff --git a/packages/solutions/app-tools/src/index.ts b/packages/solutions/app-tools/src/index.ts index 3145665bd32e..6d9473821e2c 100644 --- a/packages/solutions/app-tools/src/index.ts +++ b/packages/solutions/app-tools/src/index.ts @@ -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 { @@ -75,6 +76,7 @@ export const appTools = ( }), serverBuildPlugin(), deployPlugin(), + serverHmrPlugin(), ], post: [ '@modern-js/plugin-initialize', diff --git a/packages/solutions/app-tools/src/plugins/serverHmr.ts b/packages/solutions/app-tools/src/plugins/serverHmr.ts new file mode 100644 index 000000000000..f34a2c22c229 --- /dev/null +++ b/packages/solutions/app-tools/src/plugins/serverHmr.ts @@ -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> => ({ + 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?.(); + } + } + }, + }; + }, +}); diff --git a/packages/solutions/app-tools/src/utils/createServer.ts b/packages/solutions/app-tools/src/utils/createServer.ts index 3b7513407dd9..38a6b0d0c541 100644 --- a/packages/solutions/app-tools/src/utils/createServer.ts +++ b/packages/solutions/app-tools/src/utils/createServer.ts @@ -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) | null = null; export const getServer = () => server; @@ -23,11 +25,21 @@ export const closeServer = async () => { export const createServer = async ( options: ModernDevServerOptions, -): Promise => { + applyPluginsFn?: ApplyPlugins, +): Promise<{ + server: Server | Http2SecureServer; + afterListen: () => Promise; +}> => { 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 }; }; diff --git a/packages/solutions/app-tools/tests/utils.test.ts b/packages/solutions/app-tools/tests/utils.test.ts index 9678c982d3ac..8034264a30b8 100644 --- a/packages/solutions/app-tools/tests/utils.test.ts +++ b/packages/solutions/app-tools/tests/utils.test.ts @@ -4,6 +4,7 @@ import { closeServer, createServer, getServer, + reloadServer, } from '../src/utils/createServer'; import { getSelectedEntries } from '../src/utils/getSelectedEntries'; @@ -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();