import http , { Server , createServer , IncomingMessage , ServerResponse , } from 'http' ; import path from 'path' ; import { createServer as createHttpsServer } from 'https' ; import { API_DIR , HMR_SOCK_PATH , SERVER_DIR , SHARED_DIR , } from '@modern-js/utils' ; import type { MultiCompiler , Compiler } from 'webpack' ; import webpackDevMiddleware from 'webpack-dev-middleware' ; import { ModernServer } from '../modern-server' ; import { createMockHandler } from '../../dev-tools/mock' ; import { createProxyHandler , ProxyOptions } from '../../libs/proxy' ; import { DevServerOptions , ModernServerOptions , NextFunction , ServerHookRunner , ReadyOptions , } from '../../type' ; import SocketServer from '../../dev-tools/socket-server' ; import DevServerPlugin from '../../dev-tools/dev-server-plugin' ; import { ModernServerContext } from '../../libs/context' ; import { createLaunchEditorHandler } from '../../dev-tools/launch-editor' ; import { enableRegister } from '../../dev-tools/babel/register' ; import * as reader from '../../libs/render/reader' ; import Watcher from '../../dev-tools/watcher' ; import { AGGRED_DIR } from '../../constants' ; const DEFAULT_DEV_OPTIONS = { client : { port : '8080' , overlay : false , logging : 'none' , path : HMR_SOCK_PATH , host : 'localhost' , } , https : false , dev : { writeToDisk : true } , watch : true , hot : true , liveReload : true , } ; export class ModernDevServer extends ModernServer { private devProxyHandler : ReturnType < typeof createProxyHandler > = null ; private mockHandler : ReturnType < typeof createMockHandler > = null ; private readonly dev : DevServerOptions ; private readonly compiler ? : MultiCompiler | Compiler ; private socketServer ! : SocketServer ; private watcher ! : Watcher ; private devMiddleware ! : webpackDevMiddleware . API < http . IncomingMessage , http . ServerResponse > ; constructor ( options ) { super ( options ) ; this . compiler = options . compiler ! ; this . dev = typeof options . dev === 'boolean' ? DEFAULT_DEV_OPTIONS : { ... DEFAULT_DEV_OPTIONS , ... options . dev } ; enableRegister ( this . pwd , this . conf ) ; } public async init ( runner ) { const { conf , pwd , compiler } = this ; this . mockHandler = createMockHandler ( { pwd } ) ; this . addHandler ( ( ctx , next ) => { ctx . res . setHeader ( 'Access-Control-Allow-Origin' , '*' ) ; ctx . res . setHeader ( 'Access-Control-Allow-Credentials' , 'false' ) ; if ( this . mockHandler ) { this . mockHandler ( ctx , next ) ; } else { next ( ) ; } } ) ; this . devProxyHandler = createProxyHandler ( conf . tools ?. devServer ?. proxy as ProxyOptions ) ; if ( this . devProxyHandler ) { this . devProxyHandler . forEach ( handler => { this . addHandler ( handler ) ; } ) ; } if ( compiler ) { this . socketServer = new SocketServer ( this . dev ) ; this . addHandler ( createLaunchEditorHandler ( ) ) ; const devMiddlewareHandler = this . setupCompiler ( compiler ) ; this . addHandler ( devMiddlewareHandler ) ; } await super . init ( runner ) ; if ( this . dev . watch ) { this . startWatcher ( ) ; } } public ready ( options = { } ) { this . router . reset ( this . filterRoutes ( options . routes || this . presetRoutes || [ ] ) ) ; this . cleanSSRCache ( ) ; reader . updateFile ( ) ; this . runner . reset ( ) ; } public onListening ( app ) { this . socketServer ?. prepare ( app ) ; } public async close ( ) { super . close ( ) ; await this . watcher . close ( ) ; await new Promise < void > ( resolve => { this . devMiddleware . close ( ( ) => { resolve ( ) ; } ) ; } ) ; } public async createHTTPServer ( handler ) { const { dev } = this ; const devHttpsOption = typeof dev === 'object' && dev . https ; if ( devHttpsOption ) { const { genHttpsOptions } = require ( '../../dev-tools/https' ) ; const httpsOptions = await genHttpsOptions ( devHttpsOption ) ; return createHttpsServer ( httpsOptions , handler ) ; } else { return createServer ( handler ) ; } } private setupCompiler ( compiler ) { this . setupDevServerPlugin ( compiler ) ; this . setupHooks ( ) ; return this . setupDevMiddleware ( compiler ) ; } private setupDevServerPlugin ( compiler ) { const { dev : devConf } = this ; if ( ( compiler as MultiCompiler ) . compilers ) { ( compiler as MultiCompiler ) . compilers . forEach ( target => { if ( target . name === 'client' ) { new DevServerPlugin ( devConf ) . apply ( target ) ; } } ) ; } else { new DevServerPlugin ( devConf ) . apply ( compiler as Compiler ) ; } } private setupHooks ( ) { const invalidPlugin = ( ) => { this . socketServer . sockWrite ( 'invalid' ) ; } ; const addHooks = ( compiler ) => { const { compile , invalid , done } = compiler . hooks ; compile . tap ( 'modern-dev-server' , invalidPlugin ) ; invalid . tap ( 'modern-dev-server' , invalidPlugin ) ; done . tap ( 'modern-dev-server' , ( stats ) => { this . socketServer . updateStats ( stats ) ; if ( stats . toJson ( { all : false } ) . name === 'client' ) { this . ready ( { routes : this . readRouteSpec ( ) } ) ; } } ) ; } ; if ( ( this . compiler as MultiCompiler ) . compilers ) { ( this . compiler as MultiCompiler ) . compilers . forEach ( addHooks ) ; } else { addHooks ( this . compiler as Compiler ) ; } } private setupDevMiddleware ( compiler ) { this . devMiddleware = webpackDevMiddleware ( compiler , { publicPath : '/' , writeToDisk : this . dev . dev . writeToDisk , stats : false , } ) ; return ( ctx , next ) => { const { req , res } = ctx ; this . devMiddleware ( req , res , next ) ; } ; } private cleanSSRCache ( ) { const { distDir } = this ; const bundles = this . router . getBundles ( ) ; bundles . forEach ( bundle => { const filepath = path . join ( distDir , bundle as string ) ; if ( require . cache [ filepath ] ) { delete require . cache [ filepath ] ; } } ) ; } private startWatcher ( ) { const { pwd } = this ; const { mock } = AGGRED_DIR ; const defaultWatched = [ `${ pwd }/${ mock }/**/*` , `${ pwd }/${ SERVER_DIR }/**/*` , `${ pwd }/${ API_DIR }/!(typings)/**` , `${ pwd }/${ SHARED_DIR }/**/*` , ] ; const watcher = new Watcher ( ) ; watcher . createDepTree ( ) ; watcher . listen ( defaultWatched , ( filepath ) => { watcher . updateDepTree ( ) ; watcher . cleanDepCache ( filepath ) ; this . runner . reset ( ) ; if ( filepath . startsWith ( `${ pwd }/${ mock }` ) ) { this . mockHandler = createMockHandler ( { pwd } ) ; } else { try { this . prepareFrameHandler ( ) ; } catch ( e ) { this . logger . error ( e as Error ) ; } } } ) ; this . watcher = watcher ; } }