ali-tmodjs
Version:
Template Compiler
1,214 lines (828 loc) • 29.1 kB
JavaScript
/*!
* TmodJS - AOT Template Compiler
* https://github.com/aui/tmodjs
* Released under the MIT, BSD, and GPL Licenses
*/
'use strict';
var version = require('../package.json').version;
var AOTcompile = require('./AOTcompile.js');
var defaults = require('./defaults.js');
var runtime = require('./runtime.js');
var uglify2 = require('./uglify2.js');
var stdout = require('./stdout.js');
var watch = require('./watch.js');
var path = require('./path.js');
var semver = require('semver');
var iconv = require('iconv-lite');
var fs = require('fs');
var vm = require('vm');
var events = require('events');
var crypto = require('crypto');
var child_process = require('child_process');
var exec = child_process.exec;
//var execSync = child_process.execSync;
// 调试脚本
var DEBUG_File = '.debug.js';
// 缓存目录
var CACHE_DIR = '.cache';
var log = function (message) {
console.log(message);
};
var Tmod = function (base, options) {
// 模板项目路径
this.base = path.resolve(base);
// 项目配置选项
this.options = options = this.getConfig(options);
// 输出路径
this.output = path.resolve(this.base, options.output);
// 运行时输出路径
this.runtime = path.resolve(this.output, options.runtime);
// 清理模板项目临时文件
this._clear();
// 初始化模板引擎
this._initEngine();
// 初始化事件系统
events.EventEmitter.call(this);
// 初始化 watch 事件,修复 watch 的跨平台的 BUG
this.on('newListener', function (event, listener) {
if (/*watch && */event === 'watch') {
this.log('\n[green]Waiting...[/green]\n\n');
watch(this.base, function (data) {
this.emit('watch', data);
}.bind(this), function (folderPath) {
return this.filter(folderPath) && folderPath !== this.output;
}.bind(this), fs);
//watch = null;
}
});
// 监听模板修改事件
this.on('change', function (data) {
var time = (new Date).toLocaleTimeString();
this.log('[grey]' + time + '[/grey]\n');
});
// 监听模板删除事件(Windows NodeJS 暂时无法做到)
this.on('delete', function (data) {
var time = (new Date).toLocaleTimeString();
this.log('[grey]' + time + '[/grey]\n');
this.log('[red]-[/red] ' + data.id + '\n');
});
// 监听模板加载事件
this.on('load', function (error, data) {
if (error) {
this.log('[red]•[/red] ');
this.log(data.id);
return;
}
if (data.modified) {
this.log('[green]•[/green] ');
} else {
this.log('[grey]•[/grey] ');
}
this.log(data.id);
});
// 监听模板编译事件
this.on('compile', function (error, data) {
if (error) {
this.log(' [inverse][red]{{Syntax Error}}[/red][/inverse]\n\n');
} else {
this.log(this.options.debug ? ' [grey]<DEBUG>[/grey]' : '');
this.log(' [grey]:v' + data.version + '[/grey]');
this.log('\n');
}
});
// 调试事件(异步事件)
this.on('debug', function (error) {
this.log('[red]Debug info:[/red]\n');
if (error.line && error.source) {
this.log('[red]' + error.line + ': ' + error.source + '[/red]\n');
}
this.log('[red]' + error.message + '[/red]\n');
});
// 监听模板合并事件
this.on('combo', function (error, data) {
if (error) {
this.log('[red]' + error + '[/red]\n');
} else {
// this.log('[grey]»[/grey] ');
// this.log('[grey]' + this.options.runtime + '[/grey]');
// this.log(' [grey]:build' + data.version + '[/grey]');
// this.log('\n');
}
});
// 输出运行时 TODO: 这个时机需要优化
if(!options.ignoreOutput){
this._buildRuntime();
}
};
// 默认配置
// 用户配置将保存到模板根目录 package.json 文件中
Tmod.defaults = defaults;
Tmod.prototype = {
__proto__: events.EventEmitter.prototype,
// 获取用户配置
getConfig: function () {
var options = arguments[0];
if (!options) {
return this.options;
}
var file = path.join(this.base, 'package.json');
var defaults = Tmod.defaults;
var json = null;
var name = null;
var config = {};
// 读取目录中 package.json
if (fs.existsSync(file)) {
var fileContent = fs.readFileSync(file, 'utf-8');
if (fileContent) {
json = JSON.parse(fileContent);
}
}
if (!json) {
json = {
"name": 'template',
"version": '1.0.0',
"dependencies": {
"ali-tmodjs": "1.0.0"
},
"tmodjs-config": {}
};
}
try{
var targetVersion = json.dependencies['ali-tmodjs']?json.dependencies['ali-tmodjs'].replace(/^~/, ''):json.dependencies.tmodjs.replace(/^~/, '');
}catch(e){
var targetVersion = '1.0.8';
}
try {
// 比较模板项目版本号
if (semver.lt(version, targetVersion)) {
this.log('[red]You must upgrade to the latest version of tmodjs![/red]\n');
this.log('Local: ' + version + '\n')
this.log('Target: ' + targetVersion + '\n');
process.exit(1);
}
} catch (e) {}
// 更新模板项目的依赖版本信息
json.dependencies = {
"ali-tmodjs" : targetVersion
};
// 来自 Tmod.defaults
for (name in defaults) {
config[name] = defaults[name];
}
// 来自 package.json 文件
for (name in json['tmodjs-config']) {
config[name] = json['tmodjs-config'][name];
}
// 来自 Tmod(base, options) 的配置
for (name in options) {
if (options[name] !== undefined) {
config[name] = options[name];
}
}
config = this._fixConfig(config, defaults, json['tmodjs-config'], options);
json['tmodjs-config'] = config;
this['package.json'] = json;
this.projectVersion = json.version;
return config;
},
/**
* 保存用户配置
* @return {String} 用户配置文件路径
*/
saveConfig: function () {
var file = path.join(this.base, 'package.json');
var configName = 'tmodjs-config';
var json = this['package.json'];
var options = json[configName];
var userConfigList = Object.keys(Tmod.defaults);
// 只保存指定的字段
json[configName] = JSON.parse(
JSON.stringify(options, userConfigList)
);
var text = JSON.stringify(json, null, 4);
fs.writeFileSync(file, text, 'utf-8');
return file;
},
/**
* 编译模板
* @param {String, ArrayList} 模板文件相对路径。无此参数则编译目录所有模板
*/
compile: function (file) {
var that = this;
var error = false;
var walk;
if (file) {
var fileList = typeof file === 'string' ? [file] : file;
fileList = fileList.map(function (file) {
return path.resolve(that.base, file);
});
walk = function (list) {
list.forEach(function (file) {
if (error) {
return;
}
error = !that._compile(file);
});
};
walk(fileList);
if (!error && this.options.combo) {
this._combo();
}
} else {
walk = function (dir) {
if (dir === that.output) {
return;
}
var dirList = fs.readdirSync(dir);
dirList.forEach(function (item) {
if (error) {
return;
}
if (fs.statSync(path.join(dir, item)).isDirectory()) {
walk(path.join(dir, item));
} else if (that.filterBasename(item) && that.filterExtname(item)) {
error = !that._compile(path.join(dir, item));
}
});
};
walk(this.base);
if (!error && this.options.combo) {
this._combo();
}
}
},
/**
* 文件与路径筛选器
* @param {String} 绝对路径
* @return {Boolean}
*/
filter: function (file) {
if (fs.existsSync(file)) {
var stat = fs.statSync(file);
if (stat.isDirectory()) {
var dirs = file.split(path.sep);
var basedir = dirs[dirs.length - 1];
return this.filterBasename(basedir) ? true : false;
} else {
return this.filterBasename(path.basename(file))
&& this.filterExtname(path.extname(file));
}
} else {
return false;
}
},
/**
* 名称筛选器
* @param {String}
* @return {Boolean}
*/
filterBasename: function (name) {
// 英文、数字、点、中划线、下划线的组合,且不能以点开头
var FILTER_RE = /^\.|[^\w\.\-$]/;
return !FILTER_RE.test(name);
},
/**
* 后缀名筛选器
* @param {String}
* @return {Boolean}
*/
filterExtname: function (name) {
// 支持的后缀名
var EXTNAME_RE = /\.(html|htm|tpl|art)$/i;
return EXTNAME_RE.test(name);
},
/**
* 启动即时编译,监听文件修改自动编译
*/
watch: function () {
// 监控模板目录
this.on('watch', function (data) {
var type = data.type;
var fstype = data.fstype;
var target = data.target;
var parent = data.parent;
var fullname = path.join(parent, target);
if (target && fstype === 'file' && this.filter(fullname)) {//
if (type === 'delete') {
this.emit('delete', {
id: this._toId(target),
sourceFile: target
});
var jsFile = fullname.replace(path.extname(fullname), '');
jsFile = jsFile.replace(this.base, this.output) + '.js'
this._fsUnlink(jsFile);
this._removeCache(target);
if (this.options.combo) {
this._combo();
}
} else if (/updated|create/.test(type)) {
this.emit('change', {
id: this._toId(target),
sourceFile: target
});
if (this._compile(fullname)) {
if (this.options.combo) {
this._combo();
}
}
}
}
});
},
/**
* 打印日志
* @param {String} 消息
*/
log: function (message) {
stdout(message);
},
// 修正配置-版本兼容
_fixConfig: function (options, defaultsConfig, projectConfig, inputConfig) {
var cwd = process.cwd();
var base = this.base;
// 忽略大小写
options.type = options.type.toLowerCase();
// 模板合并规则
// 兼容 0.0.3-rc3 之前的配置
if (Array.isArray(options.combo) && !options.combo.length) {
options.combo = false;
} else {
options.combo = !!options.combo;
}
// 兼容 0.1.0 之前的配置
if (options.type === 'templatejs') {
options.type = 'default';
}
// 根据生成模块的类型删除不支持的配置字段
if (options.type === 'default' || options.type === 'global') {
delete options.alias;
} else {
delete options.combo;
}
// 处理外部输入:转换成相对于 base 的路径
if (inputConfig.output) {
options.output = path.relative(base, path.resolve(cwd, inputConfig.output));
}
if (inputConfig.syntax && /\.js$/.test(inputConfig.syntax)) {// 值可能为内置名称:native || simple
options.syntax = path.relative(base, path.resolve(cwd, inputConfig.syntax));
}
if (inputConfig.helpers) {
options.helpers = path.relative(base, path.resolve(cwd, inputConfig.helpers));
}
return options;
},
// 文件写入
_fsWrite: function (file, data, charset) {
this._fsMkdir(path.dirname(file));
if (charset !== 'utf-8') {
var buf = iconv.encode(data, charset);
fs.writeFileSync(file, buf, null);
} else {
fs.writeFileSync(file, data, charset || 'utf-8');
}
},
// 文件读取
_fsRead: function (file, charset) {
if (fs.existsSync(file)) {
if (charset === 'utf-8' || charset === '') {
return fs.readFileSync(file, charset || 'utf-8');
} else {
var buf = fs.readFileSync(file, null);
var str = iconv.decode(buf, charset);
return str;
}
}
},
// 创建目录,包括子文件夹
_fsMkdir: function (dir) {
var currPath = dir;
var toMakeUpPath = [];
while (!fs.existsSync(currPath)) {
toMakeUpPath.unshift(currPath);
currPath = path.dirname(currPath);
}
toMakeUpPath.forEach(function (pathItem) {
fs.mkdirSync(pathItem);
});
},
// 删除文件夹,包括子文件夹
_fsRmdir: function (dir) {
var walk = function (dir) {
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
return;
}
var files = fs.readdirSync(dir);
if (!files.length) {
fs.rmdirSync(dir);
return;
} else {
files.forEach(function (file) {
var fullName = path.join(dir, file);
if (fs.statSync(fullName).isDirectory()) {
walk(fullName);
} else {
fs.unlinkSync(fullName);
}
});
}
fs.rmdirSync(dir);
};
walk(dir);
},
// 删除模板文件
_fsUnlink: function (file) {
return fs.existsSync(file) && fs.unlinkSync(file);
},
// 获取字符串 md5 值
_getMd5: function (text) {
return crypto.createHash('md5').update(text).digest('hex');
},
// 获取元数据
_getMetadata: function (js) {
var data = js.match(/\/\*TMODJS\:(.*?)\*\//);
if (data) {
return JSON.parse(data[1]);
}
},
// 删除元数据
_removeMetadata: function (js) {
var data = this._getMetadata(js) || {};
var newText = '';
// 文件末尾设置一个空注释,然后让 UglifyJS 不压缩它,避免很多文件挤成一行
if (data.version) {
newText = '/*v:' + data.version + '*/';
}
return js.replace(/^\/\*TMODJS\:[\w\W]*\*\//, newText);
},
// 设置元数据
_setMetadata: function (js, data) {
data = JSON.stringify(data || {});
js = '/*TMODJS:' + data + '*/\n' + js
.replace(/\/\*TMODJS\:(.*?)\*\//, '');
return js;
},
// 调试语法错误
_debug: function (error, callback) {
var debugFile = error.debugFile;
var code = error.temp;
code = "/*! <DEBUG:" + error.id + '> */\n' + code;
this._fsWrite(debugFile, code, this.options.charset);
// 启动子进程进行调试,从根本上避免影响当前进程
exec('node ' + debugFile, function (error, stdout, stderr) {
var message = error ? error.message : '';
message = message
.replace(/^Command\sfailed\:|\s*SyntaxError[\w\W]*$/g, '')
.trim();
callback(message);
});
},
// 编译运行时
_buildRuntime: function (templates, metadata, callback) {
templates = templates || '';
metadata = metadata || {};
callback = callback || function () {};
var error = null;
var runtimeCode = runtime({
runtimeId:this.options.alias || this._toId(this.options.runtime),
type: this.options.type,
helpers: this._helpersCode,
templates: templates
});
runtimeCode = this._setMetadata(runtimeCode, metadata);
try {
this._fsWrite(this.runtime, runtimeCode, this.options.charset);
} catch (e) {
error = e;
}
if (this.options.debug || !this.options.minify) {
this._beautify(this.runtime);
} else {
this._minify(this.runtime);
}
callback.call(this, error, runtimeCode);
},
_getUglifyOptions: function () {
return {
generatedSourceMapName: this.output,
// require 变量是 AMD 、CMD 模块需要硬解析的字符
reserved: 'require',
// 忽略压缩的注释
comments: '/TMODJS\\:|^v\\:\\d+/',
compress: {
warnings: false
}
};
},
_uglify: function (file, options) {
var result;
try {
result = uglify2(file, file, options);
} catch (e) {
var err = new Error('Uglification failed.');
if (e.message) {
err.message += '\n' + e.message + '. \n';
if (e.line) {
err.message += 'Line ' + e.line + ' in ' + file + '\n';
}
}
err.origError = e;
console.log(err);
}
try {
if (result) {
fs.writeFileSync(file, result.output, this.options.charset);
}
} catch (e) {}
},
// 格式化 js
_beautify: function (file) {
var options = this._getUglifyOptions();
options.mangle = false;
options.beautify = true;
this._uglify(file, options);
},
// 压缩 js
_minify: function (file) {
var options = this._getUglifyOptions();
options.mangle = {};
options.beautify = false;
options.ascii_only = true;
this._uglify(file, options);
},
// 打包模板
_combo: function () {
var files = [];
var combo = '';
var cache = this._getCache();
var code = '';
var build = Date.now();
for (var i in cache) {
code = cache[i];
code = this._removeMetadata(code);
combo += code;
files.push(i);
}
var metadata = {};
if (this.options.debug) {
metadata.debug = true;
}
if (this.options.combo) {
metadata.version = this.projectVersion;
}
this._buildRuntime(combo, metadata, function (error, data) {
// 广播:合并事件
this.emit('combo', error, {
// 编译时间
build: build,
// 打包的代码
output: data,
// 输出的文件路径
outputFile: this.runtime,
// 被合并的文件列表
sourcefiles: files
});
});
},
// 路径转换为模板 ID
// base: /Users/tangbin/Documents/web/tpl
// file: /Users/tangbin/Documents/web/tpl/index/main.html
// >>>>> index/main
_toId: function (file) {
var extname = path.extname(file);
var id = file.replace(this.base + '/', '').replace(extname, '');
return id;
},
// 编译单个模板
// file: /Users/tangbin/Documents/web/tpl/index/main.html
_compile: function (file) {
// 模板字符串
var source = '';
var readError = null;
var compileError = null;
var writeError = null;
// 目标路径
var target = file
.replace(path.extname(file), '.js')
.replace(this.base, this.output);
var mod = this._getCache(file);
var modObject = {};
var metadata = {};
var count = 0;
var isDebug = this.options.debug;
var isCacheDir = this.options.combo;
try {
source = this._fsRead(file, this.options.charset)
} catch (e) {
readError = e;
}
var newMd5 = this._getMd5(source + JSON.stringify(this['package.json']));
// 如果开启了合并,编译后的文件使用缓存目录保存
if (isCacheDir) {
target = target.replace(this.output, path.join(this.output, CACHE_DIR));
}
// 尝试从文件中读取上一次编译的结果
if (!mod && fs.existsSync(target)) {
mod = this._fsRead(target, this.options.charset);
}
// 获取缓存的元数据
if (mod) {
metadata = this._getMetadata(mod) || {};
count = metadata.version || 0;
}
// 检查是否需要编译
var modified = !this.options.cache
|| !mod // 从来没有编译过
|| metadata.debug // 上个版本为调试版
|| isDebug // 当前配置为调试版
|| newMd5 !== metadata.md5; // 模板已经发生了修改(包括配置文件)
// 获取模板 ID
var id = this._toId(file);
// 广播:模板加载事件
this.emit('load', readError, {
// 模板 ID
id: id,
// 模板是否需要重新编译
modified: modified,
// 原始文件路径
sourceFile: file,
// 模板源代码
source: source,
// 输出路径
outputFile: target
});
if (readError) {
return;
}
try {
// 编译模板
if (modified) {
var customizePath = "";
if(this.options.customizePath){
customizePath = path.resolve(this.base, this.options.customizePath);
}
modObject = this.template.AOTcompile(source, {
filename: id,
alias: this.options.alias,
type: this.options.type,
compress: this.options.compress,
escape: this.options.escape,
runtime: this.options.runtime,
debug: isDebug,
resolve:this.options.resolve,
customizePath:customizePath
});
mod = modObject.code;
}
} catch (e) {
compileError = e;
}
// 不输出的情况:遇到错误 || 文件或配置没有更新
if (!compileError && modified) {
count ++;
mod = this._setMetadata(mod, {
debug: isDebug,
version: count,
md5: newMd5
});
try {
this._fsWrite(target, mod, this.options.charset);
} catch (e) {
writeError = e;
}
if (!isCacheDir && !writeError) {
if (isDebug || !this.options.minify) {
this._beautify(target);
} else {
this._minify(target);
}
}
}
var compileInfo = {
// 模板 ID
id: id,
// 版本
version: count,
// 源码
source: source,
// 模板文件路径
sourceFile: file,
// 编译结果代码
output: mod,
// 编译输出文件路径
outputFile: target,
// 是否被修改
modified: modified,
// 依赖的子模板 ID 列表
requires: modObject.requires || []
};
if (compileError && !compileError.source) {
// 语法错误,目前只能对比生成后的 js 来查找错误的模板语法
compileError.debugFile = path.join(this.base, DEBUG_File);
this.debuging = true;
this._debug(compileError, function (message) {
var e = {
// 错误名称
name: compileError.name,
// 报错信息
message: message,
// 调试文件地址
debugFile: compileError.debugFile,
// 编译器输出的临时文件
temp: compileError.temp
};
for (var name in e) {
compileError[name] = e[name];
}
this.emit('debug', compileError);
}.bind(this));
} else {
// 删除上次遗留的调试文件
if (this.debuging) {
this._fsUnlink(path.join(this.base, DEBUG_File));
delete this.debuging;
}
// 缓存编译好的模板
this._setCache(file, mod);
}
this.emit('compile', compileError || writeError, compileInfo);
if (compileError || writeError) {
return null;
} else {
return compileInfo;
}
},
// 计算字节长度
_getByteLength: function (content) {
return content.replace(/[^\x00-\xff]/gi, '--').length;
},
_cache: {},
// 获取缓存
_getCache: function (id) {
if (typeof id === 'undefined') {
return this._cache;
} else {
return this._cache[id];
}
},
// 设置缓存
_setCache: function (id, data) {
this._cache[id] = data;
},
// 删除缓存
_removeCache: function (id) {
delete this._cache[id];
},
// 初始化模板引擎
_initEngine: function () {
var options = this.options;
var template;
switch (String(options.syntax)) {
case 'native':
template = require('./syntax/native.js');
break;
case 'simple':
template = require('./syntax/simple.js');
break;
// 不再推荐使用动态加载自定义语法
// 为了兼容 < v1.0 的功能
default:
var syntaxFile = path.resolve(this.base, options.syntax);
if (fs.existsSync(syntaxFile)) {
template = require('./syntax/native.js');
var syntaxCode = fs.readFileSync(syntaxFile, 'utf-8');
vm.runInNewContext(syntaxCode, {
console: console,
template: template
});
} else {
this.log('[red]Not found: ' + syntaxFile + '[/red]');
process.exit(1);
}
}
// 配置模板引擎:辅助方法
if (options.helpers) {
var helpersFile = path.resolve(this.base, options.helpers);
if (fs.existsSync(helpersFile)) {
this._helpersCode = fs.readFileSync(helpersFile, 'utf-8');
vm.runInNewContext(this._helpersCode, {
console: console,
template: template
});
} else {
this.log('[red]Not found: ' + helpersFile + '[/red]');
process.exit(1);
}
}
this.template = AOTcompile(template);
},
// 清理项目临时文件
_clear: function () {
// 删除上次遗留的调试文件
this._fsUnlink(path.join(this.base, DEBUG_File));
// 删除不必要的缓存目录
if (!this.options.combo) {
this._fsRmdir(path.join(this.output, CACHE_DIR));
}
}
};
module.exports = Tmod;