/**
* 文件操作模块
*/
'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));
}
};
Namespaces
- fis
- cache
- cli
- compile
- lang
- config
- emitter
- file
- log
- project
- uri
- util
- applyMatches
- base64
- camelcase
- clone
- copy
- del
- download
- escapeReg
- escapeShellArg
- escapeShellCmd
- exist
- ext
- filter
- find
- getMimeType
- glob
- install
- isAbsolute
- isDir
- isEmpty
- isFile
- isImageFile
- isTextFile
- isUtf8
- isWin
- map
- md5
- merge
- mkdir
- mtime
- nohup
- pad
- parseUrl
- pathinfo
- pipe
- query
- read
- readBuffer
- readJson
- realpath
- realpathSafe
- stringQuote
- toEncoding
- touch
- upload
- write