import * as webpack from "webpack"; import * as chalk from "chalk"; import { devEntryCache, HOME_DIR } from "./CacheData"; import { EventEmitter } from "events"; import * as webpackDevMiddleware from "webpack-dev-middleware"; import * as express from "express"; import * as ezmok from "ezmok"; import * as serveStatic from "serve-static"; import { addWebpackEntry } from "./utils/webpack"; import { printWarn } from "./utils"; interface BuiltEntry { [index: string]: { status: symbol; }; } const host = "0.0.0.0"; const ADDED = Symbol("added"); const BUILDING = Symbol("building"); const BUILT = Symbol("built"); class Invalidator { private devMiddleware: any; private building: boolean; private rebuildAgain: boolean; constructor(devMiddleware) { this.devMiddleware = devMiddleware; this.building = false; this.rebuildAgain = false; } invalidate() { if (this.building) { this.rebuildAgain = true; return; } this.building = true; this.devMiddleware.invalidate(); } startBuilding() { this.building = true; } doneBuilding() { this.building = false; if (this.rebuildAgain) { this.rebuildAgain = false; this.invalidate(); } } } function getRandomKey(keyList: string[]): string { const randomIndex = Math.floor(Math.random() * keyList.length); return keyList[randomIndex]; } export interface ServerStaticPath { prefix: string; path: string; } export interface DevServerOption { /** * 开发服务器的端口 */ port: number; /** * 开发模式下默认的接口请求地址 */ defaultProxyOrigin: ezmok.OriginSetting; /** * 接口定义文件根目录路径 */ apiDefPath: string; /** * 开发模式初始化编译的 entry */ initEntry?: string[]; /** * 开发服务器的静态文件目录 */ contentBase?: string | string[] | ServerStaticPath | ServerStaticPath[]; /** * 静态目录相关配置 */ staticOptions?: serveStatic.ServeStaticOptions; } export default function startServer( webpackConfig: webpack.Configuration, serverConfig: DevServerOption ) { const originEntries = webpackConfig.entry as any; const initEntry = {}; const builtEntries: BuiltEntry = {}; function addToInitEntry(entryKey: string) { if (originEntries[entryKey] && !initEntry[entryKey]) { initEntry[entryKey] = originEntries[entryKey]; } } if (serverConfig.initEntry) { serverConfig.initEntry.forEach(addToInitEntry); } const cacheEntry = devEntryCache.getEntry(); if (cacheEntry.length > 0) { cacheEntry.forEach(addToInitEntry); } // if no entry, generate random one if (Object.keys(initEntry).length === 0) { const k = getRandomKey(Object.keys(originEntries)); addToInitEntry(k); } webpackConfig.entry = initEntry; Object.keys(initEntry).forEach(k => { builtEntries[k] = { status: BUILDING }; }); const compiler: any = webpack(webpackConfig); const devMiddleware = webpackDevMiddleware(compiler, { quiet: true, headers: { "Access-Control-Allow-Origin": "*" } }); const app = express(); const doneCallbacks = new EventEmitter(); const invalidator = new Invalidator(devMiddleware); compiler.plugin("make", function addEntry(compilation, done) { invalidator.startBuilding(); const allEntries = Object.keys(builtEntries).map(page => { const entry = originEntries[page]; builtEntries[page].status = BUILDING; devEntryCache.addEntry(page); return addWebpackEntry(compilation, this.context, page, entry); }); Promise.all(allEntries) .then(() => done()) .catch(done); }); compiler.plugin("done", function emitWebpackDone() { Object.keys(builtEntries).forEach(page => { const entryInfo = builtEntries[page]; if (entryInfo.status !== BUILDING) return; entryInfo.status = BUILT; doneCallbacks.emit(page); invalidator.doneBuilding(); }); }); // init express middleware app.use( ezmok({ defaultApiOrigin: serverConfig.defaultProxyOrigin, defPath: serverConfig.apiDefPath, mockDirectory: HOME_DIR }) ); app.use(function ensurePage(req, res, next) { const requestPath = req.path === "/" ? "/index.html" : req.path; const url: string[] = requestPath.split("."); if (url[1] === "html") { const entryName = url[0].substr(1); if (!originEntries[entryName]) { next(); return; } const entryInfo = builtEntries[entryName]; if (entryInfo) { if (entryInfo.status === BUILT) { next(); } else if (entryInfo.status === BUILDING) { doneCallbacks.on(entryName, next); } return; } printWarn(`> Building page: ${entryName}`); builtEntries[entryName] = { status: ADDED }; doneCallbacks.on(entryName, next); invalidator.invalidate(); } else { next(); } }); app.use(devMiddleware); app.use( require("webpack-hot-middleware")(compiler, { log: () => {} }) ); // config static folder const { contentBase, staticOptions, port } = serverConfig; if (Array.isArray(contentBase)) { (contentBase as any[]).forEach(root => { if (typeof root === "string") { app.use(express.static(root, staticOptions)); } else { app.use(root.prefix, express.static(root.path, staticOptions)); } }); } else if (contentBase) { if (typeof contentBase === "string") { app.use(express.static(contentBase, staticOptions)); } else { app.use(contentBase.prefix, express.static(contentBase.path, staticOptions)); } } app.locals.env = process.env.NODE_ENV; devMiddleware.waitUntilValid(() => { const host = ` http://localhost:${port}`; console.log(chalk.yellow(`Available entry:`)); console.log(chalk.yellow(`${host}/ezmok`)); Object.keys(webpackConfig.entry as object).forEach(k => { console.log(chalk.yellow(`${host}/${k}.html`)); }); console.log(); }); console.log("> Starting dev server..."); app.listen(port, host); }