Skip to content
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 热更新
6 changes: 6 additions & 0 deletions packages/server/core/src/serverBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ export class ServerBase<E extends Env = any> {
get onError() {
return this.app.onError.bind(this.app);
}

close() {
this.serverContext = null;
this.plugins = [];
(this.app as Hono<E> | null) = null;
}
}

export function createServerBase<E extends Env>(options: ServerBaseOptions) {
Expand Down
94 changes: 72 additions & 22 deletions packages/server/server/src/createDevServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,68 @@ 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 serverHmrPlugin from './plugins/serverReload';
import type { ApplyPlugins, ModernDevServerOptions } from './types';

export async function createDevServer(
export let serverReload: (() => Promise<void>) | null = null;

async function createServerOptions(
options: ModernDevServerOptions,
applyPlugins: ApplyPlugins,
serverConfigPath: string,
distDir: string,
) {
const { config, pwd, serverConfigPath, builder } = options;
const dev = getDevOptions(options.dev);

const distDir = path.resolve(pwd, config.output.distPath?.root || 'dist');

const serverConfig = (await loadServerRuntimeConfig(serverConfigPath)) || {};

const prodServerOptions = {
return {
...options,
pwd: distDir, // server base pwd must distDir,
pwd: distDir,
serverConfig: {
...serverConfig,
...options.serverConfig,
},
/**
* 1. server plugins from modern.server.ts
* 2. server plugins register by cli use _internalServerPlugins
* Merge plugins, the plugins from modern.server.ts will run first
*/
plugins: [...(serverConfig.plugins || []), ...(options.plugins || [])],
};
}

export async function createDevServer(
options: ModernDevServerOptions,
applyPlugins: ApplyPlugins,
) {
const { config, pwd, serverConfigPath, builder } = options;
const dev = getDevOptions(options.dev);

const distDir = path.resolve(pwd, config.output.distPath?.root || 'dist');

const server = createServerBase(prodServerOptions);
const prodServerOptions = await createServerOptions(
options,
serverConfigPath,
distDir,
);

let currentServer = createServerBase(prodServerOptions);

let isReloading = false;

const devHttpsOption = typeof dev === 'object' && dev.https;
const isHttp2 = !!devHttpsOption;
let nodeServer;

let nodeServer: Awaited<ReturnType<typeof createNodeServer>>;
if (devHttpsOption) {
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 All @@ -57,7 +74,40 @@ export async function createDevServer(
compiler: options.compiler,
});

server.addPlugins([
const reload = async () => {
if (isReloading) {
return;
}
isReloading = true;

try {
await currentServer.close();

const updatedProdServerOptions = await createServerOptions(
options,
serverConfigPath,
distDir,
);
const newServer = createServerBase(updatedProdServerOptions);

await manager.close(ResourceType.Watcher);

newServer.addPlugins([serverHmrPlugin(), devPlugin(options, true)]);
await applyPlugins(newServer, updatedProdServerOptions);
await newServer.init();

currentServer = newServer;

logger.info(`Custom Web Server reload succeeded`);
} catch (e) {
logger.error('[Custom Web Server reload failed]:', e);
} finally {
isReloading = false;
}
};
serverReload = reload;
currentServer.addPlugins([
serverHmrPlugin(),
devPlugin({
...options,
builderDevServer,
Expand All @@ -70,9 +120,9 @@ export async function createDevServer(
prodServerOptions.config.output.assetPrefix = assetPrefix;
}

await applyPlugins(server, prodServerOptions, nodeServer);
await applyPlugins(currentServer, prodServerOptions, nodeServer);

await server.init();
await currentServer.init();

const afterListen = async () => {
await builderDevServer?.afterListen();
Expand Down
39 changes: 21 additions & 18 deletions packages/server/server/src/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
onRepack,
startWatcher,
} from './helpers';
import { ResourceManager, ResourceType } from './helpers/utils';
import type { ModernDevServerOptions } from './types';

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

export const devPlugin = (options: DevPluginOptions): ServerPlugin => ({
export const manager = new ResourceManager();

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

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

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

const dev = getDevOptions(options.dev);

api.onPrepare(async () => {
Expand All @@ -36,7 +40,9 @@ export const devPlugin = (options: DevPluginOptions): ServerPlugin => ({
connectWebSocket,
} = builderDevServer || {};

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

const {
middlewares,
Expand All @@ -53,14 +59,13 @@ export const devPlugin = (options: DevPluginOptions): ServerPlugin => ({
// TODO: remove any
const hooks = (api as any).getHooks();

// Handle webpack rebuild
builder?.onDevCompileDone(({ stats }) => {
if (stats.toJson({ all: false }).name !== 'server') {
onRepack(distDirectory!, hooks);
}
});
!isReload &&
builder?.onDevCompileDone(({ stats }) => {
if (stats.toJson({ all: false }).name !== 'server') {
onRepack(distDirectory!, hooks);
}
});

// Handle watch
const { watchOptions } = config.server;
const watcher = startWatcher({
pwd,
Expand All @@ -70,13 +75,11 @@ export const devPlugin = (options: DevPluginOptions): ServerPlugin => ({
watchOptions,
server: serverBase!,
});
closeCb.push(watcher.close.bind(watcher));
closeCb.length > 0 &&
nodeServer?.on('close', () => {
closeCb.forEach(cb => {
cb();
});
});
manager.register(ResourceType.Watcher, watcher.close.bind(watcher));

nodeServer?.on('close', () => {
manager.closeAll();
});

// Handle setupMiddlewares
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;
}
}
24 changes: 24 additions & 0 deletions packages/server/server/src/plugins/serverReload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import path from 'path';
import type { ServerPlugin } from '@modern-js/server-core';

import { serverReload } from '../createDevServer';

export default (): ServerPlugin => ({
name: '@modern-js/server-reload-plugin',
setup: api => {
api.onReset(async ({ event }) => {
if (event.type === 'file-change') {
const { appDirectory } = api.getServerContext();
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 serverReload?.();
}
}
});
},
});
Loading
Loading