index.js 6.8 KB

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