UNPKG

6.01 kBJavaScriptView Raw
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