UNPKG

modules

Version:
338 lines (277 loc) 8.87 kB
/** * Provides wrapping of modules for use in the browser. **/ 'use strict'; var fs = require('fs'), extexp = /\.([^.\\\/]+)$/; // matches file extension /** * The default options used **/ exports.defaults = { encoding: 'utf8', nowrap: [ /\.amd\.js$/i ], root: process.cwd() }; /** * Turn a relative id to absolute given a parent id **/ function resolve(parent, id) { if (/^\.\.?\//.test(id) && parent) { // is a relative id id = parent.replace(/[^\/]+$/, id); // prepend parent's dirname } var terms = []; id.split('/').forEach(function(term) { if ('..' === term) { terms.pop(); } // remove previous, don't add .. else if ('.' !== term) { terms.push(term); } // add term // else if ('.' === term) // ignore . }); return terms.join('/'); } /** * Convert arbitrary file to commonjs+return exports **/ function translate(id, filename, buffer, options, next) { var ext = (filename.match(extexp) || [])[1] || '', encoding = options.encoding || exports.defaults.encoding, trans = options.translate, js, nowrap; // make a list of what not to wrap nowrap = options.nowrap || exports.defaults.nowrap; nowrap = [ 'define', 'define.min', 'define.shim' ].concat(nowrap); // should this code get wrapped with a define? nowrap = nowrap.some(function(no) { return no.test ? no.test(id) : (no === id); }); // convert commonjs to amd function wrap(err, js) { if (err) { return next(err); } var deps = exports.dependencies(id, js), params = [], undef = ''; // make sure require, exports, and module are properly passed into the factory if (/\brequire\b/.test(js)) { params.push('require'); } if (/\bexports\b/.test(js)) { params.push('exports'); } if (/\bmodule\b/.test(js)) { params.push('module'); } // make sure code follows the `exports` path instead of `define` path once wrapped if (/\bdefine\.amd\b/.test(js)) { undef = 'var define;'; } if (deps.length) { deps = ',' + JSON.stringify(params.concat(deps)); } else if (params.length) { params = [ 'require', 'exports', 'module' ]; } js = 'define(' + JSON.stringify(id) + deps + ',function(' + params + ')' + '{' + js + '\n' + undef + '}' + // rely on hoisting for define ');\n'; next(null, js); } // find the translate function trans = trans && (trans[filename] || trans[id] || trans[ext]); if (trans) { // user configured translation return trans( { id:id, filename:filename, buffer:buffer }, options, (nowrap ? next : wrap) ); } js = buffer.toString(encoding); // handle javascript files if ('js' === ext) { return (nowrap ? next : wrap)(null, js); } // handle non-javascript files if ('json' !== ext) { js = JSON.stringify(js); } // export file as json string if (nowrap) { return next(null, 'return ' + js); } return next(null, 'define(' + JSON.stringify(id) + ',' + js + ');\n'); } /** * convert module id to filename **/ function getFilename(id, options, next) { var Path = require('path'), root = options.root || exports.defaults.root, map = options.map || {}, forbid = (options.forbid || []).map(function(forbid) { return forbid.test ? forbid : Path.resolve(root, forbid); }), filename; function test(forbid) { return forbid.test ? forbid.test(filename) : (filename.slice(0, forbid.length) === forbid); // filename starts with forbid } // mapped modules can be in forbidden places. (define and define.shim should be mapped) map.define = map.define || Path.resolve(__dirname, 'define'); map['define.min'] = map['define.min'] || Path.resolve(__dirname, 'define.min'); map['define.shim'] = map['define.shim'] || Path.resolve(__dirname, 'define.shim'); // get a filename from the id filename = map[id] || id; // look for the file in the id map if ('function' === typeof filename) { filename = filename(id, options); } // if function use result filename = Path.resolve(root, filename); // relative to root if (!map[id]) { // anything below options.root is forbidden if ('..' === Path.relative(root, filename).slice(0, 2) || forbid.some(test)) { return next(new Error('Forbidden'), ''); } } fs.stat(filename, function(err, stats) { if (err) { // not found without .js, try with return fs.exists(filename + '.js', function(exists) { return next(null, filename + (exists ? '.js' : '')); }); } if (stats.isDirectory()) { // directories look for /index.js return fs.exists(filename + '/index.js', function(exists) { return next(null, filename + (exists ? '/index.js' : '')); }); } return next(null, filename); }); } /** * find all requires with string literal ids **/ exports.dependencies = function dependencies(id, js, options) { var comments = /\/\/[^\r\n]*/g, // single line comments // mcomments = /\/\*[\s\S]*?\*\//g, // multi line comments /* reqexp = /\brequire\s*\(\s*(['"]).+?\1\s*\)/g, // find require calls idexp = /(['"])(.+?)\1/, // find literal identifier in require call rid, aid, abs = [], rel = [], // find all require calls after removing comments matches = js .replace(comments, '') .replace(mcomments, '') .match(reqexp), m, mm = matches && matches.length; for (m = 0; m < mm; ++m) { // resolve to absolute id aid = resolve(id, rid = matches[m].match(idexp)[2]); if (abs.indexOf(aid) < 0) { // keep both relative and absolute ids abs.push(aid); rel.push(rid); } } // return either relative or absolute ids return (options && options.absolute) ? abs : rel; }; /** * Prints the code for the module, including define wrapper code necessary in the browser. **/ exports.module = function module(id, options, next) { // options is an optional parameter if (!next) { next = options; options = {}; } // remove .js suffix if (id.slice(-3) === '.js') { id = id.slice(0, -3); } var filename, stat, mod; getFilename(id, options, getStat); function getStat(err, mfilename) { if (err) { return next(err); } filename = mfilename; fs.stat(filename, getFile); } function getFile(err, mstat) { if (err) { return next(err); } stat = mstat; fs.readFile(filename, getCode); } function getCode(err, buffer) { if (err) { return next(err); } translate(id, filename, buffer, options, compressCode); } function compressCode(err, js) { if (err) { return next(err); } // create module object mod = { id: id, filename: filename, code: js, modified: (stat && stat.mtime) }; if (options.compress) { options.compress(mod, done); } else { done(null, js); } } function done(err, js) { if (err) { return next(err); } mod.code = (js.code || js); next(null, mod); } }; /** * Prints the code for the modules **/ exports.modules = function modules(ids, options, next) { // options is an optional parameter if (!next) { next = options; options = {}; } var results = [], doneCount = 0, compress = options.compress, modified; // don't compress each individually options.compress = null; // loop through all the modules ids.forEach(moduleToCode); if (!ids.length) { process.nextTick(alldone); } // convert a module function moduleToCode(id, r) { exports.module(id, options, done); function done(err, result) { if (err) { next(err); next = function(){}; // make sure you only call next(err) once return; } if (result && (!modified || result.modified > modified)) { modified = result.modified; // update latest last modified date } results[r] = result && result.code || ''; if (++doneCount >= ids.length) { alldone(); } // done with all of them } } // put it all together function alldone() { // combine and compress modules var module = { code: results.join(''), modified: modified }; // no compressing? done. if (!compress) { return next(null, module); } compress(module, function(err, js) { module.code = js.code || js; next(err, err ? null : module); }); } }; /** * Provides middleware to format module code for use in the browser. **/ exports.middleware = function middleware(options) { // string passed means root directory if ('string' === typeof options) { options = { root:options }; } options = options || {}; var maxage = options.maxAge; // return middleware function return function(req, res, next) { var id = req.path.replace(/^\/+/, ''); // remove leading slashes exports.module(id, options, send); function send(err, module) { if (err) { // if file not found, let other handlers try if ('ENOENT' === err.code) { return next(); } return next(err); } // send proper headers res.set('Content-Type', 'application/javascript'); res.set('Last-Modified', module.modified.toGMTString()); if (maxage || 0 === maxage) { res.set('Cache-Control', 'public, max-age=' + maxage); } // send javascript code res.send(module.code); } }; };