UNPKG

modulator

Version:

Easy build tool for running node modules in a non-CommonJS environment.

567 lines (534 loc) 14.1 kB
/*global require module __dirname process setTimeout clearTimeout console*/ (function(){"use strict"; var fs = require('fs'), ujs = require('uglify-js'), path = require('path'), JSLexer = require('./JSLexer'); function isFunc (func) { return typeof func === 'function'; } function safeApply (func, that, args) { if (isFunc(func)) return func.apply(that, args); } function Modulator (options, callback) { return this.initModulator(options, callback); } var Modulator_ = Modulator._ = function () {return this;}, modulator = Modulator_.prototype = Modulator.prototype, api, out; Modulator.check = function (obj) { return obj && obj.initModulator === modulator.initModulator; }; Modulator.create = function (obj, args) { obj = obj || new Modulator_(); obj.initModulator = modulator.initModulator || function (options, callback) { options = options || {}; this.verbose = options.verbose || false; this.apiName = options.apiName || 'api'; this.header = options.header || ''; this.footer = options.footer || ''; this.name = options.name || 'module'; this.from = options.from ? path.resolve(options.from) : process.cwd(); this.source = ''; this.output = []; this.memory = {}; return this.run(options, callback); }; obj.run = function (options, callback) { options = options || {}; callback = callback || options.callback; if (options.main) { this.lastMain = options.main; this.require(options.main, function (err) { if (!err) { this.compile(function (err) { var count = 0, that = this, errs = []; function end (err) { count -= 1; if (err) errs.push(err); if (count === 0) { safeApply(callback, that, [errs.length > 1 ? errs : errs[0]]); } } function out () { if (options.write) { count += 1; that.write(options.write, end); } if (options.uglify) { count += 1; that.uglify(options.uglify, end); } if (count === 0) { safeApply(callback, that); } } if (!err) { out(); if (options.watch || options.watch === 0) { this.watch(options.watch, function () { callback = options.watcher; count = 2; out(); }); } } else { safeApply(callback, this, [err]); } }); } else { safeApply(callback, this, [err]); } }, this.from, this.name); } return this; }; obj.reset = function (options, callback) { delete this.lastMain; return this.unwatch().initModulator(options || this, callback); }; obj.forget = function () { this.source = ''; this.output = []; this.memory = {}; delete this.main; return this; }; obj.require = function (name, callback, from) { from = from || this.from; this.log('require', [name, from]); if (('./').indexOf(name.charAt(0)) !== -1) { this.requireFileOrDirectory(from + '/' + name, callback); } else { this.requireNodeModule(name, callback, from); } return this; }; obj.requireFile = function (file, callback) { this.log('requireFile', file); if (!this.main) { this.main = this.removeFileExt(path.relative(this.from, file)); } var that = this; if (this.memory[file]) { safeApply(callback, this); } else { fs.readFile(file, 'utf8', function (err, source) { if (err || that.memory[file]) { safeApply(callback, that, [err]); } else { source = source || ''; that.output.push({file: file, source: source}); that.rememberFile(file); that.requireRequirments( path.dirname(file), path.basename(file), source, callback ); } }); } return this; }; obj.requireDirectory = function (dir, callback) { if (this.memory[dir]) { safeApply(callback, this); return this; } this.log('requireDirectory', dir); var that = this, pack = dir + '/package.json', main = '/index'; function end (err) { var src; if (!err) { src = 'module.exports = require(".' + path.normalize('/' + that.removeFileExt(main)) + '");'; that.memory[dir] = true; that.output.push({ dir: true, file: dir + '/', source: src }); } safeApply(callback, that, [err]); } path.exists(pack, function (exists) { if (exists) { fs.readFile(pack, 'utf8', function (err, data) { if (err) { safeApply(callback, that, [err]); } else try { data = JSON.parse(data); if (data.main) { main = '/' + data.main; that.requireFileOrDirectory(dir + main, end); } else { that.requireFileOrDirectory(dir + main, end); } } catch (jsonErr) { safeApply(callback, that, [ new Error(jsonErr + ' from ' + pack) ]); } }); } else { safeApply(callback, that, [that.missingFile(dir)]); } }); return this; }; obj.requireFileOrDirectory = function (name, callback) { this.log('requireFileOrDirectory', name); var clean = this.removeFileExt(path.resolve(name)), file = this.memory[clean + '.js'], that = this; function end () { that.log('end requireFileOrDirectory', name); path.exists(clean, function (exists) { if (exists) { fs.stat(clean, function (err, stat) { if (err) { safeApply(callback, that, [err]); } else if (stat.isDirectory()) { that.requireDirectory(clean, callback); } }); } else { safeApply(callback, that, [that.missingFile(clean)]); } }); } if (file) { safeApply(callback, this); } else if (name.charAt(name.length - 1) !== '/') { path.exists(clean + '.js', function (exists) { if (exists) { that.requireFile(clean + '.js', callback); } else { end(); } }); } else { end(); } return this; }; obj.requireNodeModule = function (name, callback, from) { this.log('requireNodeModule', [name, from]); if (this.memory[name]) { safeApply(callback, this); return this; } var that = this, list = this.listModuleDirectories(from || this.from), count = list.length, found; function end (err) { if (count < 1 || found) return; that.log('end requireNodeModule', [name, from]); if (!err) found = true; else if (!err.missingFile && !err.missongModule) { count = 1; } count -= 1; if (found || count === 0) { if (err && err.missingFile) { err = new Error('Missing module.'); err.missingModule = name; } safeApply(callback, that, [err]); } } if (count) { this.memory[name] = true; list.forEach(function (dir) { var f = dir + '/' + name; that.requireFileOrDirectory(f, end, name); }); } else { safeApply(callback, that, ['Missing module.']); } return this; }; obj.requireRequirments = function (from, name, source, callback) { this.log('requireRequirements', [from, name]); var count = 0, needs = this.listNeededRequirements(source), that = this; function end (err) { if (count < 1) return; that.log('end requireRequirements', [from, name]); count -= 1; if (err && (!err.missingFile || err.missingModule)) { count = 0; } if (count === 0) { safeApply(callback, that, [err]); } } if (needs && needs.length) { count = needs.length; needs.forEach(function (_name) { that.require(_name, end, from); }); } else { count = 1; end(); } return this; }; obj.listModuleDirectories = function (from, callback) { this.log('listModuleDirectories', from); var parts = from.split('/'), root = parts.indexOf('node_modules'), dirs = [], i = parts.length - 1; if (root === -1) root = 0; while (i >= root) { if (parts[i] === 'node_modules') { dirs.push(parts.join('/')); } else if (parts.length) { dirs.push(parts.join('/') + '/node_modules'); parts.pop(); } i -= 1; } return dirs; }; obj.listNeededRequirements = function (source) { this.log('listNeededRequirements'); var tokens = new JSLexer().tokenize(source).tokens, needs = []; if (tokens && tokens.length) { tokens.forEach(function (token, ind) { var last, next, i, l; if (token.type === 'identity' && token.text === 'require') { i = ind; do { last = i && tokens[i -= 1]; } while (last && last.type === 'white'); if (!last || last.text !== '.') { i = ind; l = tokens.length; do { next = i < l && tokens[i += 1]; } while (next && next.type === 'white'); if (next.text === '(') { do { next = i < l && tokens[i += 1]; } while (next && next.type === 'white'); if (next.type === 'string') { needs.push(next.text); } } } } }); } return needs; }; obj.discoverModuleAPI = function (callback) { var that = this; function end (err) { if (err || (api && out)) { safeApply(callback, that, [err, api, out]); } } function alt (err, data) { api = data; end(err); } if (api && out) { end(); } else { fs.readFile(__dirname + '/../src/api.js', 'utf8', function (err, data) { api = data; end(err); }); fs.readFile(__dirname + '/../src/out.js', 'utf8', function (err, data) { out = data; end(err); }); } return this; }; obj.rememberFile = function (file) { this.log('rememberFile', file); this.memory[file] = this.memory[this.removeFileExt(file)] = true; return this; }; obj.missingFile = function (file) { this.log('missingFile', file); var err = new Error('Missing file.'); err.missingFile = file; return err; }; obj.removeFileExt = function (file) { return path.dirname(file) + '/' + path.basename(file, '.js'); }; obj.compileFile = function (item) { this.log('compileFile', item.file); var file = path.normalize( '/' + this.removeFileExt(path.relative(this.from, item.file)) ); if (item.dir && file !== '/') { file += '/'; } return ([ this.apiName + '.provide("' + file + '", function' + ' (require, module, exports) {', item.source, '});' ]).join('\n'); }; obj.compileOutput = function () { this.log('compileOutput'); var that = this, source = []; this.output.forEach(function (item) { source.push(that.compileFile(item)); }); return source.join('\n'); }; obj.compile = function (callback) { this.log('compile'); if (this.source) { safeApply(callback, this); } else { this.discoverModuleAPI(function (err, api, out) { var source = '', name = this.name; if ((/[^a-z0-9_$]/i).test(name)) { name = '["' + name + '"]'; } else { name = '.' + name; } out = out. replace(/\(api\)/g, this.apiName). replace(/\(name\)/g, name). replace(/\(main\)/g, path.normalize('/' + this.main)); if (this.header) { source += this.header + '\n'; } source += '(function()' + '{\n'; source += 'var ' + this.apiName + ' = ' + api + '\n'; source += this.compileOutput() + '\n'; source += out + '\n'; source += '}).call(this);'; if (this.footer) { source += '\n' + this.footer; } this.source = source; safeApply(callback, this, [err]); }); } return this; }; obj.write = function (file, callback) { var that = this; function end (err) { safeApply(callback, that, [err]); } if (this.source) { fs.writeFile(file, this.source, 'utf8', end); } else { end('Empty source.'); } return this; }; obj.uglify = function (file, callback) { var that = this, src = this.source, ast; function end (err) { safeApply(callback, that, [err]); } if (src) { ast = ujs.parser.parse(src); ast = ujs.uglify.ast_mangle(ast); ast = ujs.uglify.ast_squeeze(ast); src = ujs.uglify.gen_code(ast); if (this.header) { src = this.header + '\n' + src; } fs.writeFile(file, src, 'utf8', end); } else { end('Empty source.'); } return this; }; obj.watch = function (options, callback) { var that = this, name, wait, watch, files = this.watchers, persist; options = options || {}; callback = callback || options.callback; delete options.callback; if (isFinite(options)) { watch = options; options = {}; } else { watch = options.interval; } wait = options.wait; wait = isFinite(wait) ? wait : 100; watch = isFinite(watch) ? watch : 0; persist = options.persist; if (!files) { files = this.watchers = []; } for (name in this.memory) { if (this.memory[name] === true && name.substr(name.length - 3) === '.js') { if (files.indexOf(name) === -1) { files.push(name); } } } files.forEach(function (file) { var timer; fs.watchFile(file, { persistent: persist, interval: watch }, function (curr, prev) { if (curr.mtime > prev.mtime) { clearTimeout(timer); timer = setTimeout(function () { that.forget().run({main: that.lastMain}, function (err) { safeApply(callback, that, [err]); }); }, wait); } }); }); return this; }; obj.unwatch = function () { var files = this.watchers; if (files) { files.forEach(function (file) { fs.unwatchFile(file); }); delete this.watchers; } return this; }; obj.log = function () { if (this.verbose) console.log.apply(console, arguments); return this; }; if (args) obj.initModulator.apply(obj, args); return obj; }; (module.exports = Modulator).create(modulator); }).call(this);