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