cache.js

'use strict';
/**
 * Cache 构造器,在 fis 中主要用于缓存目标文件的编译信息和编译结果。
 *
 * @param {String} path 需要被缓存的文件路径。
 * @param {String} dir 缓存目录。
 * @inner
 * @class
 * @memberOf fis.cache
 */
var Cache = Object.derive(function(path, dir) {
  var file = fis.util.realpath(path);
  if (!fis.util.isFile(file)) {
    fis.log.error('unable to cache file[%s]: No such file.', path);
  }
  this.timestamp = fis.util.mtime(file).getTime();
  this.deps = {};
  this.mDeps = {}; // 记录那些存在缓存依赖,但是项目中没找到的文件。
  this.version = fis.version;

  var info = fis.util.pathinfo(file);
  var basename = fis.project.getCachePath(dir, info.basename);
  var hash = fis.util.md5(file, 10);
  this.cacheFile = basename + '-c-' + hash + '.tmp';
  this.cacheInfo = basename + '-o-' + hash + '.json';
},

  /** @lends fis.cache~Cache.prototype */
  {

  /**
   * 保存内容以及信息。
   * @param  {String | Buffer} content 文件内容
   * @param  {Object} info 数据信息。
   */
  save: function(content, info) {
    var infos = {
      version: this.version,
      timestamp: this.timestamp,
      deps: this.deps,
      mDeps: this.mDeps,
      info: info
    };
    fis.util.write(this.cacheInfo, JSON.stringify(infos));
    fis.util.write(this.cacheFile, content);
  },

  /**
   * 从缓存目录中读取缓存内容。
   * @param  {Object} [file] 如果传入了此参数,从缓存文件中读取的内容将会赋值到 `file.content` 以及数据信息会被赋值到 `file.info`.
   * @return {Boolean} 返回 true, 如果缓存有效,否则返回 false.
   */
  revert: function(file) {
    fis.log.debug('revert cache');
    if (
      exports.enable && fis.util.exists(this.cacheInfo) && fis.util.exists(this.cacheFile)
    ) {
      fis.log.debug('cache file exists');
      var infos = fis.util.readJSON(this.cacheInfo);
      fis.log.debug('cache info read');
      if (infos.version == this.version && infos.timestamp == this.timestamp) {
        var deps = infos['deps'];
        var allValid = Object.keys(deps).every(function(f) {
          var d = fis.util.mtime(f);
          return d != 0 && deps[f] == d.getTime()
        });

        if (!allValid) {
          fis.log.debug('cache is expired');
          return false;
        }
        this.deps = deps;
        this.mDeps = infos.mDeps;
        fis.log.debug('cache is valid');
        if (file) {
          file.info = infos.info;
          file.content = fis.util.fs.readFileSync(this.cacheFile);
        }
        fis.log.debug('revert cache finished');
        return true;
      }
    }
    fis.log.debug('cache is expired');
    return false;
  },

  /**
   * 添加依赖,依赖将会被用来判断缓存是否有效,依赖中,任何一个文件修改时间发生变化,缓存失效。
   * @param {String} filepath 依赖的文件路径。
   */
  addDeps: function(file) {
    var path = fis.util.realpath(file);
    if (path) {
      this.deps[path] = fis.util.mtime(path).getTime();
    } else {
      fis.log.warning('unable to add dependency file[' + file + ']: No such file.');
    }
    return this;
  },

  /**
   * 删除依赖。
   * @param {String} filepath 依赖的文件路径。
   */
  removeDeps: function(file) {
    var path = fis.util.realpath(file);
    if (path && this.deps[path]) {
      delete this.deps[path];
    }
    return this;
  },

  /**
   * 添加没找到的文件依赖,因为后续可能会添加进来。
   * @param {String} filepath 依赖的文件路径。
   */
  addMissingDeps: function(filepath, raw) {
    raw = raw || '';
    this.mDeps[filepath] = raw;
    return this;
  },

  /**
   * 合并 cache 中的依赖列表。
   * @param {mixed} cache 此对象中的依赖会被合入到该实例依赖中。
   */
  mergeDeps: function(cache) {
    var deps = {};
    var mDeps;

    if (cache instanceof Cache) {
      deps = cache.deps;
      mDeps = cache.mDeps;
    } else if (typeof cache === 'object') {
      deps = cache;
    } else {
      fis.log.error('unable to merge deps of data[%s]', cache);
    }

    fis.util.map(deps, this.deps, true);
    mDeps && fis.util.map(mDeps, this.mDeps, true);
  }
});

/**
 * 用来创建 ~Cache 对象, 更多细节请查看 {@link fis.cache~Cache ~Cache} 说明。
 *
 * @example
 * // 检查该文件是否启用缓存
 * if (file.useCache) {
 *
 *   // 如果启用,则创建 cache 对象
 *   file.cache = fis.cache(file.realpath, CACHE_DIR);
 *
 *   // 保存内容以及信息到缓存中。
 *   file.cache.save('body', {
 *     foo: 1
 *   });
 * }
 * @see {@link fis.cache~Cache Cache 类说明}
 * @param {String} path 需要被缓存的文件路径。
 * @param {String} dir 缓存目录。
 * @function
 * @namespace fis.cache
 */
var exports = module.exports = Cache.factory();

/**
 * 是否开启缓存。当设置为 false 后, fis 编译将不会启用缓存。
 * @type {Boolean}
 * @defaultValue true
 * @memberOf fis.cache
 * @name enable
 */
exports.enable = true;

/**
 * 指向 {@link fis.cache~Cache ~Cache 类}。
 * @see {@link fis.cache~Cache Cache 类说明}
 * @memberOf fis.cache
 * @name Cache
 */
exports.Cache = Cache;

/**
 * 清除缓存目录下面指定的目录。
 * @example
 *
 * // 当命令行中设置了 --clean 属性后,开始之前,先清除之前编译产生的缓存文件。
 * if (options.clean) {
 *   fis.cache.clean('compile');
 * }
 * @param  {String} name 需要清除的目录名
 * @memberOf fis.cache
 * @name clean
 * @function
 */
exports.clean = function(name) {
  name = name || '';
  var path = fis.project.getCachePath(name);
  if (fis.util.exists(path)) {
    fis.util.del(path);
  }
};