Jump To …

watchn.js

Intelligently and continuously auto execute targets on file/directory changes. (c) 2011 Matthew Kitt

Module dependencies..

var fs = require('fs')
var path = require('path')
var Map = require('./map')

Public API

Instantiates a watcher

var Watchn = module.exports = function Watchn(silent) {
  this.watched = new Map()
  this.rules = new Map()
  this.silent = silent || false
}

noop function to quickly ignore a watchn.watch method in a runner without having to comment it out

Watchn.prototype.xwatch = function() {
}

Core method for watching files based on a set of rules with a callback function

Watchn.prototype.watch = function(rule, locs, fn) {
  if (!rule || !locs || !fn)
    throw new Error('watchn.watch requires a rule, locs (file/dir), and callback')

If a single file comes in, convert it to an array

  var items = (locs instanceof Array) ? locs : [locs]
  var watchables = this.collect(items, [])

Add each file to a map of watchables and execute the rule it's associated with

  for (var i = 0, len = watchables.length; i < len; i += 1) {
    var watchable = watchables[i]
    this.addToWatched(watchable)
    this.addToRules(rule, watchable, fn)
    this.notify('watchn: "' + watchable + '" - (' + rule + ')')
  }
}

Stop watchn stuff associated with either rules or location or both!

Watchn.prototype.unwatch = function(rule, locs) {
  var msg = ''

kill everything..

  if (!rule && !locs) {
    this.unwatchAll()
    msg = 'unwatchn: all locations and rules'

kill an entire rule..

  } else if (rule && !locs) {
    this.removeRule(rule)
    msg = 'unwatchn: rule - (' + rule + ')'

kill all locations regardless of rules..

  } else if (!rule && locs) {
    this.removeAllLocations(locs)
    msg = 'unwatchn: all rules for  "' + locs

kill locations associated with a rule..

  } else {
    this.removeLocationsFromRule(rule, locs)
    msg = 'unwatchn: "' + locs + ' for rule " - (' + rule + ')'
  }
  this.notify(msg)
}

Handle the change event and apply the callback (typically to a .watchn file)

Watchn.prototype.changed = function(options) {
  var self = this
  this.rules.each(function(key, value) {
    if (value.watched.hasValue(options.item)) {
      self.modified(options.item, key)
      value.fn(options)
    }
  })
}

Output information about what watchn is doing in the background

Watchn.prototype.notify = function(msg) {
  if (!this.silent)
    console.log(msg)
}

Convenience function to inspect what is being watched and their rules

Watchn.prototype.inspect = function() {
  var wts = this.watched.toString()
  var rts = this.rules.toString()
  return 'watched:\n' + wts + '\n\nrules:\n' + rts
}

Total and complete cleanup

Watchn.prototype.dispose = function() {
  this.unwatch()
}

Internal

Add to the queue of watched files (only add if already not being watched) and listen for changes

Watchn.prototype.addToWatched = function(item) {
  var self = this
  var isWatched = this.watched.hasValue(item)
  if (!isWatched) {
    fs.watchFile(item, {persistent: true, interval: 50}, function (curr, prev) {
      fs.stat(item, function (err, stats) {
        if (err) {
          self.removeLocation(item)
          self.removeFromWatched(item)
          self.notify('unwatchn: all rules for  "' + item)
        } else {
          self.changed({curr: curr.mtime, prev: prev.mtime, item: item, stats: stats})
        }
      })
    })
    this.watched.add(this.uid(item), item)
  }
  return isWatched
}

Add file(s) to an existing rule

Watchn.prototype.addToRules = function(rule, item, fn) {
  var key = this.uid(item)
  var isNew = true
  try {
    var map = new Map()
    map.add(key, item)
    this.rules.add(rule, {watched: map, fn: fn})
  } catch (err) {
    var watched = this.rules.get(rule).watched
    watched.add(key, item)
    isNew = false
  }
  return isNew
}

Remove a file from all rules from being watched

