index.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. // to GET CONTENTS for folder at PATH (which may be a PACKAGE):
  2. // - if PACKAGE, read path/package.json
  3. // - if bins in ../node_modules/.bin, add those to result
  4. // - if depth >= maxDepth, add PATH to result, and finish
  5. // - readdir(PATH, with file types)
  6. // - add all FILEs in PATH to result
  7. // - if PARENT:
  8. // - if depth < maxDepth, add GET CONTENTS of all DIRs in PATH
  9. // - else, add all DIRs in PATH
  10. // - if no parent
  11. // - if no bundled deps,
  12. // - if depth < maxDepth, add GET CONTENTS of DIRs in path except
  13. // node_modules
  14. // - else, add all DIRs in path other than node_modules
  15. // - if has bundled deps,
  16. // - get list of bundled deps
  17. // - add GET CONTENTS of bundled deps, PACKAGE=true, depth + 1
  18. const bundled = require('npm-bundled')
  19. const {promisify} = require('util')
  20. const fs = require('fs')
  21. const readFile = promisify(fs.readFile)
  22. const readdir = promisify(fs.readdir)
  23. const stat = promisify(fs.stat)
  24. const lstat = promisify(fs.lstat)
  25. const {relative, resolve, basename, dirname} = require('path')
  26. const normalizePackageBin = require('npm-normalize-package-bin')
  27. const readPackage = ({ path, packageJsonCache }) =>
  28. packageJsonCache.has(path) ? Promise.resolve(packageJsonCache.get(path))
  29. : readFile(path).then(json => {
  30. const pkg = normalizePackageBin(JSON.parse(json))
  31. packageJsonCache.set(path, pkg)
  32. return pkg
  33. })
  34. .catch(er => null)
  35. // just normalize bundle deps and bin, that's all we care about here.
  36. const normalized = Symbol('package data has been normalized')
  37. const rpj = ({ path, packageJsonCache }) =>
  38. readPackage({path, packageJsonCache})
  39. .then(pkg => {
  40. if (!pkg || pkg[normalized])
  41. return pkg
  42. if (pkg.bundledDependencies && !pkg.bundleDependencies) {
  43. pkg.bundleDependencies = pkg.bundledDependencies
  44. delete pkg.bundledDependencies
  45. }
  46. const bd = pkg.bundleDependencies
  47. if (bd === true) {
  48. pkg.bundleDependencies = [
  49. ...Object.keys(pkg.dependencies || {}),
  50. ...Object.keys(pkg.optionalDependencies || {}),
  51. ]
  52. }
  53. if (typeof bd === 'object' && !Array.isArray(bd)) {
  54. pkg.bundleDependencies = Object.keys(bd)
  55. }
  56. pkg[normalized] = true
  57. return pkg
  58. })
  59. const pkgContents = async ({
  60. path,
  61. depth,
  62. currentDepth = 0,
  63. pkg = null,
  64. result = null,
  65. packageJsonCache = null,
  66. }) => {
  67. if (!result)
  68. result = new Set()
  69. if (!packageJsonCache)
  70. packageJsonCache = new Map()
  71. if (pkg === true) {
  72. return rpj({ path: path + '/package.json', packageJsonCache })
  73. .then(pkg => pkgContents({
  74. path,
  75. depth,
  76. currentDepth,
  77. pkg,
  78. result,
  79. packageJsonCache,
  80. }))
  81. }
  82. if (pkg) {
  83. // add all bins to result if they exist
  84. if (pkg.bin) {
  85. const dir = dirname(path)
  86. const base = basename(path)
  87. const scope = basename(dir)
  88. const nm = /^@.+/.test(scope) ? dirname(dir) : dir
  89. const binFiles = []
  90. Object.keys(pkg.bin).forEach(b => {
  91. const base = resolve(nm, '.bin', b)
  92. binFiles.push(base, base + '.cmd', base + '.ps1')
  93. })
  94. const bins = await Promise.all(
  95. binFiles.map(b => stat(b).then(() => b).catch((er) => null))
  96. )
  97. bins.filter(b => b).forEach(b => result.add(b))
  98. }
  99. }
  100. if (currentDepth >= depth) {
  101. result.add(path)
  102. return result
  103. }
  104. // we'll need bundle list later, so get that now in parallel
  105. const [dirEntries, bundleDeps] = await Promise.all([
  106. readdir(path, { withFileTypes: true }),
  107. currentDepth === 0 && pkg && pkg.bundleDependencies
  108. ? bundled({ path, packageJsonCache }) : null,
  109. ]).catch(() => [])
  110. // not a thing, probably a missing folder
  111. if (!dirEntries)
  112. return result
  113. // empty folder, just add the folder itself to the result
  114. if (!dirEntries.length && !bundleDeps && currentDepth !== 0) {
  115. result.add(path)
  116. return result
  117. }
  118. const recursePromises = []
  119. // if we didn't get withFileTypes support, tack that on
  120. if (typeof dirEntries[0] === 'string') {
  121. // use a map so we can return a promise, but we mutate dirEntries in place
  122. // this is much slower than getting the entries from the readdir call,
  123. // but polyfills support for node versions before 10.10
  124. await Promise.all(dirEntries.map(async (name, index) => {
  125. const p = resolve(path, name)
  126. const st = await lstat(p)
  127. dirEntries[index] = Object.assign(st, {name})
  128. }))
  129. }
  130. for (const entry of dirEntries) {
  131. const p = resolve(path, entry.name)
  132. if (entry.isDirectory() === false) {
  133. result.add(p)
  134. continue
  135. }
  136. if (currentDepth !== 0 || entry.name !== 'node_modules') {
  137. if (currentDepth < depth - 1) {
  138. recursePromises.push(pkgContents({
  139. path: p,
  140. packageJsonCache,
  141. depth,
  142. currentDepth: currentDepth + 1,
  143. result,
  144. }))
  145. } else {
  146. result.add(p)
  147. }
  148. continue
  149. }
  150. }
  151. if (bundleDeps) {
  152. // bundle deps are all folders
  153. // we always recurse to get pkg bins, but if currentDepth is too high,
  154. // it'll return early before walking their contents.
  155. recursePromises.push(...bundleDeps.map(dep => {
  156. const p = resolve(path, 'node_modules', dep)
  157. return pkgContents({
  158. path: p,
  159. packageJsonCache,
  160. pkg: true,
  161. depth,
  162. currentDepth: currentDepth + 1,
  163. result,
  164. })
  165. }))
  166. }
  167. if (recursePromises.length)
  168. await Promise.all(recursePromises)
  169. return result
  170. }
  171. module.exports = ({path, depth = 1, packageJsonCache}) => pkgContents({
  172. path: resolve(path),
  173. depth,
  174. pkg: true,
  175. packageJsonCache,
  176. }).then(results => [...results])
  177. if (require.main === module) {
  178. const options = { path: null, depth: 1 }
  179. const usage = `Usage:
  180. installed-package-contents <path> [-d<n> --depth=<n>]
  181. Lists the files installed for a package specified by <path>.
  182. Options:
  183. -d<n> --depth=<n> Provide a numeric value ("Infinity" is allowed)
  184. to specify how deep in the file tree to traverse.
  185. Default=1
  186. -h --help Show this usage information`
  187. process.argv.slice(2).forEach(arg => {
  188. let match
  189. if ((match = arg.match(/^--depth=([0-9]+|Infinity)/)) ||
  190. (match = arg.match(/^-d([0-9]+|Infinity)/)))
  191. options.depth = +match[1]
  192. else if (arg === '-h' || arg === '--help') {
  193. console.log(usage)
  194. process.exit(0)
  195. } else
  196. options.path = arg
  197. })
  198. if (!options.path) {
  199. console.error('ERROR: no path provided')
  200. console.error(usage)
  201. process.exit(1)
  202. }
  203. const cwd = process.cwd()
  204. module.exports(options)
  205. .then(list => list.sort().forEach(p => console.log(relative(cwd, p))))
  206. .catch(/* istanbul ignore next - pretty unusual */ er => {
  207. console.error(er)
  208. process.exit(1)
  209. })
  210. }