ozma
Version:
Intelligent autobuild tool for OzJS
807 lines (757 loc) • 27.3 kB
JavaScript
var fs = require('fs');
var path = require('path');
var vm = require('vm');
var optimist = require('optimist');
var INDENTx1 = '';
var STEPMARK = '\033[34m==>\033[0m';
var RE_AUTOFIXNAME = /define\((?=[^'"])/;
var RE_AUTOARGS = /define\(([^\[\]]*?)function\((.*?)\)\{/g;
var RE_REQUIRE = /(^|\s)require\(\s*(\[[^\]]*\]|[^\)]+?)\s*\,/gm;
var RE_REQUIRE_VAL = /(require\(\s*)(['"].+?['"])\)/g;
var RE_REQUIRE_DEPS = /(^|[^\S\n\r]*)(require\(\s*)(\[[^\]]*\])/g;
var RE_DEFINE_DEPS = /(^|[^\S\n\r]*)(define\(\s*[^\[\),]+,\s*)(\[[^\]]*\])/g;
var RE_AMD_DEFINE = /(^|[^\w\-])define\(/;
var RE_CJS_REQUIRE = /(^|[^\w\-])require\(/;
var RE_CJS_EXPORTS = /(^|[^\w\-])exports[\.\[\s\=]/;
var CONFIG_BUILT_CODE = '\nrequire.config({ enable_ozma: true });\n\n';
var _DEFAULT_CONFIG = {
"baseUrl": "./",
"distUrl": null,
"jamPackageDir": null,
"loader": null,
"ignore": null,
"disableAutoSuffix": false
};
function ozma(opt){
opt = opt || {};
var logger = opt.logger || Object.create(console);
delete require.cache[require.resolve('ozjs')];
var oz = require('ozjs').oz;
var origin_oz = oz.origin;
var _runtime;
var _config = {};
var _build_script = '';
var _loader_config_script = '';
var _current_scope_file;
var _capture_require;
var _require_holds = [];
var _scripts = {};
var _refers = {};
var _delays= {};
var _code_cache = {};
var _code_bottom = '';
var _mods_code_cache = {};
var _file_scope_mods = {};
var _file_scope_scripts = {};
var _build_history = {};
var _lazy_loading = [];
var _is_global_scope = true;
var _delay_exec;
var _loader_readed;
var _output_count = 0;
var _begin_time;
var _complete_callback;
var _is_library_release = false;
/**
* implement hook
*/
oz.require = function(deps){
if (_capture_require) {
_require_holds.push.apply(_require_holds, typeof deps === 'string' ? [deps] : deps);
} else {
return origin_oz.require.apply(this, arguments);
}
};
oz.require.config = origin_oz.require.config;
/**
* implement hook
*/
oz.exec = function(list){
var output_mods = [], output_code = '', count = 0;
if (_is_global_scope) {
var loader = _config.loader || oz.cfg.loader;
if (loader) {
if (_loader_readed) {
list.push({
name: '__loader__',
url: loader
});
} else {
return _delay_exec = function(){
oz.exec(list);
};
}
} else if (!_is_library_release) {
output_code += CONFIG_BUILT_CODE;
}
}
logger.log(STEPMARK, 'Building');
if (_is_library_release) {
list = list.slice(1);
}
list.reverse().forEach(function(mod){
var mid = mod.name;
if (mod.is_reset) {
mod = oz.cfg.mods[mid];
}
if (mod.url || !mid) {
if (mod.built
|| !mid && !_is_global_scope) {
if (mod.built) {
logger.warn('\033[33m', 'ignore: ', mod.url, '\033[0m');
}
return;
}
var import_code = this[mid || ''];
if (!import_code) {
return;
}
output_mods.push(mod);
// semicolons are inserted between files if concatenating would cause errors.
output_code += '\n/* @source ' + (mod.url || '') + ' */;\n\n'
+ import_code;
if (mid !== '__loader__') {
_mods_code_cache[_build_script].push([mid, import_code]);
} else if (_is_global_scope) {
if (_loader_config_script) {
output_code += _loader_config_script;
}
if (!_is_library_release) {
output_code += CONFIG_BUILT_CODE;
}
}
if (mod.url && mod.url !== _build_script) {
count++;
logger.log(INDENTx1, '\033[36m' + 'import: ', mod.url + '\033[0m');
}
mod.built = true;
}
}, _code_cache);
var output = _config.disableAutoSuffix ? _build_script
: oz.namesuffix(_build_script);
if (!_is_global_scope) {
output = path.join(_config.distUrl || _config.baseUrl, output);
} else if (_config.distUrl) {
output = path.join(_config.distUrl,
relative_path(output, _config.baseUrl));
}
output_code += _code_bottom;
var _writeCallback = function(err){
if (err) {
throw err;
}
logger.log(INDENTx1, count, 'files');
logger.log(INDENTx1, 'target: ', '\033[4m' + output + '\033[0m');
logger.log(INDENTx1, 'Success!\n');
_output_count++;
_is_global_scope = false;
if (!seek_lazy_module()) {
logger.log(_output_count + ' files, built in '
+ (+new Date() - _begin_time) + 'ms');
if (_complete_callback) {
_complete_callback();
}
}
};
if(opt.pipe) { // is pipe
return opt.pipe(output, output_code, output_mods, _writeCallback);
}
return writeFile3721(output, output_code, _writeCallback);
};
/**
* implement hook
*/
oz.fetch = function(m, cb){
var url = m.url,
is_undefined_mod,
observers = _scripts[url];
if (!observers) {
var mname = m.name, delays = _delays;
if (m.deps && m.deps.length && delays[mname] !== 1) {
delays[mname] = [m.deps.length, cb];
m.deps.forEach(function(dep){
var d = oz.cfg.mods[oz.realname(dep)];
if (this[dep] !== 1 && d.url && d.loaded !== 2) {
if (!this[dep]) {
this[dep] = [];
}
this[dep].push(m);
} else {
delays[mname][0]--;
}
}, _refers);
if (delays[mname][0] > 0) {
return;
} else {
delays[mname] = 1;
}
}
observers = _scripts[url] = [[cb, m]];
read(m, function(data){
var _mods = oz.cfg.mods;
if (data) {
exec_in_runtime(data, function(deps){
merge(_mods[m.name].deps, deps);
}, function(){
logger.info(INDENTx1, '\033[33m' + 'unrecognized module: ', m.name + '\033[0m');
});
if (_mods[m.name] === m) {
is_undefined_mod = true;
}
}
observers.forEach(function(args){
args[0].call(args[1]);
});
if (data) {
auto_fix_anon(_mods[m.name]);
if (is_undefined_mod) {
if (_mods[m.name] === m) {
_code_cache[m.name] += '\n/* autogeneration */'
+ '\ndefine("' + m.name + '", ['
+ (m.deps && m.deps.length ? ('"' + m.deps.join('", "') + '"') : '')
+ '], function(){});\n';
} else {
auto_fix_name(m.name);
}
}
auto_fix_deps(_mods[m.name]);
}
_scripts[url] = 1;
if (_refers[mname] && _refers[mname] !== 1) {
_refers[mname].forEach(function(dm){
var b = this[dm.name];
if (--b[0] <= 0) {
this[dm.name] = 1;
oz.fetch(dm, b[1]);
}
}, delays);
_refers[mname] = 1;
}
});
} else if (observers === 1) {
cb.call(m);
} else {
observers.push([cb, m]);
}
};
function read(m, cb){
var file = path.resolve(path.join(_config.baseUrl, m.url));
if (!fs.existsSync(file)) {
setTimeout(function(){
logger.log(INDENTx1, '\033[33m' + 'undefined module: ', m.name + '\033[0m');
cb();
}, 0);
return;
}
fs.readFile(file, 'utf-8', function(err, data){
if (err) {
return logger.error("\033[31m", 'ERROR: Can not read "' + file + '"\033[0m');
}
if (data) {
if (is_commonjs(data)) {
data = 'define(function(require, exports, module){\n'
+ data
+ '\n});';
}
_code_cache[m.name] = data;
}
cb(data);
});
}
function is_commonjs(data){
if (RE_AMD_DEFINE.test(data)) {
return false;
}
var is_cjs = false;
var new_exports = {};
var new_require = function(mod, fn){
if (!fn && (Array.isArray(mod)
|| typeof mod === 'string')) {
is_cjs = true;
}
};
new_require.config = function(){};
var new_context = {
require: new_require,
define: function(){},
module: {
exports: new_exports
},
exports: new_exports
};
new_context.window = new_exports;
try {
vm.runInNewContext(data, new_context);
} catch (ex) {
if (RE_CJS_REQUIRE.test(data) || RE_CJS_EXPORTS.test(data)) {
return true;
}
}
if (new_exports !== new_context.module.exports) {
return true;
}
for (var i in new_exports) {
i = null;
return true;
}
return is_cjs;
}
function exec_in_runtime(data, done, error){
try {
_capture_require = true;
vm.runInContext(data, _runtime);
_capture_require = false;
if (done) {
done(_require_holds);
}
_require_holds.length = 0;
} catch(ex) {
if (error) {
error(ex);
} else {
logger.info(INDENTx1, '\033[33m' + 'build script: [' + ex + ']\033[0m');
}
_capture_require = false;
_require_holds.length = 0;
}
}
function seek_lazy_module(){
if (!_lazy_loading.length) {
var code, clip;
for (var file in _mods_code_cache) {
code = _mods_code_cache[file];
clip = code.pop();
_current_scope_file = file;
break;
}
if (!clip) {
delete _mods_code_cache[file];
if (!code) {
return false;
} else {
return seek_lazy_module();
}
}
var r;
while (r = RE_REQUIRE.exec(clip[1])) {
if (r[2]) {
var deps_str = r[2].trim();
try {
if (!/^\[/.test(deps_str)) {
deps_str = '[' + deps_str + ']';
}
_lazy_loading.push.apply(_lazy_loading, eval(deps_str));
} catch (ex) {
logger.warn(STEPMARK, 'Ignore\n', '\033[33m'
+ r[0].replace(/\n/g, '').replace(/\s/g, '')
+ '...\033[0m\n');
continue;
}
}
}
if (!_lazy_loading.length) {
return seek_lazy_module();
}
unique(_lazy_loading);
if (clip[0]) {
logger.log(STEPMARK, 'Analyzing dynamic dependencies inside', '"' + clip[0]
+ '"(included in', '\033[4m' + _current_scope_file + '\033[0m)');
logger.log('\033[36m', _lazy_loading.map(function(str){
return 'require: "' + str + '"';
}).join('\n '), '\033[0m', '\n');
}
}
var mid = _lazy_loading.pop();
if (!mid) {
return false;
}
var mods = _file_scope_mods[_current_scope_file];
var scripts = _file_scope_scripts[_current_scope_file];
var m = mods[oz.realname(mid)];
if (m && m.loaded == 2) {
return seek_lazy_module();
}
var new_build = m && m.url || oz.filesuffix(oz.realname(mid));
if (_build_history[new_build]) {
//return seek_lazy_module();
var last_build = _build_history[new_build];
mods = interset(copy(last_build[0], 1), mods);
scripts = interset(copy(last_build[1], 1), scripts);
}
_build_history[new_build] = [mods, scripts];
oz.cfg.mods = copy(mods, 1);
_scripts = copy(scripts, 1);
switch_build_script(new_build);
logger.log(STEPMARK, 'Running', '"' + mid + '"(' + '\033[4m' + new_build + '\033[0m' + ')', 'as build script');
logger.log(STEPMARK, 'Analyzing');
oz.require(mid, function(){});
return true;
}
function switch_build_script(url){
_build_script = url;
_mods_code_cache[_build_script] = [];
_file_scope_mods[url] = oz.cfg.mods;
_file_scope_scripts[url] = _scripts;
}
function relative_path(origin, target){
target = path.resolve(target).split(path.sep);
origin = path.resolve(origin).split(path.sep);
var file = origin.pop();
var output = [];
for (var i = 0; i < target.length; i++) {
if (target[i] !== origin[i]) {
for (var j = 0; j < target.length - i; j++) {
output.push('..');
}
break;
}
}
if (origin[i]) {
output = output.concat(origin.slice(i));
}
output.push(file);
return output.join(path.sep);
}
function auto_fix_name(mid){
_code_cache[mid] = _code_cache[mid].replace(RE_AUTOFIXNAME, function($0){
return $0 + '"' + mid + '", ';
});
}
function auto_fix_anon(mod){
var hiddenDeps = [];
(mod.block && mod.block.hiddenDeps || []).forEach(function(mid){
var unique_mid = oz.realname(oz.basename(mid, mod));
if (!this[unique_mid]) {
this[unique_mid] = true;
hiddenDeps.push(mid);
}
}, {});
_code_cache[mod.name] = _code_cache[mod.name].replace(RE_AUTOARGS, function($0, $1, $2){
return 'define(' + $1
+ (hiddenDeps.length ? ('["' + hiddenDeps.join('", "') + '"]') : '[]')
+ ', function('
+ ($2 && hiddenDeps.length
? hiddenDeps.map(function(n, i){
return '__oz' + i;
}).join(', ') + ', ' : '')
+ $2 + '){';
});
}
function auto_fix_deps(mod){
_code_cache[mod.name] = _code_cache[mod.name]
.replace(RE_REQUIRE_VAL, function($0, $1, $2){
var dep = eval($2);
return $1 + '"' + oz.basename(dep, mod) + '")';
})
.replace(RE_REQUIRE_DEPS, tidy)
.replace(RE_DEFINE_DEPS, tidy);
function tidy($0, $1, $2, $3){
var deps = eval($3);
if (typeof deps === 'string') {
deps = [deps];
}
deps = deps.map(function(dep){
return oz.basename(dep, mod);
});
return $1 + $2 + (deps.length ?
('[\n' + $1 + ' "' + deps.join('",\n' + $1 + ' "') + '"\n' + $1 + ']')
: $3);
}
}
function load_config(file){
if (!fs.existsSync(file)) {
return false;
}
var json;
try {
json = JSON.parse(fs.readFileSync(file, 'utf-8'));
} catch(ex) {
logger.error("\033[31m", 'ERROR: Can not parse', file, ' ['
+ ex.toString().replace(/\s*\n/g, '') + ']', "\033[0m");
throw ex;
}
config(_config, json, _DEFAULT_CONFIG);
return _config;
}
function main(args, callback, opt){
opt = opt || {};
_complete_callback = callback;
_begin_time = +new Date();
var input_dir = path.dirname(args._[0]);
if (args['silent']) {
disable_methods(logger, ['error']);
}
if (args['library-release']) {
_is_library_release = true;
}
if (!_config["baseUrl"]) {
logger.log(STEPMARK, 'Configuring');
var cfg, input_config = args['config'];
if (input_config) {
cfg = typeof input_config === 'string'
? load_config(args['config'])
: config(_config, input_config, _DEFAULT_CONFIG);
}
if (!cfg) {
cfg = load_config(path.join(input_dir, 'ozconfig.json'));
}
if (!cfg) {
cfg = config(_config, _DEFAULT_CONFIG, _DEFAULT_CONFIG);
logger.warn("\033[33m", "Can not find config file, using defaults: ", "\033[0m",
"\n", '\033[36m', cfg, '\033[0m');
}
}
if (!_runtime) {
var doc = require("jsdom-nogyp").jsdom("<html><head></head><body></body></html>");
var win = merge({
oz: oz,
define: oz.define,
require: oz.require,
console: Object.create(logger),
process: process
}, doc.createWindow());
_runtime = vm.createContext(win);
_runtime.window = _runtime;
if (_config.ignore) {
_config.ignore.forEach(function(mid){
oz.define(mid, [], function(){});
});
}
if (args['jam']) {
logger.log(STEPMARK, 'Building for Jam');
var jam_dir = _config.jamPackageDir || 'jam/';
var jam_path = path.join(_config.baseUrl, jam_dir);
fs.readFile(jam_path + 'require.config.js', 'utf-8', function(err, data){
if (err) {
return logger.error("\033[31m", 'ERROR: Directory "' + jam_path + '" not found in the current path', "\033[0m");
}
vm.runInContext(data, _runtime);
var autoconfig = _runtime.jam.packages.map(function(m){
return 'define("' + m.name + '", "'
+ path.join(jam_dir, (/[^\/]+$/.exec(m.location)[0]), m.main)
+ '");\n';
}).join('');
vm.runInContext(autoconfig, _runtime);
_config.loader = jam_dir + 'oz.js';
fs.readFile(
path.join(
path.dirname(/\S+$/.exec(module.filename)[0]),
'node_modules/ozjs/oz.js'
), 'utf-8', function(err, data){
writeFile3721(path.join(jam_path, 'oz.config.js'), [autoconfig].join('\n'), function(){
logger.log(INDENTx1, 'updating', '\033[4m' + path.join(jam_path, 'oz.config.js') + '\033[0m');
writeFile3721(path.join(jam_path, 'oz.js'), [data, autoconfig].join('\n'), function(){
logger.log(INDENTx1, 'updating', '\033[4m' + _config.loader + '\033[0m');
if (args._.length) {
main(args, callback, {
loader: _config.loader,
loader_config: autoconfig
});
}
});
});
});
});
return;
}
}
if (!args._.length) {
optimist.showHelp(logger.warn);
logger.error("\033[31m", 'ERROR: Missing input file', "\033[0m");
return false;
}
switch_build_script(args._[0]);
_current_scope_file = _build_script;
if (!args['enable-modulelog']) {
disable_methods(_runtime.console);
}
fs.readFile(_build_script, 'utf-8', function(err, data){
if (err) {
return logger.error("\033[31m", 'ERROR: Can not read "' + _build_script + '"\033[0m');
}
if (is_commonjs(data)) {
var MAIN_FOR_CJS = '__main4cjs';
var config_data = data.split(/[\n\r]/);
for (var i = 0, l = config_data.length; i < l; i++) {
if (RE_CJS_REQUIRE.test(config_data[i])) {
break;
}
}
data = config_data.splice(i);
if (!data.length) {
logger.error("\033[31m", "ERROR: build script\033[0m");
return;
}
data = 'define("' + MAIN_FOR_CJS + '", function(require, exports, module){\n'
+ data.join('\n')
+ '\n});';
_code_cache[MAIN_FOR_CJS] = data;
var _mods = oz.cfg.mods;
exec_in_runtime(data);
auto_fix_anon(_mods[MAIN_FOR_CJS]);
data = config_data.join('\n') + '\n'
+ _code_cache[MAIN_FOR_CJS]
.replace(/^define\("\w+",\s*/, 'require(');
}
_code_cache[''] = data;
logger.log(STEPMARK, 'Analyzing');
exec_in_runtime(data, function(deps){
oz.define('__main__', deps.slice(), function(){});
oz.require('__main__', function(){});
});
//read loader script
var loader = _config.loader || oz.cfg.loader;
if (loader) {
if (opt.loader !== loader) {
_loader_config_script = opt.loader_config;
}
read({
name: '__loader__',
url: loader
}, function(){
_loader_readed = true;
if (_delay_exec) {
_delay_exec();
}
});
}
});
}
return {
exec: main
};
}
function merge(origins, news){
if (Array.isArray(origins)) {
var lib = {};
origins.forEach(function(i){
lib[i] = 1;
}, lib);
news.forEach(function(i){
if (!this[i]) {
origins.push(i);
}
}, lib);
} else {
for (var i in news) {
if (!origins.hasOwnProperty(i)) {
origins[i] = news[i];
}
}
}
return origins;
}
function config(cfg, opt, default_cfg){
for (var i in default_cfg) {
if (opt.hasOwnProperty(i)) {
if (default_cfg[i] && typeof default_cfg[i] === 'object' && !Array.isArray(opt[i])) {
if (!cfg[i]) {
cfg[i] = default_cfg[i];
}
for (var j in opt[i]) {
cfg[i][j] = opt[i][j];
}
} else {
cfg[i] = opt[i];
}
} else if (typeof cfg[i] === 'undefined') {
cfg[i] = default_cfg[i];
}
}
return cfg;
}
function interset(origin, other){
for (var i in origin) {
if (!other.hasOwnProperty(i)) {
delete origin[i];
}
}
return origin;
}
function copy(obj, lvl) {
lvl = lvl || 0;
if (!obj || lvl < 0) {
return obj;
}
var newo;
if (Array.isArray(obj)) {
newo = [];
for (var i = 0, l = obj.length; i < l; i++) {
if (typeof obj[i] === 'object') {
newo[i] = copy(obj[i], lvl - 1);
} else {
newo[i] = obj[i];
}
}
} else {
newo = {};
for (var p in obj) {
if (typeof obj[p] === 'object') {
newo[p] = copy(obj[p], lvl - 1);
} else {
newo[p] = obj[p];
}
}
}
return newo;
}
function unique(list){
var r = {}, temp = list.slice();
for (var i = 0, v, l = temp.length; i < l; i++) {
v = temp[i];
if (!r[v]) {
r[v] = true;
list.push(v);
}
}
list.splice(0, temp.length);
return list;
}
function mkdir_p(dirPath, mode, callback) {
fs.mkdir(dirPath, mode, function(err) {
if (err && err.errno === 34) {
return mkdir_p(path.dirname(dirPath), mode, function(){
mkdir_p(dirPath, mode, callback);
});
}
if (callback) {
callback(err);
}
});
}
function writeFile3721(target, content, callback){
fs.writeFile(target, content, function(err){
if (err && err.errno === 34) {
return mkdir_p(path.dirname(target), 0777, function(){
writeFile3721(target, content, callback);
});
}
if (callback) {
callback(err);
}
});
}
function disable_methods(obj, reserved_methods){
var noop = function(){};
for (var method in obj) {
if (reserved_methods && reserved_methods.indexOf(method) !== -1) {
continue;
}
obj[method] = noop;
}
}
optimist.usage('Autobuild tool for OzJS based WebApp.\n'
+ 'Usage: $0 [build script] --config [configuration file]')
.alias('s', 'silent')
.alias('c', 'config')
.boolean('jam')
.boolean('silent')
.boolean('enable-modulelog');
exports.ozma = exports.Ozma = ozma;
exports.exec = function(){
ozma().exec(optimist.alias().argv);
};
if (!module.parent) {
exports.exec();
}