Watchn.prototype.removeFromWatched = function(item) {
  var key = this.uid(item)
  var self = this
  var stillWatched = false

  this.rules.each(function (key, value) {
    if (value.watched.hasKey(self.uid(item))) {
      stillWatched = true
      return;
    }
  })

  if (!stillWatched) {
    try {
      fs.unwatchFile(item)
      this.watched.remove(key)
    } catch (err) {}
  }
  return stillWatched
}

Kill all files from being watched

Watchn.prototype.unwatchAll = function() {
  var self = this
  this.rules.dispose()
  this.watched.each(function(key, value) {
    self.removeFromWatched(value)
  })
  this.watched.dispose()
}

Remove a rule and files from being watched, if files are contained in another rule they will continue to be watched

Watchn.prototype.removeRule = function(rule) {
  var r = this.rules.remove(rule)
  var unwatchables = r.watched.values
  for (var i = 0, len = unwatchables.length; i < len; i += 1) {
    this.removeFromWatched(unwatchables[i])
  }
  return unwatchables
}

Remove a single file from responding to a rule

Watchn.prototype.removeLocationFromRule = function(rule, item) {
  var watched = this.rules.get(rule).watched
  var removed = watched.remove(this.uid(item))
  this.removeFromWatched(item)
  return removed
}

Remove the locations from the rule

Watchn.prototype.removeLocationsFromRule = function(rule, locs) {
  var items = (locs instanceof Array) ? locs : [locs]
  var unwatchables = this.collect(items, [])
  for (var i = 0, len = unwatchables.length; i < len; i += 1) {
    this.removeLocationFromRule(rule, unwatchables[i])
  }
  return unwatchables
}

Remove the file from all rules

Watchn.prototype.removeLocation = function(item) {
  var self = this
  var count = 0
  this.rules.each(function(key, value) {
    if (value.watched.hasValue(item)) {
      self.removeLocationFromRule(key, item)
      count += 1;
    }
  })
  return count
}

Watchn.prototype.removeAllLocations = function(locs) {
  var items = (locs instanceof Array) ? locs : [locs]
  var unwatchables = this.collect(items, [])

  for (var i = 0, len = unwatchables.length; i < len; i += 1) {
    this.removeLocation(unwatchables[i])
  }
  return unwatchables
}

Check if a directory has been modified

Watchn.prototype.modified = function(dir, rule) {
  if (fs.statSync(dir).isDirectory()) {
    var r = this.rules.get(rule)
    var dirs = this.collectSubDirs(dir, [])
    var mod = this.difference(dirs, r.watched.values)
    if (mod.length > 0) {
      this.watch(rule, mod, r.fn)
    }
  }
}

Watchn.prototype.difference = function(a1, a2) {
  return a1.filter(function(i) {
    return (a2.indexOf(i) < 0);
  })
}

Collect files in a directory

Watchn.prototype.collect = function(items, watchables) {
  var self = this
  for (var i = 0, len = items.length; i < len; i += 1) {
    var item = path.normalize(items[i])
    if (fs.statSync(item).isFile()) {
      watchables.push(item)
    } else {
      var dirs = this.collectSubDirs(item, [])
      watchables = watchables.concat(dirs)
    }
  }
  return watchables
}

Collect files in sub directories

Watchn.prototype.collectSubDirs = function(dir, dirs) {
  var self = this
  if (fs.statSync(dir).isDirectory()) {
    dirs.push(dir)
    fs.readdirSync(dir).forEach(function (item) {
      self.collectSubDirs(path.normalize(path.join(dir, item)), dirs)
    })
  }
  return dirs
}

Give each file a unique identifier

Watchn.prototype.uid = function(str) {
  return str.replace(/[\/\.]/g, '_')
}

Helper Methods

For use in a .watchn file

Clean up surrounding white space

Watchn.prototype.trim = function(str) {
  return str.replace(/^\s+|\s+$/g,'')
}

Clean up any ansi characters

Watchn.prototype.trimANSI = function(str) {
  return str.replace(/\[\d+m/g, '')
}

Clean up new lines

Watchn.prototype.trimNewlines = function(str) {
  return str.replace(/\n/g, ' ')
}