| var fs = require('fs'); |
| var path = require('path'); |
| var relativePath = require('cached-path-relative') |
| |
| var browserResolve = require('browser-resolve'); |
| var nodeResolve = require('resolve'); |
| var detective = require('detective'); |
| var through = require('through2'); |
| var concat = require('concat-stream'); |
| var parents = require('parents'); |
| var combine = require('stream-combiner2'); |
| var duplexer = require('duplexer2'); |
| var xtend = require('xtend'); |
| var defined = require('defined'); |
| |
| var inherits = require('inherits'); |
| var Transform = require('readable-stream').Transform; |
| |
| module.exports = Deps; |
| inherits(Deps, Transform); |
| |
| function Deps (opts) { |
| var self = this; |
| if (!(this instanceof Deps)) return new Deps(opts); |
| Transform.call(this, { objectMode: true }); |
| |
| if (!opts) opts = {}; |
| |
| this.basedir = opts.basedir || process.cwd(); |
| this.persistentCache = opts.persistentCache || function (file, id, pkg, fallback, cb) { |
| process.nextTick(function () { |
| fallback(null, cb); |
| }); |
| }; |
| this.cache = opts.cache; |
| this.fileCache = opts.fileCache; |
| this.pkgCache = opts.packageCache || {}; |
| this.pkgFileCache = {}; |
| this.pkgFileCachePending = {}; |
| this._emittedPkg = {}; |
| this.visited = {}; |
| this.walking = {}; |
| this.entries = []; |
| this._input = []; |
| |
| this.paths = opts.paths || process.env.NODE_PATH || ''; |
| if (typeof this.paths === 'string') { |
| var delimiter = path.delimiter || (process.platform === 'win32' ? ';' : ':'); |
| this.paths = this.paths.split(delimiter); |
| } |
| this.paths = this.paths |
| .filter(Boolean) |
| .map(function (p) { |
| return path.resolve(self.basedir, p); |
| }); |
| |
| this.transforms = [].concat(opts.transform).filter(Boolean); |
| this.globalTransforms = [].concat(opts.globalTransform).filter(Boolean); |
| this.resolver = opts.resolve || browserResolve; |
| this.options = xtend(opts); |
| if (!this.options.modules) this.options.modules = {}; |
| |
| // If the caller passes options.expose, store resolved pathnames for exposed |
| // modules in it. If not, set it anyway so it's defined later. |
| if (!this.options.expose) this.options.expose = {}; |
| this.pending = 0; |
| this.inputPending = 0; |
| |
| var topfile = path.join(this.basedir, '__fake.js'); |
| this.top = { |
| id: topfile, |
| filename: topfile, |
| paths: this.paths, |
| basedir: this.basedir |
| }; |
| } |
| |
| Deps.prototype._isTopLevel = function (file) { |
| var isTopLevel = this.entries.some(function (main) { |
| var m = relativePath(path.dirname(main), file); |
| return m.split(/[\\\/]/).indexOf('node_modules') < 0; |
| }); |
| if (!isTopLevel) { |
| var m = relativePath(this.basedir, file); |
| isTopLevel = m.split(/[\\\/]/).indexOf('node_modules') < 0; |
| } |
| return isTopLevel; |
| }; |
| |
| Deps.prototype._transform = function (row, enc, next) { |
| var self = this; |
| if (typeof row === 'string') { |
| row = { file: row }; |
| } |
| if (row.transform && row.global) { |
| this.globalTransforms.push([ row.transform, row.options ]); |
| return next(); |
| } |
| else if (row.transform) { |
| this.transforms.push([ row.transform, row.options ]); |
| return next(); |
| } |
| |
| self.pending ++; |
| var basedir = defined(row.basedir, self.basedir); |
| |
| if (row.entry !== false) { |
| self.entries.push(path.resolve(basedir, row.file || row.id)); |
| } |
| |
| self.lookupPackage(row.file, function (err, pkg) { |
| if (err && self.options.ignoreMissing) { |
| self.emit('missing', row.file, self.top); |
| self.pending --; |
| return next(); |
| } |
| if (err) return self.emit('error', err) |
| self.pending --; |
| self._input.push({ row: row, pkg: pkg }); |
| next(); |
| }); |
| }; |
| |
| Deps.prototype._flush = function () { |
| var self = this; |
| var files = {}; |
| self._input.forEach(function (r) { |
| var w = r.row, f = files[w.file || w.id]; |
| if (f) { |
| f.row.entry = f.row.entry || w.entry; |
| var ex = f.row.expose || w.expose; |
| f.row.expose = ex; |
| if (ex && f.row.file === f.row.id && w.file !== w.id) { |
| f.row.id = w.id; |
| } |
| } |
| else files[w.file || w.id] = r; |
| }); |
| |
| Object.keys(files).forEach(function (key) { |
| var r = files[key]; |
| var pkg = r.pkg || {}; |
| var dir = r.row.file ? path.dirname(r.row.file) : self.basedir; |
| if (!pkg.__dirname) pkg.__dirname = dir; |
| self.walk(r.row, xtend(self.top, { |
| filename: path.join(dir, '_fake.js') |
| })); |
| }); |
| if (this.pending === 0) this.push(null); |
| this._ended = true; |
| }; |
| |
| Deps.prototype.resolve = function (id, parent, cb) { |
| var self = this; |
| var opts = self.options; |
| |
| if (xhas(self.cache, parent.id, 'deps', id) |
| && self.cache[parent.id].deps[id]) { |
| var file = self.cache[parent.id].deps[id]; |
| var pkg = self.pkgCache[file]; |
| if (pkg) return cb(null, file, pkg); |
| return self.lookupPackage(file, function (err, pkg) { |
| cb(null, file, pkg); |
| }); |
| } |
| |
| parent.packageFilter = function (p, x) { |
| var pkgdir = path.dirname(x); |
| if (opts.packageFilter) p = opts.packageFilter(p, x); |
| p.__dirname = pkgdir; |
| |
| return p; |
| }; |
| |
| if (opts.extensions) parent.extensions = opts.extensions; |
| if (opts.modules) parent.modules = opts.modules; |
| |
| self.resolver(id, parent, function onresolve (err, file, pkg, fakePath) { |
| if (err) return cb(err); |
| if (!file) return cb(new Error( |
| 'module not found: "' + id + '" from file ' |
| + parent.filename |
| )); |
| |
| if (!pkg || !pkg.__dirname) { |
| self.lookupPackage(file, function (err, p) { |
| if (err) return cb(err); |
| if (!p) p = {}; |
| if (!p.__dirname) p.__dirname = path.dirname(file); |
| self.pkgCache[file] = p; |
| onresolve(err, file, opts.packageFilter |
| ? opts.packageFilter(p, p.__dirname) : p, |
| fakePath |
| ); |
| }); |
| } |
| else cb(err, file, pkg, fakePath); |
| }); |
| }; |
| |
| Deps.prototype.readFile = function (file, id, pkg) { |
| var self = this; |
| if (xhas(this.fileCache, file)) { |
| return toStream(this.fileCache[file]); |
| } |
| var rs = fs.createReadStream(file, { |
| encoding: 'utf8' |
| }); |
| rs.on('error', function (err) { self.emit('error', err) }); |
| this.emit('file', file, id); |
| return rs; |
| }; |
| |
| Deps.prototype.getTransforms = function (file, pkg, opts) { |
| if (!opts) opts = {}; |
| var self = this; |
| |
| var isTopLevel; |
| if (opts.builtin || opts.inNodeModules) isTopLevel = false; |
| else isTopLevel = this._isTopLevel(file); |
| |
| var transforms = [].concat(isTopLevel ? this.transforms : []) |
| .concat(getTransforms(pkg, { |
| globalTransform: this.globalTransforms, |
| transformKey: this.options.transformKey |
| })) |
| ; |
| if (transforms.length === 0) return through(); |
| |
| var pending = transforms.length; |
| var streams = []; |
| var input = through(); |
| var output = through(); |
| var dup = duplexer(input, output); |
| |
| for (var i = 0; i < transforms.length; i++) (function (i) { |
| makeTransform(transforms[i], function (err, trs) { |
| if (err) return self.emit('error', err) |
| streams[i] = trs; |
| if (-- pending === 0) done(); |
| }); |
| })(i); |
| return dup; |
| |
| function done () { |
| var middle = combine.apply(null, streams); |
| middle.on('error', function (err) { |
| err.message += ' while parsing file: ' + file; |
| if (!err.filename) err.filename = file; |
| self.emit('error', err); |
| }); |
| input.pipe(middle).pipe(output); |
| } |
| |
| function makeTransform (tr, cb) { |
| var trOpts = {}; |
| if (Array.isArray(tr)) { |
| trOpts = tr[1] || {}; |
| tr = tr[0]; |
| } |
| trOpts._flags = trOpts.hasOwnProperty('_flags') ? trOpts._flags : self.options; |
| if (typeof tr === 'function') { |
| var t = tr(file, trOpts); |
| self.emit('transform', t, file); |
| nextTick(cb, null, wrapTransform(t)); |
| } |
| else { |
| loadTransform(tr, trOpts, function (err, trs) { |
| if (err) return cb(err); |
| cb(null, wrapTransform(trs)); |
| }); |
| } |
| } |
| |
| function loadTransform (id, trOpts, cb) { |
| var params = { basedir: path.dirname(file) }; |
| nodeResolve(id, params, function nr (err, res, again) { |
| if (err && again) return cb && cb(err); |
| |
| if (err) { |
| params.basedir = pkg.__dirname; |
| return nodeResolve(id, params, function (e, r) { |
| nr(e, r, true) |
| }); |
| } |
| |
| if (!res) return cb(new Error( |
| 'cannot find transform module ' + tr |
| + ' while transforming ' + file |
| )); |
| |
| var r = require(res); |
| if (typeof r !== 'function') { |
| return cb(new Error( |
| 'Unexpected ' + typeof r + ' exported by the ' |
| + JSON.stringify(res) + ' package. ' |
| + 'Expected a transform function.' |
| )); |
| } |
| |
| var trs = r(file, trOpts); |
| self.emit('transform', trs, file); |
| cb(null, trs); |
| }); |
| } |
| }; |
| |
| Deps.prototype.walk = function (id, parent, cb) { |
| var self = this; |
| var opts = self.options; |
| this.pending ++; |
| |
| var rec = {}; |
| var input; |
| if (typeof id === 'object') { |
| rec = xtend(id); |
| if (rec.entry === false) delete rec.entry; |
| id = rec.file || rec.id; |
| input = true; |
| this.inputPending ++; |
| } |
| |
| self.resolve(id, parent, function (err, file, pkg, fakePath) { |
| // this is checked early because parent.modules is also modified |
| // by this function. |
| var builtin = has(parent.modules, id); |
| |
| if (rec.expose) { |
| // Set options.expose to make the resolved pathname available to the |
| // caller. They may or may not have requested it, but it's harmless |
| // to set this if they didn't. |
| self.options.expose[rec.expose] = |
| self.options.modules[rec.expose] = file; |
| } |
| if (pkg && !self._emittedPkg[pkg.__dirname]) { |
| self._emittedPkg[pkg.__dirname] = true; |
| self.emit('package', pkg); |
| } |
| |
| if (opts.postFilter && !opts.postFilter(id, file, pkg)) { |
| if (--self.pending === 0) self.push(null); |
| if (input) --self.inputPending; |
| return cb && cb(null, undefined); |
| } |
| if (err && rec.source) { |
| file = rec.file; |
| |
| var ts = self.getTransforms(file, pkg); |
| ts.pipe(concat(function (body) { |
| rec.source = body.toString('utf8'); |
| fromSource(file, rec.source, pkg); |
| })); |
| return ts.end(rec.source); |
| } |
| if (err && self.options.ignoreMissing) { |
| if (--self.pending === 0) self.push(null); |
| if (input) --self.inputPending; |
| self.emit('missing', id, parent); |
| return cb && cb(null, undefined); |
| } |
| if (err) return self.emit('error', err); |
| if (self.visited[file]) { |
| if (-- self.pending === 0) self.push(null); |
| if (input) --self.inputPending; |
| return cb && cb(null, file); |
| } |
| self.visited[file] = true; |
| |
| if (rec.source) { |
| var ts = self.getTransforms(file, pkg); |
| ts.pipe(concat(function (body) { |
| rec.source = body.toString('utf8'); |
| fromSource(file, rec.source, pkg); |
| })); |
| return ts.end(rec.source); |
| } |
| |
| var c = self.cache && self.cache[file]; |
| if (c) return fromDeps(file, c.source, c.package, fakePath, Object.keys(c.deps)); |
| |
| self.persistentCache(file, id, pkg, persistentCacheFallback, function (err, c) { |
| if (err) { |
| self.emit('error', err); |
| return; |
| } |
| fromDeps(file, c.source, c.package, fakePath, Object.keys(c.deps)); |
| }); |
| |
| function persistentCacheFallback (dataAsString, cb) { |
| var stream = dataAsString ? toStream(dataAsString) : self.readFile(file, id, pkg); |
| stream |
| .pipe(self.getTransforms(fakePath || file, pkg, { |
| builtin: builtin, |
| inNodeModules: parent.inNodeModules |
| })) |
| .pipe(concat(function (body) { |
| var src = body.toString('utf8'); |
| var deps = getDeps(file, src); |
| if (deps) { |
| cb(null, { |
| source: src, |
| package: pkg, |
| deps: deps.reduce(function (deps, dep) { |
| deps[dep] = true; |
| return deps; |
| }, {}) |
| }); |
| } |
| })); |
| } |
| }); |
| |
| function getDeps (file, src) { |
| return rec.noparse ? [] : self.parseDeps(file, src); |
| } |
| |
| function fromSource (file, src, pkg, fakePath) { |
| var deps = getDeps(file, src); |
| if (deps) fromDeps(file, src, pkg, fakePath, deps); |
| } |
| |
| function fromDeps (file, src, pkg, fakePath, deps) { |
| var p = deps.length; |
| var resolved = {}; |
| |
| if (input) --self.inputPending; |
| |
| (function resolve () { |
| if (self.inputPending > 0) return setTimeout(resolve); |
| deps.forEach(function (id) { |
| if (opts.filter && !opts.filter(id)) { |
| resolved[id] = false; |
| if (--p === 0) done(); |
| return; |
| } |
| var isTopLevel = self._isTopLevel(fakePath || file); |
| var current = { |
| id: file, |
| filename: file, |
| paths: self.paths, |
| package: pkg, |
| inNodeModules: parent.inNodeModules || !isTopLevel |
| }; |
| self.walk(id, current, function (err, r) { |
| resolved[id] = r; |
| if (--p === 0) done(); |
| }); |
| }); |
| if (deps.length === 0) done(); |
| })(); |
| |
| function done () { |
| if (!rec.id) rec.id = file; |
| if (!rec.source) rec.source = src; |
| if (!rec.deps) rec.deps = resolved; |
| if (!rec.file) rec.file = file; |
| |
| if (self.entries.indexOf(file) >= 0) { |
| rec.entry = true; |
| } |
| self.push(rec); |
| |
| if (cb) cb(null, file); |
| if (-- self.pending === 0) self.push(null); |
| } |
| } |
| }; |
| |
| Deps.prototype.parseDeps = function (file, src, cb) { |
| if (this.options.noParse === true) return []; |
| if (/\.json$/.test(file)) return []; |
| |
| if (Array.isArray(this.options.noParse) |
| && this.options.noParse.indexOf(file) >= 0) { |
| return []; |
| } |
| |
| try { var deps = detective(src) } |
| catch (ex) { |
| var message = ex && ex.message ? ex.message : ex; |
| this.emit('error', new Error( |
| 'Parsing file ' + file + ': ' + message |
| )); |
| return; |
| } |
| return deps; |
| }; |
| |
| Deps.prototype.lookupPackage = function (file, cb) { |
| var self = this; |
| |
| var cached = this.pkgCache[file]; |
| if (cached) return nextTick(cb, null, cached); |
| if (cached === false) return nextTick(cb, null, undefined); |
| |
| var dirs = parents(file ? path.dirname(file) : self.basedir); |
| |
| (function next () { |
| if (dirs.length === 0) { |
| self.pkgCache[file] = false; |
| return cb(null, undefined); |
| } |
| var dir = dirs.shift(); |
| if (dir.split(/[\\\/]/).slice(-1)[0] === 'node_modules') { |
| return cb(null, undefined); |
| } |
| |
| var pkgfile = path.join(dir, 'package.json'); |
| |
| var cached = self.pkgCache[pkgfile]; |
| if (cached) return nextTick(cb, null, cached); |
| else if (cached === false) return next(); |
| |
| var pcached = self.pkgFileCachePending[pkgfile]; |
| if (pcached) return pcached.push(onpkg); |
| pcached = self.pkgFileCachePending[pkgfile] = []; |
| |
| fs.readFile(pkgfile, function (err, src) { |
| if (err) return onpkg(); |
| try { var pkg = JSON.parse(src) } |
| catch (err) { |
| return onpkg(new Error([ |
| err + ' while parsing json file ' + pkgfile |
| ].join(''))) |
| } |
| pkg.__dirname = dir; |
| |
| self.pkgCache[pkgfile] = pkg; |
| self.pkgCache[file] = pkg; |
| onpkg(null, pkg); |
| }); |
| |
| function onpkg (err, pkg) { |
| if (self.pkgFileCachePending[pkgfile]) { |
| var fns = self.pkgFileCachePending[pkgfile]; |
| delete self.pkgFileCachePending[pkgfile]; |
| fns.forEach(function (f) { f(err, pkg) }); |
| } |
| if (err) cb(err) |
| else if (pkg) cb(null, pkg) |
| else { |
| self.pkgCache[pkgfile] = false; |
| next(); |
| } |
| } |
| })(); |
| }; |
| |
| function getTransforms (pkg, opts) { |
| var trx = []; |
| if (opts.transformKey) { |
| var n = pkg; |
| var keys = opts.transformKey; |
| for (var i = 0; i < keys.length; i++) { |
| if (n && typeof n === 'object') n = n[keys[i]]; |
| else break; |
| } |
| if (i === keys.length) { |
| trx = [].concat(n).filter(Boolean); |
| } |
| } |
| return trx.concat(opts.globalTransform || []); |
| } |
| |
| function nextTick (cb) { |
| var args = [].slice.call(arguments, 1); |
| process.nextTick(function () { cb.apply(null, args) }); |
| } |
| |
| function xhas (obj) { |
| if (!obj) return false; |
| for (var i = 1; i < arguments.length; i++) { |
| var key = arguments[i]; |
| if (!has(obj, key)) return false; |
| obj = obj[key]; |
| } |
| return true; |
| } |
| |
| function toStream (dataAsString) { |
| var tr = through(); |
| tr.push(dataAsString); |
| tr.push(null); |
| return tr; |
| } |
| |
| function has (obj, key) { |
| return obj && Object.prototype.hasOwnProperty.call(obj, key); |
| } |
| |
| function wrapTransform (tr) { |
| if (typeof tr.read === 'function') return tr; |
| var input = through(), output = through(); |
| input.pipe(tr).pipe(output); |
| var wrapper = duplexer(input, output); |
| tr.on('error', function (err) { wrapper.emit('error', err) }); |
| return wrapper; |
| } |