• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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