'use strict' // walk the tree of deps starting from the top level list of bundled deps // Any deps at the top level that are depended on by a bundled dep that // does not have that dep in its own node_modules folder are considered // bundled deps as well. This list of names can be passed to npm-packlist // as the "bundled" argument. Additionally, packageJsonCache is shared so // packlist doesn't have to re-read files already consumed in this pass const fs = require('fs') const path = require('path') const EE = require('events').EventEmitter class BundleWalker extends EE { constructor (opt) { opt = opt || {} super(opt) this.path = path.resolve(opt.path || process.cwd()) this.parent = opt.parent || null if (this.parent) { this.result = this.parent.result // only collect results in node_modules folders at the top level // since the node_modules in a bundled dep is included always if (!this.parent.parent) { const base = path.basename(this.path) const scope = path.basename(path.dirname(this.path)) this.result.add(/^@/.test(scope) ? scope + '/' + base : base) } this.root = this.parent.root this.packageJsonCache = this.parent.packageJsonCache } else { this.result = new Set() this.root = this.path this.packageJsonCache = opt.packageJsonCache || new Map() } this.seen = new Set() this.didDone = false this.children = 0 this.node_modules = [] this.package = null this.bundle = null } addListener (ev, fn) { return this.on(ev, fn) } on (ev, fn) { const ret = super.on(ev, fn) if (ev === 'done' && this.didDone) { this.emit('done', this.result) } return ret } done () { if (!this.didDone) { this.didDone = true if (!this.parent) { const res = Array.from(this.result) this.result = res this.emit('done', res) } else { this.emit('done') } } } start () { const pj = path.resolve(this.path, 'package.json') if (this.packageJsonCache.has(pj)) this.onPackage(this.packageJsonCache.get(pj)) else this.readPackageJson(pj) return this } readPackageJson (pj) { fs.readFile(pj, (er, data) => er ? this.done() : this.onPackageJson(pj, data)) } onPackageJson (pj, data) { try { this.package = JSON.parse(data + '') } catch (er) { return this.done() } this.packageJsonCache.set(pj, this.package) this.onPackage(this.package) } onPackage (pkg) { // all deps are bundled if we got here as a child. // otherwise, only bundle bundledDeps // Get a unique-ified array with a short-lived Set const bdRaw = this.parent ? Object.keys(pkg.dependencies || {}).concat( Object.keys(pkg.optionalDependencies || {})) : pkg.bundleDependencies || pkg.bundledDependencies || [] const bd = Array.from(new Set( Array.isArray(bdRaw) ? bdRaw : Object.keys(bdRaw))) if (!bd.length) return this.done() this.bundle = bd const nm = this.path + '/node_modules' this.readModules() } readModules () { readdirNodeModules(this.path + '/node_modules', (er, nm) => er ? this.onReaddir([]) : this.onReaddir(nm)) } onReaddir (nm) { // keep track of what we have, in case children need it this.node_modules = nm this.bundle.forEach(dep => this.childDep(dep)) if (this.children === 0) this.done() } childDep (dep) { if (this.node_modules.indexOf(dep) !== -1 && !this.seen.has(dep)) { this.seen.add(dep) this.child(dep) } else if (this.parent) { this.parent.childDep(dep) } } child (dep) { const p = this.path + '/node_modules/' + dep this.children += 1 const child = new BundleWalker({ path: p, parent: this }) child.on('done', _ => { if (--this.children === 0) this.done() }) child.start() } } class BundleWalkerSync extends BundleWalker { constructor (opt) { super(opt) } start () { super.start() this.done() return this } readPackageJson (pj) { try { this.onPackageJson(pj, fs.readFileSync(pj)) } catch (er) {} return this } readModules () { try { this.onReaddir(readdirNodeModulesSync(this.path + '/node_modules')) } catch (er) { this.onReaddir([]) } } child (dep) { new BundleWalkerSync({ path: this.path + '/node_modules/' + dep, parent: this }).start() } } const readdirNodeModules = (nm, cb) => { fs.readdir(nm, (er, set) => { if (er) cb(er) else { const scopes = set.filter(f => /^@/.test(f)) if (!scopes.length) cb(null, set) else { const unscoped = set.filter(f => !/^@/.test(f)) let count = scopes.length scopes.forEach(scope => { fs.readdir(nm + '/' + scope, (er, pkgs) => { if (er || !pkgs.length) unscoped.push(scope) else unscoped.push.apply(unscoped, pkgs.map(p => scope + '/' + p)) if (--count === 0) cb(null, unscoped) }) }) } } }) } const readdirNodeModulesSync = nm => { const set = fs.readdirSync(nm) const unscoped = set.filter(f => !/^@/.test(f)) const scopes = set.filter(f => /^@/.test(f)).map(scope => { try { const pkgs = fs.readdirSync(nm + '/' + scope) return pkgs.length ? pkgs.map(p => scope + '/' + p) : [scope] } catch (er) { return [scope] } }).reduce((a, b) => a.concat(b), []) return unscoped.concat(scopes) } const walk = (options, callback) => { const p = new Promise((resolve, reject) => { new BundleWalker(options).on('done', resolve).on('error', reject).start() }) return callback ? p.then(res => callback(null, res), callback) : p } const walkSync = options => { return new BundleWalkerSync(options).start().result } module.exports = walk walk.sync = walkSync walk.BundleWalker = BundleWalker walk.BundleWalkerSync = BundleWalkerSync