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