UNPKG

fis3

Version:
723 lines (642 loc) 19.9 kB
/** * 文件操作模块 */ 'use strict'; var _ = require('./util.js'); var path = require('path'); var fs = require('fs'); var matchCache = {}; /* * 编译文件后缀处理 * @deprecated 直接在文件属性中设置就好,不要通过 project.ext 来配置了。 * @param {String} ext 后缀 * @return {String} release时使用的后缀 */ function getReleaseExt(ext) { if (ext) { var rExt = fis.media().get('project.ext' + ext); if (rExt) { ext = normalizeExt(rExt); } } return ext; } /* * 后缀规范函数,保证后缀串第一个字符必为. * @param {String} ext 后缀串 * @return {String} 若第一个没有.则加上,若有.则去掉 */ function normalizeExt(ext) { if (ext[0] !== '.') { ext = '.' + ext; } return ext; } /* * 路径规范函数,路径中出现:*?"<>|均被替换为_,支持去掉自定义字符的正则处理 * @param {String} path 路径 * @param {RegExp} reg 正则 * @param {String} rExt 后缀 * @return {String} */ function normalizePath(path, reg, rExt) { return path .replace(reg, '') .replace(/[:*?"<>|]/g, '_') + rExt; } /* * 给文件添加md5标识符 * @param {String} path 文件路径 * @param {Object} file fis File对象 * @return {String} 带有md5 hash标识的路径 */ function addHash(path, file) { var rExt = file.rExt, qRExt = fis.util.escapeReg(rExt), qExt = fis.util.escapeReg(file.ext), hash = file.getHash(), onnector = fis.media().get('project.md5Connector', '_'), reg = new RegExp(qRExt + '$|' + qExt + '$', 'i'); return path.replace(reg, '') + onnector + hash + rExt; } /* * 获取路径对应下的全部domain * @param {String} path 路径 * @return {Array} 符合的全部domain值 */ function getDomainsByPath(path) { var domain = fis.media().get('project.domain', {}), value = []; if (typeof domain === 'string') { value = domain.split(/\s*,\s*/); } else if (fis.util.is(domain, 'Array')) { value = domain; } else { fis.util.map(domain, function(pattern, domain) { if ((pattern === 'image' && fis.util.isImageFile(path)) || fis.util.glob(pattern, path)) { if (typeof domain === 'string') { value = domain.split(/\s*,\s*/); } else if (fis.util.is(domain, 'Array')) { value = domain; } else { fis.log.warning('invalid domain [' + domain + '] of [project.domain.' + pattern + ']'); } return true; } }); } return value; } function getDomain(path, domains) { domains = domains || getDomainsByPath(path); var hash = fis.util.md5(path), len = domains.length, domain = ''; if (len) { domain = domains[hash.charCodeAt(0) % len]; } return domain; } /** * 大小写敏感的文件路径判断,fs 没有提供更直接的 API,只能通过 list 接口来判断。 */ function caseSensiveFileExits(filepath) { var dir = path.dirname(filepath); var basename = path.basename(filepath); var filenames = fs.readdirSync(dir); if (~filenames.indexOf(basename)) { return true; } return false; } var slice = [].slice; /** * File,fis 编译过程中,文件会被此类进行封装,对于文件的操作,都是通过此类来完成。 * * @property {String} ext 文件名后缀。 * @property {String} realpath 文件物理地址。 * @property {String} realpathNoExt 文件物理地址,没有后缀。 * @property {String} subpath 文件基于项目 root 的绝对路径。 * @property {String} subpathNoExt 文件基于项目 root 的绝对路径,没有后缀。 * @property {Boolean} useCompile 标记是否需要编译。 * @property {Boolean} useDomain 标记是否使用带domain的地址。 * @property {Boolean} useCache 编译过程中是否采用缓存。 * @property {Boolean} useMap 编译后是否将资源信息写入 map.json 表。 * @property {String} domain 文件的 domain 信息,在 useDomain 为 true 时会被作用在链接上。 * @property {String} release 文件的发布路径,当值为 false 时,文件不会发布。 * @property {String} url 文件访问路径。 * @property {String} id 文件 id 属性,默认为文件在项目中的绝对路径,不建议修改。 * @property {Array} requires 用来记录文件依赖。 * @property {Array} asyncs 用来记录异步依赖。 * @property {Array} links 用来记录此文件用到了哪些文件。 * @property {Array} derived 用来存放派生的文件,比如 sourcemap 文件。 * @property {Object} extras 用来存放一些附属信息,注意:此属性将会添加到 map.json 里面。 * @property {Boolean} isHtmlLike 标记此文件是否为 html 性质的文件。 * @property {Boolean} isCssLike 标记此文件是否为 css 性质的文件。 * @property {Boolean} isJsLike 标记此文件是否为 javascript 性质的文件。 * @property {Boolean} isJsonLike 标记此文件是否为 json 性质的文件。 * * * * @class * @inner * @param {String} path... 文件路径,可以作为多个参数输入,多个参数会被 `/` 串联起来。 * @param {Object} [props] 可以默认给文件添加一些属性 * @memberOf fis.file */ var File = Object.derive(function() { // 构造函数 var args = slice.call(arguments); var props = args.pop(); if (!_.isPlainObject(props)) { args.push(props); props = null; } var self = _.assign(this, _.pathinfo(args)); props && _.assign(this, props); var ext = self.ext; // var realpath = this.realpath = _.realpathSafe(self.fullname); var realpath = this.realpath = _.isAbsolute(self.fullname) && _.exists(self.fullname) ? _(self.fullname) : _.realpathSafe(self.fullname); var realpathNoExt = this.realpathNoExt = self.rest; var root = fis.project.getProjectPath(); var properties = {}; if (realpath.indexOf(root) === 0) { var len = root.length; var subpath; this.subpath = subpath = realpath.substring(len); this.subdirname = self.dirname.substring(len); this.subpathNoExt = realpathNoExt.substring(len); // cache apply matches. var matches = fis.media().getSortedMatches(); var mcache = matchCache._owner === matches ? matchCache : (matchCache = {_owner: matches}); var mpath = subpath + (this.xLang || ''); if (mcache[mpath]) { properties = _.assign({}, mcache[mpath]); } else { properties = _.applyMatches(mpath, matches); mcache[mpath] = _.assign({}, properties); } } var rExt = this.rExt = properties.rExt ? normalizeExt(properties.rExt) : getReleaseExt(ext); this.useCompile = true; // this.useDomain = false; this.useCache = true; this.useHash = false; this.useMap = false; this.domain = ''; this.requires = []; this.links = []; this.asyncs = []; this.derived = []; this.extras = {}; this._isImage = true; this._isText = false; this._likes = {}; this.defineLikes(); if (_.isTextFile(ext)) { this._isImage = false; this._isText = true; this.charset = null; switch (rExt) { case '.js': case '.es6': case '.jsx': case '.coffee': this.isJsLike = true; // this.useDomain = true; // this.useHash = true; this.useMap = true; break; case '.css': case '.less': case '.sass': case '.styl': case '.scss': this.isCssLike = true; // this.useDomain = true; // this.useHash = true; this.useMap = true; break; case '.html': case '.xhtml': case '.shtml': case '.htm': case '.tpl': //smarty template case '.ftl': //freemarker template case '.vm': //velocity template case '.php': case '.phtml': case '.jsp': case '.asp': case '.aspx': case '.ascx': case '.cshtml': case '.master': this.isHtmlLike = true; break; case '.json': this.isJsonLike = true; break; } } else if (_.isImageFile(ext)) { // this.useDomain = true; // this.useHash = rExt !== '.ico'; } else { this.useCompile = false; } // 将应用上的属性,复制到文件对象。 delete properties.rExt; _.assign(this, properties); if (subpath) { if (this.release === false && !this.url) { this.useMap = false; var self = this; Object.defineProperty(this, 'url', { enumerable: true, get: function() { fis.log.error('unreleasable file [%s]', self.realpath); } }); } else { this.useMap = this.isMod ? true : this.useMap; //release & url var reg = new RegExp(_.escapeReg(ext) + '$|' + _.escapeReg(rExt) + '$', 'i'); var release; if (this.release) { release = this.release === true ? this.subpath : this.release.replace(/[\/\\]+/g, '/'); if (release[0] !== '/') { release = '/' + release; } } else { // 在没有设置的情况下,内嵌的资源就不 release 了。 release = this.isInline || this.release === false ? false : this.subpath; } this.release = release ? normalizePath(release, reg, rExt) : release; this.url = this.url ? normalizePath(this.url, reg, rExt) : this.release; } // charset if (this._isText) { this.charset = ( this.charset || fis.media().get('project.charset', 'utf8') ).toLowerCase(); } //file id var id = this.id || subpath.replace(/^\//, ''); var ns = fis.media().get('namespace'); if (ns) { id = ns + fis.media().get('namespaceConnector', ':') + id; } this.id = id; this.moduleId = this.moduleId || this.id.replace(/\.js$/i, ''); } }, /** @lends fis.file~File.prototype */{ /** * 定义默认的文件类型,如:isHtmlLike、isJsLike、isCssLike。他们是互斥的,当设置其中某个值为 true 时, * 其他属性应该为 false. * * 提取到一个方法里面原因是:isXXXLike 的属性是通过 `Object.defineProperties` * 定义的,当 file 对象从文件 cache 里面反序列回来后,isXxxLike 系列的属性都会丢失。 所以这块需要重新调用(定义)一次。 * @function * @return {Undefined} */ defineLikes: function() { if (this.hasOwnProperty('isHtmlLike')) { return; } var likes = ['isHtmlLike', 'isJsLike', 'isCssLike']; var props = {}; likes.forEach(function(v) { props[v] = { set: function(val) { if (val === false) { this._likes[v] = false; return; } var that = this; likes.forEach(function(v2) { if (v === v2) { that._likes[v2] = true } else { that._likes[v2] = false; } }); }, get: function() { return this._likes[v]; } }; }); Object.defineProperties(this, props); }, /** * 返回文件是否真实存在。 * @function * @return {Boolean} */ exists: function() { return fis.util.exists(this.realpath); }, /** * 返回文件是否为文本文件。 * @return {Boolean} */ isText: function() { return this._isText; }, /** * 返回文件是否为图片文件。 * @return {Boolean} */ isImage: function() { return this._isImage; }, /** * 返回真实的物理路径 * @return {String} 路径 */ toString: function() { return this.realpath; }, /* * 获取文件最近修改时间,注意这里是文件的修改时间,而不是此类内容的修改时间。 * @return {Date} 最近修改时间 */ getMtime: function() { return fis.util.mtime(this.realpath) || new Date(); }, /** * 判断文件路径是否为文件 * @return {Boolean} */ isFile: function() { return fis.util.isFile(this.realpath); }, /** * 判断文件路径是否为文件夹 * @return {Boolean} */ isDir: function() { return fis.util.isDir(this.realpath); }, /** * 设置文件内容,可以是字符串或者 Buffer 对象。 * @param {String | Buffer} c 文件内容 * @return {Object} 返回自身,方便链式调用 */ setContent: function(c) { this._content = c; return this; }, /** * 获取文件内容 * @return {String| Buffer} 文件内容 */ getContent: function() { if (typeof this._content === 'undefined') { this._content = fis.util.read(this.realpath, this.isText()); } return this._content; }, /** * 获取文件内容的md5序列,多次调用,尽管文件内容有变化,也只会返回第一次调用时根据当时文件内容计算出来的结果。 * @return {String} 文件内容md5序列后的结果 */ getHash: function() { if (typeof this._md5 === 'undefined') { Object.defineProperty(this, '_md5', { value: fis.util.md5(this.getContent()), writable: false }); } return this._md5; }, /** * 返回文件内容的base64编码 * @param {Boolean} prefix 是否需要base64格式头, 默认为 `true`。 * @return {String} */ getBase64: function(prefix) { prefix = typeof prefix === 'undefined' ? true : prefix; if (prefix) { prefix = 'data:' + fis.util.getMimeType(this.rExt) + ';base64,'; } else { prefix = ''; } return prefix + fis.util.base64(this._content); }, /** * 返回文件 ID * @return {String} */ getId: function() { return this.id; }, /** * 返回文件url, 跟直接获取 url 属性不同,此方法会受 useHash 和 useDomain 设置影响,会对应加上内容。 * @return {String} */ getUrl: function() { var url = this.url; if (this.useHash) { url = addHash(url, this); } // if (this.useDomain) { if (!this.domain) { this.domain = getDomain(this.subpath); } else if (Array.isArray(this.domain)) { this.domain = getDomain(this.subpath, this.domain); } url = this.domain + url; // } return url + this.query; }, /** * 获取文件的发布路径,会带上 hash 信息,如果设置了 useHash 的话。 * @param {String} release 路径,不指定时使用此对象上的 release 属性。 * @return {String} */ getHashRelease: function(release) { release = release || this.release; if (release) { if (this.useHash) { return addHash(release, this); } else { return release; } } else { fis.log.error('unreleasable file [%s]', this.realpath); } }, /** * 添加新的同步依赖,同一个 ID 只会保留一条记录。 * @param {String} id 依赖的文件标识(id) */ addRequire: function(id) { if (id && (id = id.trim())) { // js embend 同名.html 而 同名.html 又开启了依赖同名.js,然后就出现了,自己依赖自己了。 // 所以这里做了 id !== this.id 的判断 if (id !== this.id && !~this.requires.indexOf(id)) { this.requires.push(id); } // 如果在异步依赖中,则需要把它删了。 var idx; if (~(idx = this.asyncs.indexOf(id))) { this.asyncs.splice(idx, 1); } return id; } return false; }, /** * 添加异步依赖,同一个 ID 只会保留一条记录。 * @param {String} id 依赖的文件标识(id) */ addAsyncRequire: function(id) { if (id && (id = id.trim())) { // 已经在同步依赖中,则忽略 if (~this.requires.indexOf(id)) { return id; } if (!~this.asyncs.indexOf(id)) { this.asyncs.push(id); } // 兼容老的用法。 this.extras.async = this.asyncs.concat(); return id; } return false; }, /** * 向File.links中追加不重复link连接 * @param {String} filepath 连接 */ addLink: function(filepath) { var links = this.links; ~links.indexOf(filepath) || links.push(filepath); }, /** * 处理同名但不同后缀名的require * @param {String} ext 可以指定后缀,如果指定了,则添加指定后缀的同名依赖。 */ addSameNameRequire: function(ext) { var path, map; if (fis.util.isFile(this.realpathNoExt + ext) && caseSensiveFileExits(this.realpathNoExt + ext)) { path = './' + this.filename + ext; } else if ((map = fis.media().get('project.ext'))) { for (var key in map) { if (map.hasOwnProperty(key)) { var oExt = normalizeExt(key); var rExt = normalizeExt(map[key]); if (rExt === ext && fis.util.isFile(this.realpathNoExt + oExt) && caseSensiveFileExits(this.realpathNoExt + oExt)) { path = './' + this.filename + oExt; break; } } } } else { // 没有设置 project.ext 且 this.realpathNoExt + ext 也没有找到。 // 只能把当前目录下面的其他文件列出来了。 // // 推荐像 fis2 中一样设置 project.ext 来提高性能。 var pattern = this.subpathNoExt + '.*'; var files = fis.project.getSourceByPatterns(pattern); Object.keys(files).every(function(subpath) { var file = files[subpath]; if (file.rExt === ext) { path = subpath; return false; } return true; }); } if (path) { var info = fis.uri.getId(path, this.dirname); if (info.file && info.file.useMap && !~this.asyncs.indexOf(info.id)) { this.addLink(info.file.subpath); this.addRequire(info.id); } } }, /** * 删除同步require路径 * @param {String} id 文件id */ removeRequire: function(id) { var pos = this.requires.indexOf(id); if (pos > -1) { this.requires.splice(pos, 1); } }, /** * 删除异步require路径 * @param {String} id 文件id */ removeAsyncRequire: function(id) { var pos = this.asyncs.indexOf(id); if (pos > -1) { this.asyncs.splice(pos, 1); // 兼容老的用法。 this.extras.async = this.asyncs.concat(); } }, /** * 获取缓存数据, 文件每次编译都会存储一些属性到文件缓存。 * * 默认除了 _xxxx 私有属性外所有的属性都会存入到缓存。 * * @return {Object} */ getCacheData: function() { var obj = {}; var self = this; Object .keys(this) .forEach(function(key) { // 过滤掉私有属性。 _key // 但是不过滤 __key if (key !== 'cache' && key !== 'url' && !/^_[^_]/.test(key)) { obj[key] = self[key]; } }); return obj; }, /** * 当缓存有效时,将缓存数据还原到文件对象上。 * @param {Object} cached 缓存数据 */ revertFromCacheData: function(cached) { _.assign(this, cached); } }); /** * 用来创建 File 对象, 更多细节请查看 {@link fis.file~File File} 说明。 * * ```js * var file = fis.file(root, 'static/xxx.js'); * ``` * * @see {@link fis.file~File File 类说明} * @param {String} filepath... 文件路径 * @function * @namespace fis.file */ module.exports = File.factory(); /** * 用来包裹文件,输入可以是路径也可以是文件对象,输出统一为文件对象。 * @param {Mixed} file 文件路径,或者文件对象 * @return {@link fis.file~File File 对象} * @example * var file = fis.file.wrap('path of file'); * var file2 = fis.file.wrap(file); * @memberOf fis.file * @name wrap */ module.exports.wrap = function(file) { if (typeof file === 'string') { return new File(file); } else if (file instanceof File) { return file; } else { fis.log.error('unable to convert [%s] to [File] object.', (typeof file)); } };