• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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