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