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 return this 81 } 82 83 readPackageJson (pj) { 84 fs.readFile(pj, (er, data) => 85 er ? this.done() : this.onPackageJson(pj, data)) 86 } 87 88 onPackageJson (pj, data) { 89 try { 90 this.package = normalizePackageBin(JSON.parse(data + '')) 91 } catch (er) { 92 return this.done() 93 } 94 this.packageJsonCache.set(pj, this.package) 95 this.onPackage(this.package) 96 } 97 98 allDepsBundled (pkg) { 99 return Object.keys(pkg.dependencies || {}).concat( 100 Object.keys(pkg.optionalDependencies || {})) 101 } 102 103 onPackage (pkg) { 104 // all deps are bundled if we got here as a child. 105 // otherwise, only bundle bundledDeps 106 // Get a unique-ified array with a short-lived Set 107 const bdRaw = this.parent ? this.allDepsBundled(pkg) 108 : pkg.bundleDependencies || pkg.bundledDependencies || [] 109 110 const bd = Array.from(new Set( 111 Array.isArray(bdRaw) ? bdRaw 112 : bdRaw === true ? this.allDepsBundled(pkg) 113 : Object.keys(bdRaw))) 114 115 if (!bd.length) 116 return this.done() 117 118 this.bundle = bd 119 const nm = this.path + '/node_modules' 120 this.readModules() 121 } 122 123 readModules () { 124 readdirNodeModules(this.path + '/node_modules', (er, nm) => 125 er ? this.onReaddir([]) : this.onReaddir(nm)) 126 } 127 128 onReaddir (nm) { 129 // keep track of what we have, in case children need it 130 this.node_modules = nm 131 132 this.bundle.forEach(dep => this.childDep(dep)) 133 if (this.children === 0) 134 this.done() 135 } 136 137 childDep (dep) { 138 if (this.node_modules.indexOf(dep) !== -1 && !this.seen.has(dep)) { 139 this.seen.add(dep) 140 this.child(dep) 141 } else if (this.parent) { 142 this.parent.childDep(dep) 143 } 144 } 145 146 child (dep) { 147 const p = this.path + '/node_modules/' + dep 148 this.children += 1 149 const child = new BundleWalker({ 150 path: p, 151 parent: this 152 }) 153 child.on('done', _ => { 154 if (--this.children === 0) 155 this.done() 156 }) 157 child.start() 158 } 159} 160 161class BundleWalkerSync extends BundleWalker { 162 constructor (opt) { 163 super(opt) 164 } 165 166 start () { 167 super.start() 168 this.done() 169 return this 170 } 171 172 readPackageJson (pj) { 173 try { 174 this.onPackageJson(pj, fs.readFileSync(pj)) 175 } catch (er) {} 176 return this 177 } 178 179 readModules () { 180 try { 181 this.onReaddir(readdirNodeModulesSync(this.path + '/node_modules')) 182 } catch (er) { 183 this.onReaddir([]) 184 } 185 } 186 187 child (dep) { 188 new BundleWalkerSync({ 189 path: this.path + '/node_modules/' + dep, 190 parent: this 191 }).start() 192 } 193} 194 195const readdirNodeModules = (nm, cb) => { 196 fs.readdir(nm, (er, set) => { 197 if (er) 198 cb(er) 199 else { 200 const scopes = set.filter(f => /^@/.test(f)) 201 if (!scopes.length) 202 cb(null, set) 203 else { 204 const unscoped = set.filter(f => !/^@/.test(f)) 205 let count = scopes.length 206 scopes.forEach(scope => { 207 fs.readdir(nm + '/' + scope, (er, pkgs) => { 208 if (er || !pkgs.length) 209 unscoped.push(scope) 210 else 211 unscoped.push.apply(unscoped, pkgs.map(p => scope + '/' + p)) 212 if (--count === 0) 213 cb(null, unscoped) 214 }) 215 }) 216 } 217 } 218 }) 219} 220 221const readdirNodeModulesSync = nm => { 222 const set = fs.readdirSync(nm) 223 const unscoped = set.filter(f => !/^@/.test(f)) 224 const scopes = set.filter(f => /^@/.test(f)).map(scope => { 225 try { 226 const pkgs = fs.readdirSync(nm + '/' + scope) 227 return pkgs.length ? pkgs.map(p => scope + '/' + p) : [scope] 228 } catch (er) { 229 return [scope] 230 } 231 }).reduce((a, b) => a.concat(b), []) 232 return unscoped.concat(scopes) 233} 234 235const walk = (options, callback) => { 236 const p = new Promise((resolve, reject) => { 237 new BundleWalker(options).on('done', resolve).on('error', reject).start() 238 }) 239 return callback ? p.then(res => callback(null, res), callback) : p 240} 241 242const walkSync = options => { 243 return new BundleWalkerSync(options).start().result 244} 245 246module.exports = walk 247walk.sync = walkSync 248walk.BundleWalker = BundleWalker 249walk.BundleWalkerSync = BundleWalkerSync 250