server.js 8.89 KB
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;