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