kmc
Version:
KISSY Module Compiler
524 lines (484 loc) • 19.7 kB
JavaScript
/**
*
* @author: daxingplay<daxingplay@gmail.com>
* @time: 13-1-23 16:22
* @description:
*/
var UglifyJS = require('uglify-js'),
fs = require('fs'),
path = require('path'),
os = require('os'),
_ = require('lodash'),
iconv = require('iconv-lite'),
getAst = require('./get-ast'),
utils = require('./utils'),
dependencies = require('./dependencies');
// modified by bachi@taobao.com 2013-12-12
// op:解析后文件(要输出的)路径
function Compiler(config, op){
this.outputFilePath = op;
this.modules = {};
this.fileList = [];
this.analyzedModules = [];
this.combinedModules = [];
this.buildComboModules = [];
this.buildAnalyzedModules = [];
this.config = config;
this.packages = config.packages;
}
Compiler.prototype._addModuleName = function(moduleContent, mod){
var modName = mod.name;
//add module path
var start = moduleContent.indexOf('KISSY.add(');
if(start == -1) {
start = moduleContent.indexOf('.add(');
if(start != -1) {
start = start + 5;
} else {
start = moduleContent.indexOf('define(');
if(start != -1){
start += 7;
}
}
} else {
start = start + 10;
}
var end = moduleContent.indexOf('function', start);
//find it
if(start > -1 && end > start) {
//KISSY.add(/*xxx*/function(xxx)) or KISSY.add('xxx', function(xxx))
//remove user comments but preserve user defined module name.
var oldModuleName = moduleContent.substring(start, end),
moduleNameRegResult = oldModuleName.match(/^(\s*)\/\*(.*)\*\/(\s*)$/);
if(moduleNameRegResult !== null || oldModuleName.replace(/[\r\n\s]/g, '') === ''){
// replace user comments or white spaces with my module name.
moduleContent = moduleContent.replace(oldModuleName, '\'' + modName + '\', ');
}
} else if(start > -1 && end == start) {
//KISSY.add(function(xxx))
var addedContentArr = [moduleContent.slice(0, start), '\'' + modName + '\','];
if(mod.version === '1.4+' && mod.requires){
var requires = [];
mod.requires.forEach(function(requirePath){
requires.push("'" + requirePath + "'");
});
addedContentArr.push("[" + requires.join(", ") + "], ");
}
addedContentArr.push(moduleContent.slice(end));
moduleContent = addedContentArr.join('');
}
return moduleContent;
};
Compiler.prototype._isFileIgnored = function(filePath){
return utils.isExcluded(filePath, this.config.ignoreFiles);
};
Compiler.prototype._isModuleExcluded = function(modName){
return utils.isExcluded(modName, this.config.exclude);
};
Compiler.prototype._getModuleRealPathByParent = function(requiredModule, parentModule){
var self = this;
if(requiredModule.match(/^\./) && _.isObject(parentModule)){
return path.resolve(path.dirname(parentModule.path), utils.addPathSuffix(requiredModule));
}else if(!requiredModule.match(/\//) || requiredModule.match(/^gallery\//)){
// may be kissy module.
return requiredModule;
}else{
for(var i = 0; i < self.packages.length; i++){
var pkg = self.packages[i],
mod = requiredModule.match(new RegExp(pkg.name + '\/(.*)'));
if(mod && mod[1]){
var modulePath = path.resolve(pkg.path, pkg.ignorePackageNameInUri ? './' : pkg.name, utils.addPathSuffix(mod[1]));
// console.log('%s ==> %s, package path is %s, mod1 is %s ', requiredModule, modulePath, pkg.path, utils.addPathSuffix(mod[1]));
if(fs.existsSync(modulePath)){
return modulePath;
}
}
}
}
var lastPath = path.resolve(path.dirname(parentModule.path), utils.addPathSuffix(requiredModule));
return fs.existsSync(lastPath) ? lastPath : requiredModule;
};
Compiler.prototype._getModuleRealPathByPkg = function(filePath, pkg){
return path.resolve(pkg.path, pkg.name, utils.addPathSuffix(filePath));
};
/**
* get module's package by its full path.
* @param filePath this is file full path.
* @return {*}
* @private
*/
Compiler.prototype._getPackage = function(filePath){
var self = this,
pkgPath = '',
modPkg = null;
if(filePath.match(/^[a-zA-Z]+$/)){
return 'kissy';
}
if(filePath.match(/^gallery\//)){
return 'gallery';
}
if(!fs.existsSync(filePath)){
return null;
}
// TODO optimize.
for(var i = 0; i < self.packages.length; i++){
var pkg = self.packages[i];
var dirname = path.dirname(filePath);
//by hanwen 2013-08-16
//是否属于当前包
var isBelongPkg = false;
if(filePath.match(new RegExp(pkg.name))){
isBelongPkg = true;
} else if (pkg.ignorePackageNameInUri && (dirname.indexOf(pkg.path) > -1 || path.resolve(filePath) === path.resolve(pkg.path + '.js'))) {
//增加ignorePackageNameInUri支持,使用路径判断,如果pkg.path和
//filePath的路径进行对比
isBelongPkg = true;
}
if(isBelongPkg){
//如果有配置ignorePackageNameInUri, 把pkg.name忽略
var name = pkg.ignorePackageNameInUri ? '' : pkg.name;
var curRelativePath = path.relative(path.resolve(pkg.path, name), filePath);
if(pkgPath == '' || pkgPath.length > curRelativePath.length){
pkgPath = curRelativePath;
modPkg = pkg;
}
}
}
return modPkg;
};
Compiler.prototype._getModuleNameFromRequire = function(requirePath){
var matches = /^([^\/]+?)\/\S+/.exec(requirePath);
return matches && matches[1] ? matches[1] : '';
};
Compiler.prototype._moduleName = function(filePath, pkg){
var relativePkgPath = path.relative(pkg.path, filePath).replace(/\\/g, '/'),
moduleName = relativePkgPath.replace(/^\.\//, '');
if(!utils.startWith(relativePkgPath, pkg.name + '/') && pkg.name !== 'kissy'){
moduleName = pkg.name + '/' + moduleName;
}
return path.normalize(moduleName.replace(/\.js$/i, '')).replace( /\\/g, '/' );
};
Compiler.prototype._buildCombo = function(mod){
var self = this,
result = {};
_.forEach(mod.dependencies, function(subMod){
if(_.indexOf(self.buildAnalyzedModules, subMod.name) === -1){
self.buildAnalyzedModules.push(subMod.name);
if(subMod.pkg == 'kissy' || subMod.status != 'ok' || subMod.type === 'css'){
self.buildComboModules.push(subMod.name);
}
!_.isEmpty(subMod.dependencies) && self._buildCombo(subMod);
}
});
result[mod.name] = self.buildComboModules;
return result;
};
Compiler.prototype._mapModuleName = function(modName){
var self = this,
rules = self.config.map;
if(modName && !_.isEmpty(rules)){
_.forEach(rules, function(rule){
modName = modName.replace(new RegExp(rule[0]), rule[1]);
});
}
return modName;
};
Compiler.prototype._mapContent = function(content, mod){
var self = this,
rules = self.config.map;
if(content && !_.isEmpty(content)){
_.forEach(mod.requires, function(requirePath){
if(!/^\.{1,2}/.test(requirePath)){
var mappedRequirePath = requirePath;
_.forEach(rules, function(rule){
mappedRequirePath = requirePath.replace(new RegExp(rule[0]), rule[1]);
});
if(mappedRequirePath != requirePath){
// TODO require regex is toooooooooooo complex. So I chose the simple one.
content = content.replace(new RegExp("(['\"])" + requirePath + "(['\"])"), '$1' + mappedRequirePath + '$2');
}
}
});
}
return content;
};
Compiler.prototype._generateRealModuleContent = function(mod){
var self = this;
if(!mod || !mod.path) return '';
var fileContent = fs.readFileSync(mod.path);
var modContent = iconv.decode(fileContent, mod.charset || 'utf-8');
// fix issue 23: remove file BOM before combo.
if(/^\uFEFF/.test(modContent)){
modContent = modContent.toString().replace(/^\uFEFF/, '');
}
modContent = self._addModuleName(modContent, mod);
return self._mapContent(modContent, mod);
};
Compiler.prototype._alias = function(modName){
var self = this;
if(self.config.modules && self.config.modules[modName]){
var modConfig = self.config.modules[modName];
if(modConfig.alias){
var modNew = {
name: modConfig.alias,
path: '',
pkg: self._getModuleNameFromRequire(modConfig.alias)
};
if(modNew.pkg && self.config.pkgs[modNew.pkg]){
var modPkg = self.config.pkgs[modNew.pkg];
var dir = modPkg.ignorePackageNameInUri ? path.resolve(modPkg.path, '../') : modPkg.path;
modNew.path = path.resolve(dir, utils.addPathSuffix(modNew.name));
if(fs.existsSync(modNew.path)){
modNew.pkg = modPkg;
return modNew;
}
}
}
}
return null;
};
Compiler.prototype.getAst = getAst;
/**
* analyze a file's dependencies.
* @param inputFilePath please use absolute file path.
* @return {*}
*/
Compiler.prototype.analyze = function(inputFilePath){
var self = this,
mod = null,
modRequires,
modName,
modPkg,
modType,
modVersion,
modRealPath;
if(!inputFilePath) return null;
// analyze module package
modPkg = self._getPackage(inputFilePath);
switch (modPkg){
case null:
!self.config.silent && console.log('cannot get package for %s, guess it is kissy module.', inputFilePath);
mod = {
name: inputFilePath,
pkg: 'unknown' || self._getModuleNameFromRequire(inputFilePath),
status: 'missing'
};
self.modules[mod.name] = mod;
break;
case 'kissy':
mod = {
name: inputFilePath,
pkg: 'kissy',
status: 'ok'
};
self.modules[mod.name] = mod;
break;
case 'gallery':
mod = {
name: inputFilePath,
pkg: 'gallery',
status: 'ok'
};
self.modules[mod.name] = mod;
break;
default :
// get module real path based on package path.
modRealPath = self._getModuleRealPathByPkg(inputFilePath, modPkg);
if(!self._isFileIgnored(modRealPath)){
modName = self._moduleName(modRealPath, modPkg);
// map module names if user configured map rules.
modName = self._mapModuleName(modName);
var modAlias = self._alias(modName);
if(modAlias){
modName = modAlias.name;
modPkg = modAlias.pkg;
modRealPath = modAlias.path;
}
if(_.indexOf(self.fileList, modRealPath) === -1){
// to avoid recursive analyze.
self.fileList.push(modRealPath);
if(!modRealPath.match(/\.css$/)){
// get this file's dependencies.
modRequires = dependencies.getRequiresFromFn(modRealPath, !self.config.silent);
if(modRequires.length){
modVersion = '1.4+';
}else{
modRequires = dependencies.requires(modRealPath);
}
// if user named module himself, use his name. map rules will not work then.
if(_.isPlainObject(modRequires) && modRequires.name){
modName = modRequires.name;
modRequires = modRequires.deps;
}
modType = 'js';
}else{
modRequires = [];
modType = 'css';
}
var isModExcluded = self._isModuleExcluded(modName);
// add this module to list.
mod = {
name: modName,
pkg: modPkg,
status: isModExcluded ? 'excluded' : 'ok',
path: modRealPath,
requires: modRequires,
dependencies: [],
dependencyTree: {},
charset: modPkg.charset || 'utf8',
type: modType,
version: modVersion
};
self.modules[modName] = mod;
if(modType === 'js'){
// analyze sub modules' dependencies recursively.
_.forEach(modRequires, function(subModPath){
var subMod = self.analyze(self._getModuleRealPathByParent(subModPath, mod));
if(subMod){
mod.dependencies.push(subMod);
mod.dependencyTree[subMod.name] = subMod.dependencyTree;
}
});
!isModExcluded && self.analyzedModules.push(modName);
}
}else{
mod = self.modules[modName];
}
}
break;
}
return mod;
};
Compiler.prototype.build = function(inputFilePath, outputFilePath, outputCharset){
var self = this;
var result = self.analyze(inputFilePath);
// deal with output charset. if not specified, use charset in config.
outputCharset = ((typeof outputCharset === 'undefined' || outputCharset === null) ? self.config.charset : outputCharset) || 'utf8';
var combineFile = self.config.suffix ? outputFilePath.replace(/\.js$/i, self.config.suffix + '.js') : outputFilePath;
if(path.extname(combineFile) != '.js'){
combineFile = path.resolve(combineFile, path.basename(inputFilePath, '.js') + self.config.suffix + '.js');
}
result.outputFile = combineFile;
//prepare output dir
utils.mkdirsSync(path.dirname(combineFile));
// if exists, unlink first, otherwise, there may be some problems with the file encoding.
if(fs.existsSync(combineFile)){
fs.unlinkSync(combineFile);
}
if(self.analyzedModules.length){
var combinedComment = [
'/*',
'combined files : ',
'',
self.analyzedModules.join(os.EOL),
'',
'*/',
''
].join(os.EOL);
var fd = fs.openSync(combineFile, 'w');
var combinedCommentBuffer = iconv.encode(combinedComment, outputCharset || 'utf-8');
fs.writeSync(fd, combinedCommentBuffer, 0, combinedCommentBuffer.length);
fs.closeSync(fd);
fd = fs.openSync(combineFile, 'a');
_.forEach(self.analyzedModules, function(modName){
var mod = self.modules[modName];
// add EOL to end of file.
var modContent = self._generateRealModuleContent(mod) + os.EOL;
var buffer = iconv.encode(modContent, outputCharset || 'utf-8');
fs.writeSync(fd, buffer, 0, buffer.length);
self.combinedModules.push(modName);
});
fs.closeSync(fd);
}else{
fs.writeFileSync(combineFile, fs.readFileSync(inputFilePath));
}
!self.config.silent && console.info('[ok]'.bold.green+' %s ===> %s', inputFilePath, combineFile);
result.autoCombo = self._buildCombo(result);
result.combined = self.combinedModules;
return result;
};
Compiler.prototype.info = function(){
var self = this;
var dependencies = {};
var modules = {};
if(_.isObject(self.modules)){
_.forEach(self.modules, function(mod, modName){
if(_.indexOf(self.combinedModules, modName) === -1 && !_.isEmpty(mod.dependencies)){
var requires = [];
_.forEach(mod.dependencies, function(subMod){
requires.push(subMod.name);
});
dependencies[modName] = {
requires: requires
};
}
if(mod && mod.path){
var modContent = self._generateRealModuleContent(mod);
mod.contents = iconv.encode(modContent, mod.charset);
modules[mod.name] = mod;
}
});
}
return {
modules: modules,
dependencies: dependencies
};
}
Compiler.prototype.combo = function(fixModuleName, outputDir ,comboOnly, isJson){
var self = this;
var content = [];
if(outputDir){
outputDir = path.resolve(outputDir);
if(!fs.existsSync(path.resolve(outputDir))){
utils.mkdirsSync(outputDir);
}
}
if(_.isObject(self.modules)){
_.forEach(self.modules, function(mod, modName){
if(_.indexOf(self.combinedModules, modName) === -1 && !_.isEmpty(mod.dependencies)){
var requires = [];
_.forEach(mod.dependencies, function(subMod){
requires.push(subMod.name);
});
if(isJson){
content.push("\"" + modName + "\": [\"" + requires.join("\", \"") + "\"]");
}else{
content.push("'" + modName + "': { requires: ['" + requires.join("', '") + "']}");
}
}
if(fixModuleName === true && mod && mod.path){
var modContent = self._generateRealModuleContent(mod);
var buffer = iconv.encode(modContent, mod.charset || 'utf-8');
var outputPath = mod.path;
if(outputDir){
var relativePath = mod.name;
if(mod.pkg && mod.pkg.ignorePackageNameInUri){
relativePath = mod.name.replace(mod.pkg.name + '/', '');
}
if(mod.type === 'js'){
relativePath = relativePath + '.js';
}
outputPath = path.resolve(outputDir, relativePath);
utils.mkdirsSync(path.dirname(outputPath));
}
// info by 拔赤
// 建议增加开关,是否写到目标路径,这样会便于用户自定义copy规则
// 2013-12-12
// bugfix:grunt-kmc配置comboOnly时,文件写的位置出错的bugfix
if(comboOnly && comboOnly === true){
var tModName = mod.pkg ? modName.replace(mod.pkg.name, '') : modName;
if(outputPath.indexOf(path.normalize(tModName)) >= 0){
fs.writeFileSync(outputPath, buffer);
}
} else {
fs.writeFileSync(outputPath, buffer);
}
}
});
}
var json="{" + os.EOL + " " + content.join("," + os.EOL + " ") + " " + os.EOL + "}";
return content.length ?
(isJson?json:"KISSY.config('modules', "+json+");") :
"";
};
module.exports = Compiler;