const { lodash, portfinder, createDebug } = require('@umijs/utils'); const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const spdy = require('spdy'); const compress = require('compression'); const url = require('url'); const http = require('http'); // const https = require('https'); const sockjs = require('sockjs'); const debug = createDebug('umi:server:Server'); const getCredentials = (options) => {}; const defaultOpts = { afterMiddlewares: [], beforeMiddlewares: [], compilerMiddleware: null, compress: true, https: process.env.HTTP2 ? true : !!process.env.HTTPS, onListening: argv => argv, onConnection: () => {}, onConnectionClose: () => {}, proxy: null, headers: {}, host: 'localhost', port: 8000, writeToDisk: false, }; class Server { constructor(opts) { this.opts = { ...defaultOpts, ...lodash.omitBy(opts, lodash.isUndefined), }; this.app = express(); this.setupFeatures(); this.createServer(); (this.socketProxies || []).forEach(wsProxy => { this.listeningApp.on('upgrade', wsProxy.upgrade); }, this); } getHttpsOptions() { if (this.opts.https) { const credential = getCredentials(this.opts); if (typeof this.opts.https === 'object' && this.opts.https.spdy) { console.warn( 'Providing custom spdy server options is deprecated and will be removed in the next major version.', ); return credential; } return { spdy: { protocols: ['h2', 'http/1.1'], }, ...credential, }; } } setupFeatures() { const features = { compress: () => { if (this.opts.compress) { this.setupCompress(); } }, headers: () => { if (lodash.isPlainObject(this.opts.headers)) { this.setupHeaders(); } }, beforeMiddlewares: () => { this.opts.beforeMiddlewares.forEach(middleware => { this.app.use(middleware); }); }, proxy: () => { if (this.opts.proxy) { this.setupProxy(); } }, compilerMiddleware: () => { if (this.opts.compilerMiddleware) { this.app.use(this.opts.compilerMiddleware); } }, afterMiddlewares: () => { this.opts.afterMiddlewares.forEach(middleware => { this.app.use(middleware); }); }, }; Object.keys(features).forEach(stage => { features[stage](); }); } setupHeaders() { this.app.all('*', (req, res, next) => { res.set(this.opts.headers); next(); }); } setupCompress() { const compressOpts = lodash.isBoolean(this.opts.compress) ? {} : this.opts.compress; this.app.use(compress(compressOpts)); } deleteRoutes() { let startIndex = null; let endIndex = null; // eslint-disable-next-line no-underscore-dangle this.app._router.stack.forEach((item, index) => { if (item.name === 'PROXY_START') startIndex = index; if (item.name === 'PROXY_END') endIndex = index; }); debug( `routes before changed: ${this.app._router.stack .map(item => item.name || 'undefined name') .join(', ')}`, ); if (startIndex !== null && endIndex !== null) { // eslint-disable-next-line no-underscore-dangle this.app._router.stack.splice(startIndex, endIndex - startIndex + 1); } debug( `routes after changed: ${this.app._router.stack .map(item => item.name || 'undefined name') .join(', ')}`, ); } setupProxy(proxyOpts, isWatch = false) { let proxy = proxyOpts || this.opts.proxy; if (!Array.isArray(proxy)) { if (proxy && 'target' in proxy) { proxy = [proxy]; } else { proxy = Object.keys(proxy || {}).map(context => { let proxyOptions; const correctedContext = context .replace(/^\*$/, '**') .replace(/\/\*$/, ''); if (typeof (proxy && proxy.context) === 'string') { proxyOptions = { context: correctedContext, target: proxy[context], }; } else { proxyOptions = { ...((proxy && proxy[context]) || {}), context: correctedContext, }; } proxyOptions.logLevel = proxyOptions.logLevel || 'warn'; return proxyOptions; }); } } const getProxyMiddleware = proxyConfig => { const context = proxyConfig.context || proxyConfig.path; if (proxyConfig.target) { return createProxyMiddleware(context, { ...proxyConfig, onProxyRes(proxyRes, req, res) { const target = typeof proxyConfig.target === 'object' ? url.format(proxyConfig.target) : proxyConfig.target; const realUrl = new URL(req.url || '', target).href || ''; proxyRes.headers['x-real-url'] = realUrl; proxyConfig.onProxyRes && proxyConfig.onProxyRes(proxyRes, req, res); }, }); } return; }; let startIndex = null; let endIndex = null; let routesLength = null; if (isWatch) { this.app._router.stack.forEach((item, index) => { if (item.name === 'PROXY_START') startIndex = index; if (item.name === 'PROXY_END') endIndex = index; }); if (startIndex !== null && endIndex !== null) { this.app._router.stack.splice(startIndex, endIndex - startIndex + 1); } routesLength = this.app._router.stack.length; this.deleteRoutes(); } this.app.use(function PROXY_START(req, res, next) { next(); }); proxy.forEach(proxyConfigOrCallback => { let proxyMiddleware; let proxyConfig = typeof proxyConfigOrCallback === 'function' ? proxyConfigOrCallback() : proxyConfigOrCallback; proxyMiddleware = getProxyMiddleware(proxyConfig); if (proxyConfig.ws && proxyMiddleware) { this.socketProxies.push(proxyMiddleware); } this.app.use((req, res, next) => { if (typeof proxyConfigOrCallback === 'function') { const newProxyConfig = proxyConfigOrCallback(); if (newProxyConfig !== proxyConfig) { proxyConfig = newProxyConfig; proxyMiddleware = getProxyMiddleware(proxyConfig); } } const bypassUrl = lodash.isFunction(proxyConfig.bypass) ? proxyConfig.bypass(req, res, proxyConfig) : null; if (typeof bypassUrl === 'boolean') { // skip the proxy req.url = null; next(); } else if (typeof bypassUrl === 'string') { // byPass to that url req.url = bypassUrl; next(); } else if (proxyMiddleware) { return proxyMiddleware(req, res, next); } else { next(); } }); }); this.app.use(function PROXY_END(req, res, next) { next(); }); // log proxy middleware after if (isWatch) { const newRoutes = this.app._router.stack.splice( routesLength, this.app._router.stack.length - routesLength, ); this.app._router.stack.splice(startIndex, 0, ...newRoutes); } } sockWrite({ sockets = this.sockets, type, data }) { sockets.forEach(socket => { socket.write(JSON.stringify({ type, data })); }); } createServer() { const httpsOpts = this.getHttpsOptions(); if (httpsOpts) { this.listeningApp = spdy.listeningApp(httpsOpts, this.app); } else { this.listeningApp = http.createServer(this.app); } } async listen({ port = 8000, hostname }) { const foundPort = await portfinder.getPortPromise({ port }); return new Promise(resolve => { this.listeningApp.listen(foundPort, hostname, 5, () => { this.createSocketServer(); const ret = { port: foundPort, hostname, listeningApp: this.listeningApp, server: this, }; this.opts.onListening(ret); resolve(ret); }); }).catch(error => { console.error(error); }); } createSocketServer() { const server = sockjs.createServer({ log: (severity, line) => { if (line.includes('bound to')) return; }, }); server.installHandlers(this.listeningApp, { prefix: '/dev-server', }); server.on('connection', connection => { if (!connection) { return; } this.opts.onConnection({ connection, server: this, }); this.sockets.push(connection); connection.on('close', () => { this.opts.onConnectionClose({ connection, }); const index = this.sockets.indexOf(connection); if (index >= 0) { this.sockets.splice(index, 1); } }); }); this.socketServer = server; } } module.exports = Server;