ember-cli/ember-cli

View on GitHub
lib/tasks/server/express-server.js

Summary

Maintainability
B
5 hrs
Test Coverage
A
94%
'use strict';

const path = require('path');
const EventEmitter = require('events').EventEmitter;
const chalk = require('chalk');
const fs = require('fs');
const { debounce } = require('ember-cli-lodash-subset');
const mapSeries = require('promise-map-series');
const Task = require('../../models/task');
const SilentError = require('silent-error');
const LiveReloadServer = require('./livereload-server');

class ExpressServerTask extends Task {
  constructor(options) {
    super(options);
    this.emitter = new EventEmitter();
    this.express = this.express || require('express');
    this.http = this.http || require('http');
    this.https = this.https || require('https');

    let serverRestartDelayTime = this.serverRestartDelayTime || 100;
    this.scheduleServerRestart = debounce(function () {
      this.restartHttpServer();
    }, serverRestartDelayTime);
  }

  on() {
    this.emitter.on.apply(this.emitter, arguments);
  }

  off() {
    this.emitter.off.apply(this.emitter, arguments);
  }

  emit() {
    this.emitter.emit.apply(this.emitter, arguments);
  }

  displayHost(specifiedHost) {
    return specifiedHost || 'localhost';
  }

  setupHttpServer() {
    if (this.startOptions.ssl) {
      this.httpServer = this.createHttpsServer();
    } else {
      this.httpServer = this.http.createServer(this.app);
    }

    // We have to keep track of sockets so that we can close them
    // when we need to restart.
    this.sockets = {};
    this.nextSocketId = 0;
    this.httpServer.on('connection', (socket) => {
      let socketId = this.nextSocketId++;
      this.sockets[socketId] = socket;

      socket.on('close', () => {
        delete this.sockets[socketId];
      });
    });
  }

  createHttpsServer() {
    if (!fs.existsSync(this.startOptions.sslKey)) {
      throw new TypeError(
        `SSL key couldn't be found in "${this.startOptions.sslKey}", ` +
          `please provide a path to an existing ssl key file with --ssl-key`
      );
    }

    if (!fs.existsSync(this.startOptions.sslCert)) {
      throw new TypeError(
        `SSL certificate couldn't be found in "${this.startOptions.sslCert}", ` +
          `please provide a path to an existing ssl certificate file with --ssl-cert`
      );
    }

    let options = {
      key: fs.readFileSync(this.startOptions.sslKey),
      cert: fs.readFileSync(this.startOptions.sslCert),
    };

    return this.https.createServer(options, this.app);
  }

  listen(port, host) {
    let server = this.httpServer;

    return new Promise((resolve, reject) => {
      server.listen(port, host);
      server.on('listening', () => {
        resolve();
        this.emit('listening');
      });
      server.on('error', reject);
    });
  }

  processAddonMiddlewares(options) {
    this.project.initializeAddons();

    return mapSeries(
      this.project.addons,
      function (addon) {
        if (addon.serverMiddleware) {
          return addon.serverMiddleware({
            app: this.app,
            options,
          });
        }
      },
      this
    );
  }

  processAppMiddlewares(options) {
    if (this.project.has(this.serverRoot)) {
      try {
        let server = this.project.require(this.serverRoot);

        if (typeof server !== 'function') {
          throw new TypeError('ember-cli expected ./server/index.js to be the entry for your mock or proxy server');
        }

        if (server.length === 3) {
          // express app is function of form req, res, next
          return this.app.use(server);
        }

        return server(this.app, options);
      } catch (e) {
        if (e.code !== 'MODULE_NOT_FOUND') {
          throw e;
        }
      }
    }
  }

  start(options) {
    options.project = this.project;
    options.watcher = this.watcher;
    options.serverWatcher = this.serverWatcher;
    options.ui = this.ui;

    this.startOptions = options;

    if (this.serverWatcher) {
      this.serverWatcher.on('change', this.serverWatcherDidChange.bind(this));
      this.serverWatcher.on('add', this.serverWatcherDidChange.bind(this));
      this.serverWatcher.on('delete', this.serverWatcherDidChange.bind(this));
    }

    return this.startHttpServer();
  }

  serverWatcherDidChange() {
    this.scheduleServerRestart();
  }

  restartHttpServer() {
    if (this.serverRestartPromise) {
      return this.serverRestartPromise.then(() => this.restartHttpServer());
    }

    this.serverRestartPromise = (async () => {
      try {
        await this.stopHttpServer();
        this.invalidateCache(this.serverRoot);
        await this.startHttpServer();

        this.emit('restart');
        this.ui.writeLine('');
        this.ui.writeLine(chalk.green('Server restarted.'));
        this.ui.writeLine('');
      } catch (err) {
        this.ui.writeError(err);
      } finally {
        this.serverRestartPromise = null;
      }
    })();

    return this.serverRestartPromise;
  }

  stopHttpServer() {
    return new Promise((resolve, reject) => {
      if (!this.httpServer) {
        return resolve();
      }
      this.httpServer.close((err) => {
        if (err) {
          reject(err);
          return;
        }
        this.httpServer = null;
        resolve();
      });

      // We have to force close all sockets in order to get a fast restart
      let sockets = this.sockets;
      for (let socketId in sockets) {
        sockets[socketId].destroy();
      }
    });
  }

  async startHttpServer() {
    this.app = this.express();
    const compression = require('compression');
    this.app.use(
      compression({
        filter(req, res) {
          let type = res.getHeader('Content-Type');

          if (res.getHeader('x-no-compression')) {
            // don't compress responses with this response header
            return false;
          } else if (type && type.indexOf('text/event-stream') > -1) {
            // don't attempt to compress server sent events
            return false;
          } else {
            return compression.filter(req, res);
          }
        },
      })
    );

    this.setupHttpServer();

    let options = this.startOptions;
    options.httpServer = this.httpServer;
    let liveReloadServer;

    if (options.path) {
      liveReloadServer = {
        setupMiddleware() {},
      };
    } else {
      liveReloadServer = new LiveReloadServer({
        app: this.app,
        ui: options.ui,
        watcher: options.watcher,
        project: options.project,
        httpServer: options.httpServer,
      });
    }

    const config = this.project.config(options.environment);
    const middlewareOptions = Object.assign({}, options, {
      rootURL: config.rootURL,
    });

    await liveReloadServer.setupMiddleware(this.startOptions);
    await this.processAppMiddlewares(middlewareOptions);
    await this.processAddonMiddlewares(middlewareOptions);

    return this.listen(options.port, options.host).catch(() => {
      throw new SilentError(
        `Could not serve on http://${this.displayHost(options.host)}:${options.port}. ` +
          `It is either in use or you do not have permission.`
      );
    });
  }

  invalidateCache(serverRoot) {
    let absoluteServerRoot = path.resolve(serverRoot);
    if (absoluteServerRoot[absoluteServerRoot.length - 1] !== path.sep) {
      absoluteServerRoot += path.sep;
    }

    let allKeys = Object.keys(require.cache);
    for (let i = 0; i < allKeys.length; i++) {
      if (allKeys[i].indexOf(absoluteServerRoot) === 0) {
        delete require.cache[allKeys[i]];
      }
    }
  }
}

module.exports = ExpressServerTask;