1'use strict' 2 3// walk the tree of deps starting from the top level list of bundled deps 4// Any deps at the top level that are depended on by a bundled dep that 5// does not have that dep in its own node_modules folder are considered 6// bundled deps as well. This list of names can be passed to npm-packlist 7// as the "bundled" argument. Additionally, packageJsonCache is shared so 8// packlist doesn't have to re-read files already consumed in this pass 9 10const fs = require('fs') 11const path = require('path') 12const EE = require('events').EventEmitter 13// we don't care about the package bins, but we share a pj cache 14// with other modules that DO care about it, so keep it nice. 15const normalizePackageBin = require('npm-normalize-package-bin') 16 17class BundleWalker extends EE { 18 constructor (opt) { 19 opt = opt || {} 20 super(opt) 21 this.path = path.resolve(opt.path || process.cwd()) 22 23 this.parent = opt.parent || null 24 if (this.parent) { 25 this.result = this.parent.result 26 // only collect results in node_modules folders at the top level 27 // since the node_modules in a bundled dep is included always 28 if (!this.parent.parent) { 29 const base = path.basename(this.path) 30 const scope = path.basename(path.dirname(this.path)) 31 this.result.add(/^@/.test(scope) ? scope + '/' + base : base) 32 } 33 this.root = this.parent.root 34 this.packageJsonCache = this.parent.packageJsonCache 35 } else { 36 this.result = new Set() 37 this.root = this.path 38 this.packageJsonCache = opt.packageJsonCache || new Map() 39 } 40 41 this.seen = new Set() 42 this.didDone = false 43 this.children = 0 44 this.node_modules = [] 45 this.package = null 46 this.bundle = null 47 } 48 49 addListener (ev, fn) { 50 return this.on(ev, fn) 51 } 52 53 on (ev, fn) { 54 const ret = super.on(ev, fn) 55 if (ev === 'done' && this.didDone) { 56 this.emit('done', this.result) 57 } 58 return ret 59 } 60 61 done () { 62 if (!this.didDone) { 63 this.didDone = true 64 if (!this.parent) { 65 const res = Array.from(this.result) 66 this.result = res 67 this.emit('done', res) 68 } else { 69 this.emit('done') 70 } 71 } 72 } 73 74 start () { 75 const pj = path.resolve(this.path, 'package.json') 76 if (this.packageJsonCache.has(pj)) { 77 this.onPackage(this.packageJsonCache.get(pj)) 78 } else { 79 this.readPackageJson(pj) 80 } 81 return this 82 } 83 84 readPackageJson (pj) { 85 fs.readFile(pj, (er, data) => 86 er ? this.done() : this.onPackageJson(pj, data)) 87 } 88 89 onPackageJson (pj, data) { 90 try { 91 this.package = normalizePackageBin(JSON.parse(data + '')) 92 } catch (er) { 93 return this.done() 94 } 95 this.packageJsonCache.set(pj, this.package) 96 this.onPackage(this.package) 97 } 98 99 allDepsBundled (pkg) { 100 return Object.keys(pkg.dependencies || {}).concat( 101 Object.keys(pkg.optionalDependencies || {})) 102 } 103 104 onPackage (pkg) { 105 // all deps are bundled if we got here as a child. 106 // otherwise, only bundle bundledDeps 107 // Get a unique-ified array with a short-lived Set 108 const bdRaw = this.parent ? this.allDepsBundled(pkg) 109 : pkg.bundleDependencies || pkg.bundledDependencies || [] 110 111 const bd = Array.from(new Set( 112 Array.isArray(bdRaw) ? bdRaw 113 : bdRaw === true ? this.allDepsBundled(pkg) 114 : Object.keys(bdRaw))) 115 116 if (!bd.length) { 117 return this.done() 118 } 119 120 this.bundle = bd 121 this.readModules() 122 } 123 124 readModules () { 125 readdirNodeModules(this.path + '/node_modules', (er, nm) => 126 er ? this.onReaddir([]) : this.onReaddir(nm)) 127 } 128 129 onReaddir (nm) { 130 // keep track of what we have, in case children need it 131 this.node_modules = nm 132 133 this.bundle.forEach(dep => this.childDep(dep)) 134 if (this.children === 0) { 135 this.done() 136 } 137 } 138 139 childDep (dep) { 140 if (this.node_modules.indexOf(dep) !== -1) { 141 if (!this.seen.has(dep)) { 142 this.seen.add(dep) 143 this.child(dep) 144 } 145 } else if (this.parent) { 146 this.parent.childDep(dep) 147 } 148 } 149 150 child (dep) { 151 const p = this.path + '/node_modules/' + dep 152 this.children += 1 153 const child = new BundleWalker({ 154 path: p, 155 parent: this, 156 }) 157 child.on('done', _ => { 158 if (--this.children === 0) { 159 this.done() 160 } 161 }) 162 child.start() 163 } 164} 165 166class BundleWalkerSync extends BundleWalker { 167 start () { 168 super.start() 169 this.done() 170 return this 171 } 172 173 readPackageJson (pj) { 174 try { 175 this.onPackageJson(pj, fs.readFileSync(pj)) 176 } catch { 177 // empty catch 178 } 179 return this 180 } 181 182 readModules () { 183 try { 184 this.onReaddir(readdirNodeModulesSync(this.path + '/node_modules')) 185 } catch { 186 this.onReaddir([]) 187 } 188 } 189 190 child (dep) { 191 new BundleWalkerSync({ 192 path: this.path + '/node_modules/' + dep, 193 parent: this, 194 }).start() 195 } 196} 197 198const readdirNodeModules = (nm, cb) => { 199 fs.readdir(nm, (er, set) => { 200 if (er) { 201 cb(er) 202 } else { 203 const scopes = set.filter(f => /^@/.test(f)) 204 if (!scopes.length) { 205 cb(null, set) 206 } else { 207 const unscoped = set.filter(f => !/^@/.test(f)) 208 let count = scopes.length 209 scopes.forEach(scope => { 210 fs.readdir(nm + '/' + scope, (readdirEr, pkgs) => { 211 if (readdirEr || !pkgs.length) { 212 unscoped.push(scope) 213 } else { 214 unscoped.push.apply(unscoped, pkgs.map(p => scope + '/' + p)) 215 } 216 if (--count === 0) { 217 cb(null, unscoped) 218 } 219 }) 220 }) 221 } 222 } 223 }) 224} 225 226const readdirNodeModulesSync = nm => { 227 const set = fs.readdirSync(nm) 228 const unscoped = set.filter(f => !/^@/.test(f)) 229 const scopes = set.filter(f => /^@/.test(f)).map(scope => { 230 try { 231 const pkgs = fs.readdirSync(nm + '/' + scope) 232 return pkgs.length ? pkgs.map(p => scope + '/' + p) : [scope] 233 } catch (er) { 234 return [scope] 235 } 236 }).reduce((a, b) => a.concat(b), []) 237 return unscoped.concat(scopes) 238} 239 240const walk = (options, callback) => { 241 const p = new Promise((resolve, reject) => { 242 new BundleWalker(options).on('done', resolve).on('error', reject).start() 243 }) 244 return callback ? p.then(res => callback(null, res), callback) : p 245} 246 247const walkSync = options => { 248 return new BundleWalkerSync(options).start().result 249} 250 251module.exports = walk 252walk.sync = walkSync 253walk.BundleWalker = BundleWalker 254walk.BundleWalkerSync = BundleWalkerSync 255