ember-cli/ember-cli

View on GitHub
lib/models/watcher.js

Summary

Maintainability
B
4 hrs
Test Coverage
B
84%
'use strict';

const chalk = require('chalk');
const logger = require('heimdalljs-logger')('ember-cli:watcher');
const CoreObject = require('core-object');
const serveURL = require('../utilities/get-serve-url');
const printSlowTrees = require('broccoli-slow-trees');

const eventTypeNormalization = {
  add: 'added',
  delete: 'deleted',
  change: 'changed',
};

const ConstructedFromBuilder = Symbol('Watcher.build');

module.exports = class Watcher extends CoreObject {
  constructor(_options, build) {
    if (build !== ConstructedFromBuilder) {
      throw new Error('instantiate Watcher with (await Watcher.build()).watcher, not new Watcher()');
    }

    super(_options);

    this.verbose = true;
    this.serving = _options.serving;
  }

  static async build(_options) {
    let watcher = new this(_options, ConstructedFromBuilder);
    await watcher.setupBroccoliWatcher(_options);

    // This indirection is because Watcher instances are themselves spec
    // noncompliant thennables (see the then() method) so returning watcher
    // directly will interfere with `await Watcher.build()`
    return { watcher };
  }

  async setupBroccoliWatcher() {
    let options = this.buildOptions();

    logger.info('initialize %o', options);
    this.watcher = this.watcher || (await this.constructBroccoliWatcher(options));

    this.setupBroccoliChangeEvent();
    this.watcher.on('buildStart', this._setupBroccoliWatcherBuild.bind(this));
    this.watcher.on('buildSuccess', this.didChange.bind(this));

    // build errors arriving via watcher
    this.watcher.on('buildFailure', this.didError.bind(this));
    // watcher specific errors
    this.watcher.on('error', this.didError.bind(this));

    this.serveURL = serveURL;
  }

  async constructBroccoliWatcher(options) {
    const { Watcher } = require('broccoli');
    await this.builder.ensureBroccoliBuilder();
    const { watchedSourceNodeWrappers } = this.builder.builder;

    let watcher = new Watcher(this.builder, watchedSourceNodeWrappers, { saneOptions: options, ignored: this.ignored });

    watcher.start();

    return watcher;
  }

  setupBroccoliChangeEvent() {
    // This is to keep backwards compatibility with broccoli-sane-watcher.
    // https://github.com/ember-cli/broccoli-sane-watcher/blob/48860/index.js#L158
    if (this.verbose) {
      this.watcher.on('change', (event, filePath) => {
        this.ui.writeLine(`file ${eventTypeNormalization[event]} ${filePath}`);
      });
    }
  }

  async _setupBroccoliWatcherBuild() {
    try {
      const hash = await this.watcher.currentBuild;
      hash.graph.__heimdall__.remove();
    } catch (e) {
      if (e !== null && typeof e === 'object' && e.isBuilderError === true) {
        // e must be a builder error which we can safely ignore.
        //
        // Builder errors are expected failures which are handled and presented
        // to the user else-where. To prevent double reporting faliures to the
        // user, it is important that we ignore them here as this functions
        // single responsibilty is to reset the himedall state after each build.
        //
        // exists to prune heimdall data after a build. Those errors are
        // reported elsewhere.
        //
      } else {
        // this error is unexpected, we must re-throw as we cannot be sure it
        // has been handled else-where
        throw e;
      }
    }
  }

  _totalTime(hash) {
    const sumNodes = (node, cb) => {
      let total = 0;
      node.visitPreOrder((node) => {
        total += cb(node);
      });
      return total;
    };

    return sumNodes(hash.graph.__heimdall__, (node) => node.stats.time.self);
  }

  didError(error) {
    logger.info('didError %o', error);
    this.ui.writeError(error);
  }

  didChange(results) {
    logger.info('didChange %o', results);

    results.totalTime = this._totalTime(results);
    let totalTime = results.totalTime / 1e6;
    let message = chalk.green(`Build successful (${Math.round(totalTime)}ms)`);

    this.ui.writeLine('');

    if (this.serving) {
      message += ` – Serving on ${this.serveURL(this.options, this.options.project)}`;
    }

    this.ui.writeLine(message);

    if (this.verbose) {
      printSlowTrees(results.graph.__heimdall__);
    }
  }

  buildOptions() {
    let watcher = this.options && this.options.watcher;

    if (watcher && ['polling', 'watchman', 'node', 'events'].indexOf(watcher) === -1) {
      throw new Error(`Unknown watcher type --watcher=[polling|watchman|node|events] but was: ${watcher}`);
    }

    return {
      verbose: this.verbose,
      poll: watcher === 'polling',
      watchman: watcher === 'watchman' || watcher === 'events',
      node: watcher === 'node',
    };
  }

  then() {
    return this.watcher.currentBuild.then.apply(this.watcher.currentBuild, arguments);
  }

  on() {
    const args = arguments;
    if (args[0] === 'change' && !this.watchedDir) {
      args[0] = 'buildSuccess';
    }
    this.watcher.on.apply(this.watcher, args);
  }

  off() {
    const args = arguments;
    if (args[0] === 'change' && !this.watchedDir) {
      args[0] = 'buildSuccess';
    }
    this.watcher.off.apply(this.watcher, args);
  }
};