index.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  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 { readFile, readdir, stat } = require('fs/promises')
  20. const { resolve, basename, dirname } = require('path')
  21. const normalizePackageBin = require('npm-normalize-package-bin')
  22. const readPackage = ({ path, packageJsonCache }) => packageJsonCache.has(path)
  23. ? Promise.resolve(packageJsonCache.get(path))
  24. : readFile(path).then(json => {
  25. const pkg = normalizePackageBin(JSON.parse(json))
  26. packageJsonCache.set(path, pkg)
  27. return pkg
  28. }).catch(() => null)
  29. // just normalize bundle deps and bin, that's all we care about here.
  30. const normalized = Symbol('package data has been normalized')
  31. const rpj = ({ path, packageJsonCache }) => readPackage({ path, packageJsonCache })
  32. .then(pkg => {
  33. if (!pkg || pkg[normalized]) {
  34. return pkg
  35. }
  36. if (pkg.bundledDependencies && !pkg.bundleDependencies) {
  37. pkg.bundleDependencies = pkg.bundledDependencies
  38. delete pkg.bundledDependencies
  39. }
  40. const bd = pkg.bundleDependencies
  41. if (bd === true) {
  42. pkg.bundleDependencies = [
  43. ...Object.keys(pkg.dependencies || {}),
  44. ...Object.keys(pkg.optionalDependencies || {}),
  45. ]
  46. }
  47. if (typeof bd === 'object' && !Array.isArray(bd)) {
  48. pkg.bundleDependencies = Object.keys(bd)
  49. }
  50. pkg[normalized] = true
  51. return pkg
  52. })
  53. const pkgContents = async ({
  54. path,
  55. depth = 1,
  56. currentDepth = 0,
  57. pkg = null,
  58. result = null,
  59. packageJsonCache = null,
  60. }) => {
  61. if (!result) {
  62. result = new Set()
  63. }
  64. if (!packageJsonCache) {
  65. packageJsonCache = new Map()
  66. }
  67. if (pkg === true) {
  68. return rpj({ path: path + '/package.json', packageJsonCache })
  69. .then(p => pkgContents({
  70. path,
  71. depth,
  72. currentDepth,
  73. pkg: p,
  74. result,
  75. packageJsonCache,
  76. }))
  77. }
  78. if (pkg) {
  79. // add all bins to result if they exist
  80. if (pkg.bin) {
  81. const dir = dirname(path)
  82. const scope = basename(dir)
  83. const nm = /^@.+/.test(scope) ? dirname(dir) : dir
  84. const binFiles = []
  85. Object.keys(pkg.bin).forEach(b => {
  86. const base = resolve(nm, '.bin', b)
  87. binFiles.push(base, base + '.cmd', base + '.ps1')
  88. })
  89. const bins = await Promise.all(
  90. binFiles.map(b => stat(b).then(() => b).catch(() => null))
  91. )
  92. bins.filter(b => b).forEach(b => result.add(b))
  93. }
  94. }
  95. if (currentDepth >= depth) {
  96. result.add(path)
  97. return result
  98. }
  99. // we'll need bundle list later, so get that now in parallel
  100. const [dirEntries, bundleDeps] = await Promise.all([
  101. readdir(path, { withFileTypes: true }),
  102. currentDepth === 0 && pkg && pkg.bundleDependencies
  103. ? bundled({ path, packageJsonCache }) : null,
  104. ]).catch(() => [])
  105. // not a thing, probably a missing folder
  106. if (!dirEntries) {
  107. return result
  108. }
  109. // empty folder, just add the folder itself to the result
  110. if (!dirEntries.length && !bundleDeps && currentDepth !== 0) {
  111. result.add(path)
  112. return result
  113. }
  114. const recursePromises = []
  115. for (const entry of dirEntries) {
  116. const p = resolve(path, entry.name)
  117. if (entry.isDirectory() === false) {
  118. result.add(p)
  119. continue
  120. }
  121. if (currentDepth !== 0 || entry.name !== 'node_modules') {
  122. if (currentDepth < depth - 1) {
  123. recursePromises.push(pkgContents({
  124. path: p,
  125. packageJsonCache,
  126. depth,
  127. currentDepth: currentDepth + 1,
  128. result,
  129. }))
  130. } else {
  131. result.add(p)
  132. }
  133. continue
  134. }
  135. }
  136. if (bundleDeps) {
  137. // bundle deps are all folders
  138. // we always recurse to get pkg bins, but if currentDepth is too high,
  139. // it'll return early before walking their contents.
  140. recursePromises.push(...bundleDeps.map(dep => {
  141. const p = resolve(path, 'node_modules', dep)
  142. return pkgContents({
  143. path: p,
  144. packageJsonCache,
  145. pkg: true,
  146. depth,
  147. currentDepth: currentDepth + 1,
  148. result,
  149. })
  150. }))
  151. }
  152. if (recursePromises.length) {
  153. await Promise.all(recursePromises)
  154. }
  155. return result
  156. }
  157. module.exports = ({ path, ...opts }) => pkgContents({
  158. path: resolve(path),
  159. ...opts,
  160. pkg: true,
  161. }).then(results => [...results])