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']