rpt.js 5.9 KB

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