var fs = require('fs') var rpj = require('read-package-json') var path = require('path') var dz = require('dezalgo') var once = require('once') var readdir = require('readdir-scoped-modules') var debug = require('debuglog')('rpt') function asyncForEach (items, todo, done) { var remaining = items.length if (remaining === 0) return done() var seenErr items.forEach(function (item) { todo(item, handleComplete) }) function handleComplete (err) { if (seenErr) return if (err) { seenErr = true return done(err) } if (--remaining === 0) done() } } function dpath (p) { if (!p) return '' if (p.indexOf(process.cwd()) === 0) { p = p.substr(process.cwd().length + 1) } return p } module.exports = rpt rpt.Node = Node rpt.Link = Link var ID = 0 function Node (pkg, logical, physical, er, cache, fromLink) { if (!(this instanceof Node)) { return new Node(pkg, logical, physical, er, cache) } var node = cache[physical] || this if (fromLink && cache[physical]) return cache[physical] debug(node.constructor.name, dpath(physical), pkg && pkg._id) const parent = path.dirname(logical) if (parent[0] === '@') { node.name = path.basename(parent) + '/' + path.basename(logical) } else { node.name = path.basename(logical) } node.path = logical node.realpath = physical node.error = er if (!cache[physical]) { node.id = ID++ node.package = pkg || {} node.parent = null node.isLink = false node.children = [] } return cache[physical] = node } Node.prototype.package = null Node.prototype.path = '' Node.prototype.realpath = '' Node.prototype.children = null Node.prototype.error = null function Link (pkg, logical, physical, realpath, er, cache) { if (cache[physical]) return cache[physical] if (!(this instanceof Link)) { return new Link(pkg, logical, physical, realpath, er, cache) } cache[physical] = this debug(this.constructor.name, dpath(physical), pkg && pkg._id) const dir = path.dirname(logical) const parent = path.dirname(dir) if (parent[0] === '@') { this.name = path.basename(parent) + '/' + path.basename(dir) } else { this.name = path.basename(dir) } this.id = ID++ this.path = logical this.realpath = realpath this.package = pkg || {} this.parent = null this.target = new Node(this.package, logical, realpath, er, cache, true) this.isLink = true this.children = this.target.children this.error = er } Link.prototype = Object.create(Node.prototype, { constructor: { value: Link } }) Link.prototype.target = null Link.prototype.realpath = '' function loadNode (logical, physical, cache, cb) { debug('loadNode', dpath(logical)) return fs.realpath(physical, thenReadPackageJson) var realpath function thenReadPackageJson (er, real) { if (er) { var node = new Node(null, logical, physical, er, cache) return cb(null, node) } debug('realpath l=%j p=%j real=%j', dpath(logical), dpath(physical), dpath(real)) var pj = path.join(real, 'package.json') realpath = real return rpj(pj, thenCreateNode) } function thenCreateNode (er, pkg) { pkg = pkg || null var node if (physical === realpath) { node = new Node(pkg, logical, physical, er, cache) } else { node = new Link(pkg, logical, physical, realpath, er, cache) } cb(null, node) } } function loadChildren (node, cache, filterWith, cb) { debug('loadChildren', dpath(node.path)) // needed 'cause we process all kids async-like and errors // short circuit, so we have to be sure that after an error // the cbs from other kids don't result in calling cb a second // (or more) time. cb = once(cb) var nm = path.join(node.path, 'node_modules') var rm return fs.realpath(path.join(node.path, 'node_modules'), thenReaddir) function thenReaddir (er, real_nm) { if (er) return cb(null, node) rm = real_nm readdir(nm, thenLoadKids) } function thenLoadKids (er, kids) { // If there are no children, that's fine, just return if (er) return cb(null, node) kids = kids.filter(function (kid) { return kid[0] !== '.' && (!filterWith || filterWith(node, kid)) }) asyncForEach(kids, thenLoadNode, thenSortChildren) } function thenLoadNode (kid, done) { var kidPath = path.join(nm, kid) var kidRealPath = path.join(rm, kid) loadNode(kidPath, kidRealPath, cache, andAddNode(done)) } function andAddNode (done) { return function (er, kid) { if (er) return done(er) node.children.push(kid) kid.parent = node done() } } function thenSortChildren (er) { sortChildren(node) cb(er, node) } } function sortChildren (node) { node.children = node.children.sort(function (a, b) { a = a.package.name ? a.package.name.toLowerCase() : a.path b = b.package.name ? b.package.name.toLowerCase() : b.path return a > b ? 1 : -1 }) } function loadTree (node, did, cache, filterWith, cb) { debug('loadTree', dpath(node.path), !!cache[node.path]) if (did[node.realpath]) { return dz(cb)(null, node) } did[node.realpath] = true // needed 'cause we process all kids async-like and errors // short circuit, so we have to be sure that after an error // the cbs from other kids don't result in calling cb a second // (or more) time. cb = once(cb) return loadChildren(node, cache, filterWith, thenProcessChildren) function thenProcessChildren (er, node) { if (er) return cb(er) var kids = node.children.filter(function (kid) { return !did[kid.realpath] }) return asyncForEach(kids, loadTreeForKid, cb) } function loadTreeForKid (kid, done) { loadTree(kid, did, cache, filterWith, done) } } function rpt (root, filterWith, cb) { if (!cb) { cb = filterWith filterWith = null } var cache = Object.create(null) var topErr var tree return fs.realpath(root, thenLoadNode) function thenLoadNode (er, realRoot) { if (er) return cb(er) debug('rpt', dpath(realRoot)) loadNode(root, realRoot, cache, thenLoadTree) } function thenLoadTree(er, node) { // even if there's an error, it's fine, as long as we got a node if (node) { topErr = er tree = node loadTree(node, {}, cache, filterWith, thenHandleErrors) } else { cb(er) } } function thenHandleErrors (er) { cb(topErr && topErr.code !== 'ENOENT' ? topErr : er, tree) } }