watcher.js 3.18 KB
var chokidar = require('chokidar')
var mm = require('minimatch')
var expandBraces = require('expand-braces')

var helper = require('./helper')
var log = require('./logger').create('watcher')

var DIR_SEP = require('path').sep

// Get parent folder, that be watched (does not contain any special globbing character)
var baseDirFromPattern = function (pattern) {
  return pattern
    .replace(/[\/\\][^\/\\]*\*.*$/, '')              // remove parts with *
    .replace(/[\/\\][^\/\\]*[!\+]\(.*$/, '')         // remove parts with !(...) and +(...)
    .replace(/[\/\\][^\/\\]*\)\?.*$/, '') || DIR_SEP // remove parts with (...)?
}

var watchPatterns = function (patterns, watcher) {
  // filter only unique non url patterns paths
  var pathsToWatch = []
  var uniqueMap = {}
  var path

  // expand ['a/{b,c}'] to ['a/b', 'a/c']
  patterns = expandBraces(patterns)

  patterns.forEach(function (pattern) {
    path = baseDirFromPattern(pattern)
    if (!uniqueMap[path]) {
      uniqueMap[path] = true
      pathsToWatch.push(path)
    }
  })

  // watch only common parents, no sub paths
  pathsToWatch.forEach(function (path) {
    if (!pathsToWatch.some(function (p) {
      return p !== path && path.substr(0, p.length + 1) === p + DIR_SEP
    })) {
      watcher.add(path)
      log.debug('Watching "%s"', path)
    }
  })
}

// Function to test if a path should be ignored by chokidar.
var createIgnore = function (patterns, excludes) {
  return function (path, stat) {
    if (!stat || stat.isDirectory()) {
      return false
    }

    // Check if the path matches any of the watched patterns.
    if (!patterns.some(function (pattern) {
      return mm(path, pattern, {dot: true})
    })) {
      return true
    }

    // Check if the path matches any of the exclude patterns.
    if (excludes.some(function (pattern) {
      return mm(path, pattern, {dot: true})
    })) {
      return true
    }

    return false
  }
}

var onlyWatchedTrue = function (pattern) {
  return pattern.watched
}

var getWatchedPatterns = function (patternObjects) {
  return patternObjects.filter(onlyWatchedTrue).map(function (patternObject) {
    return patternObject.pattern
  })
}

exports.watch = function (patterns, excludes, fileList, usePolling, emitter) {
  var watchedPatterns = getWatchedPatterns(patterns)
  var options = {
    usePolling: usePolling,
    ignorePermissionErrors: true,
    ignoreInitial: true,
    ignored: createIgnore(watchedPatterns, excludes)
  }
  var chokidarWatcher = new chokidar.FSWatcher(options)

  watchPatterns(watchedPatterns, chokidarWatcher)

  var bind = function (fn) {
    return function (path) {
      return fn.call(fileList, helper.normalizeWinPath(path))
    }
  }

  // register events
  chokidarWatcher.on('add', bind(fileList.addFile))
    .on('change', bind(fileList.changeFile))
    .on('unlink', bind(fileList.removeFile))
    // If we don't subscribe; unhandled errors from Chokidar will bring Karma down
    // (see GH Issue #959)
    .on('error', function (e) {
      log.debug(e)
    })

  emitter.on('exit', function (done) {
    chokidarWatcher.close()
    done()
  })

  return chokidarWatcher
}

exports.watch.$inject = ['config.files', 'config.exclude', 'fileList', 'config.usePolling',
  'emitter']