1const fs = require('fs') 2/* istanbul ignore next */ 3const promisify = require('util').promisify || require('util-promisify') 4const { resolve, basename, dirname, join } = require('path') 5const rpj = promisify(require('read-package-json')) 6const readdir = promisify(require('readdir-scoped-modules')) 7const realpath = require('./realpath.js') 8 9let ID = 0 10class Node { 11 constructor (pkg, logical, physical, er, cache) { 12 // should be impossible. 13 const cached = cache.get(physical) 14 /* istanbul ignore next */ 15 if (cached && !cached.then) 16 throw new Error('re-creating already instantiated node') 17 18 cache.set(physical, this) 19 20 const parent = basename(dirname(logical)) 21 if (parent.charAt(0) === '@') 22 this.name = `${parent}/${basename(logical)}` 23 else 24 this.name = basename(logical) 25 this.path = logical 26 this.realpath = physical 27 this.error = er 28 this.id = ID++ 29 this.package = pkg || {} 30 this.parent = null 31 this.isLink = false 32 this.children = [] 33 } 34} 35 36class Link extends Node { 37 constructor (pkg, logical, physical, realpath, er, cache) { 38 super(pkg, logical, physical, er, cache) 39 40 // if the target has started, but not completed, then 41 // a Promise will be in the cache to indicate this. 42 const cachedTarget = cache.get(realpath) 43 if (cachedTarget && cachedTarget.then) 44 cachedTarget.then(node => { 45 this.target = node 46 this.children = node.children 47 }) 48 49 this.target = cachedTarget || new Node(pkg, logical, realpath, er, cache) 50 this.realpath = realpath 51 this.isLink = true 52 this.error = er 53 this.children = this.target.children 54 } 55} 56 57// this is the way it is to expose a timing issue which is difficult to 58// test otherwise. The creation of a Node may take slightly longer than 59// the creation of a Link that targets it. If the Node has _begun_ its 60// creation phase (and put a Promise in the cache) then the Link will 61// get a Promise as its cachedTarget instead of an actual Node object. 62// This is not a problem, because it gets resolved prior to returning 63// the tree or attempting to load children. However, it IS remarkably 64// difficult to get to happen in a test environment to verify reliably. 65// Hence this kludge. 66const newNode = (pkg, logical, physical, er, cache) => 67 process.env._TEST_RPT_SLOW_LINK_TARGET_ === '1' 68 ? new Promise(res => setTimeout(() => 69 res(new Node(pkg, logical, physical, er, cache)), 10)) 70 : new Node(pkg, logical, physical, er, cache) 71 72const loadNode = (logical, physical, cache, rpcache, stcache) => { 73 // cache temporarily holds a promise placeholder so we 74 // don't try to create the same node multiple times. 75 // this is very rare to encounter, given the aggressive 76 // caching on fs.realpath and fs.lstat calls, but 77 // it can happen in theory. 78 const cached = cache.get(physical) 79 /* istanbul ignore next */ 80 if (cached) 81 return Promise.resolve(cached) 82 83 const p = realpath(physical, rpcache, stcache, 0).then(real => 84 rpj(join(real, 'package.json')) 85 .then(pkg => [pkg, null], er => [null, er]) 86 .then(([pkg, er]) => 87 physical === real ? newNode(pkg, logical, physical, er, cache) 88 : new Link(pkg, logical, physical, real, er, cache) 89 ), 90 // if the realpath fails, don't bother with the rest 91 er => new Node(null, logical, physical, er, cache)) 92 93 cache.set(physical, p) 94 return p 95} 96 97const loadChildren = (node, cache, filterWith, rpcache, stcache) => { 98 // if a Link target has started, but not completed, then 99 // a Promise will be in the cache to indicate this. 100 // 101 // XXX When we can one day loadChildren on the link *target* instead of 102 // the link itself, to match real dep resolution, then we may end up with 103 // a node target in the cache that isn't yet done resolving when we get 104 // here. For now, though, this line will never be reached, so it's hidden 105 // 106 // if (node.then) 107 // return node.then(node => loadChildren(node, cache, filterWith, rpcache, stcache)) 108 109 const nm = join(node.path, 'node_modules') 110 return realpath(nm, rpcache, stcache, 0) 111 .then(rm => readdir(rm).then(kids => [rm, kids])) 112 .then(([rm, kids]) => Promise.all( 113 kids.filter(kid => 114 kid.charAt(0) !== '.' && (!filterWith || filterWith(node, kid))) 115 .map(kid => loadNode(join(nm, kid), join(rm, kid), cache, rpcache, stcache))) 116 ).then(kidNodes => { 117 kidNodes.forEach(k => k.parent = node) 118 node.children.push.apply(node.children, kidNodes.sort((a, b) => 119 (a.package.name ? a.package.name.toLowerCase() : a.path) 120 .localeCompare( 121 (b.package.name ? b.package.name.toLowerCase() : b.path) 122 ))) 123 return node 124 }) 125 .catch(() => node) 126} 127 128const loadTree = (node, did, cache, filterWith, rpcache, stcache) => { 129 // impossible except in pathological ELOOP cases 130 /* istanbul ignore next */ 131 if (did.has(node.realpath)) 132 return Promise.resolve(node) 133 134 did.add(node.realpath) 135 136 // load children on the target, not the link 137 return loadChildren(node, cache, filterWith, rpcache, stcache) 138 .then(node => Promise.all( 139 node.children 140 .filter(kid => !did.has(kid.realpath)) 141 .map(kid => loadTree(kid, did, cache, filterWith, rpcache, stcache)) 142 )).then(() => node) 143} 144 145// XXX Drop filterWith and/or cb in next semver major bump 146const rpt = (root, filterWith, cb) => { 147 if (!cb && typeof filterWith === 'function') { 148 cb = filterWith 149 filterWith = null 150 } 151 152 const cache = new Map() 153 // we can assume that the cwd is real enough 154 const cwd = process.cwd() 155 const rpcache = new Map([[ cwd, cwd ]]) 156 const stcache = new Map() 157 const p = realpath(root, rpcache, stcache, 0) 158 .then(realRoot => loadNode(root, realRoot, cache, rpcache, stcache)) 159 .then(node => loadTree(node, new Set(), cache, filterWith, rpcache, stcache)) 160 161 if (typeof cb === 'function') 162 p.then(tree => cb(null, tree), cb) 163 164 return p 165} 166 167rpt.Node = Node 168rpt.Link = Link 169module.exports = rpt 170