node-devでrequireしたモジュールを監視できる仕組み

node-devがどういう仕組みでファイルを監視してるのか気になって調べたのでメモ。基本的にはfs.watchFileで監視するんだけど、ロードしてるモジュールも全部監視してるのが謎だった。

node.jsはrequireでモジュールをロードするときrequire.extensionsというのを見て、拡張子毎にコールバックが設定されていて、そのコールバックでモジュールを読み込んでるみたい。

node本体のソース見るとこんな感じ

Module.prototype.load = function(filename) {
  debug('load ' + JSON.stringify(filename) +
        ' for module ' + JSON.stringify(this.id));

  assert(!this.loaded);
  this.filename = filename;
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  Module._extensions[extension](this, filename);
  this.loaded = true;
};

...

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  var content = NativeModule.require('fs').readFileSync(filename, 'utf8');
  module._compile(content, filename);
};

// Native extension for .node
Module._extensions['.node'] = function(module, filename) {
  process.dlopen(filename, module.exports);
};

.jsのときは普通にロードして、.nodeのときはdlopenでアドオンモジュールとしてロードされる。なのでこのコールバックを上書きしてやればいい。

というわけでnode-devの実装はこうなってる。

/** Hook into `require()` */
function hookInto(ext) {
  var extensionHandler = require.extensions[ext];
  require.extensions[ext] = function(module, filename) {
    if (module.id == main) {
      module.id = '.';
      module.parent = null;
      process.mainModule = module;
    }
    watch(module);
    extensionHandler(module, filename);
    if (ext == '.js' && Path.basename(filename, ext) == 'coffee-script') {
      hookInto('.coffee');
    }
  };
}

hookInto('.js');

コールバックにフックしてwatch()で各モジュールファイルを監視する。で変更があったらリロードするという感じ。