• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3// Do a two-pass walk, first to get the list of packages that need to be
4// bundled, then again to get the actual files and folders.
5// Keep a cache of node_modules content and package.json data, so that the
6// second walk doesn't have to re-do all the same work.
7
8const bundleWalk = require('npm-bundled')
9const BundleWalker = bundleWalk.BundleWalker
10const BundleWalkerSync = bundleWalk.BundleWalkerSync
11
12const ignoreWalk = require('ignore-walk')
13const IgnoreWalker = ignoreWalk.Walker
14const IgnoreWalkerSync = ignoreWalk.WalkerSync
15
16const rootBuiltinRules = Symbol('root-builtin-rules')
17const packageNecessaryRules = Symbol('package-necessary-rules')
18const path = require('path')
19
20const normalizePackageBin = require('npm-normalize-package-bin')
21
22const defaultRules = [
23  '.npmignore',
24  '.gitignore',
25  '**/.git',
26  '**/.svn',
27  '**/.hg',
28  '**/CVS',
29  '**/.git/**',
30  '**/.svn/**',
31  '**/.hg/**',
32  '**/CVS/**',
33  '/.lock-wscript',
34  '/.wafpickle-*',
35  '/build/config.gypi',
36  'npm-debug.log',
37  '**/.npmrc',
38  '.*.swp',
39  '.DS_Store',
40  '**/.DS_Store/**',
41  '._*',
42  '**/._*/**',
43  '*.orig',
44  '/package-lock.json',
45  '/yarn.lock',
46  'archived-packages/**',
47  'core',
48  '!core/',
49  '!**/core/',
50  '*.core',
51  '*.vgcore',
52  'vgcore.*',
53  'core.+([0-9])',
54]
55
56// There may be others, but :?|<> are handled by node-tar
57const nameIsBadForWindows = file => /\*/.test(file)
58
59// a decorator that applies our custom rules to an ignore walker
60const npmWalker = Class => class Walker extends Class {
61  constructor (opt) {
62    opt = opt || {}
63
64    // the order in which rules are applied.
65    opt.ignoreFiles = [
66      rootBuiltinRules,
67      'package.json',
68      '.npmignore',
69      '.gitignore',
70      packageNecessaryRules
71    ]
72
73    opt.includeEmpty = false
74    opt.path = opt.path || process.cwd()
75    const dirName = path.basename(opt.path)
76    const parentName = path.basename(path.dirname(opt.path))
77    opt.follow =
78      dirName === 'node_modules' ||
79      (parentName === 'node_modules' && /^@/.test(dirName))
80    super(opt)
81
82    // ignore a bunch of things by default at the root level.
83    // also ignore anything in node_modules, except bundled dependencies
84    if (!this.parent) {
85      this.bundled = opt.bundled || []
86      this.bundledScopes = Array.from(new Set(
87        this.bundled.filter(f => /^@/.test(f))
88        .map(f => f.split('/')[0])))
89      const rules = defaultRules.join('\n') + '\n'
90      this.packageJsonCache = opt.packageJsonCache || new Map()
91      super.onReadIgnoreFile(rootBuiltinRules, rules, _=>_)
92    } else {
93      this.bundled = []
94      this.bundledScopes = []
95      this.packageJsonCache = this.parent.packageJsonCache
96    }
97  }
98
99  onReaddir (entries) {
100    if (!this.parent) {
101      entries = entries.filter(e =>
102        e !== '.git' &&
103        !(e === 'node_modules' && this.bundled.length === 0)
104      )
105    }
106    return super.onReaddir(entries)
107  }
108
109  filterEntry (entry, partial) {
110    // get the partial path from the root of the walk
111    const p = this.path.substr(this.root.length + 1)
112    const pkgre = /^node_modules\/(@[^\/]+\/?[^\/]+|[^\/]+)(\/.*)?$/
113    const isRoot = !this.parent
114    const pkg = isRoot && pkgre.test(entry) ?
115      entry.replace(pkgre, '$1') : null
116    const rootNM = isRoot && entry === 'node_modules'
117    const rootPJ = isRoot && entry === 'package.json'
118
119    return (
120      // if we're in a bundled package, check with the parent.
121      /^node_modules($|\/)/i.test(p) ? this.parent.filterEntry(
122          this.basename + '/' + entry, partial)
123
124      // if package is bundled, all files included
125      // also include @scope dirs for bundled scoped deps
126      // they'll be ignored if no files end up in them.
127      // However, this only matters if we're in the root.
128      // node_modules folders elsewhere, like lib/node_modules,
129      // should be included normally unless ignored.
130      : pkg ? -1 !== this.bundled.indexOf(pkg) ||
131        -1 !== this.bundledScopes.indexOf(pkg)
132
133      // only walk top node_modules if we want to bundle something
134      : rootNM ? !!this.bundled.length
135
136      // always include package.json at the root.
137      : rootPJ ? true
138
139      // otherwise, follow ignore-walk's logic
140      : super.filterEntry(entry, partial)
141    )
142  }
143
144  filterEntries () {
145    if (this.ignoreRules['package.json'])
146      this.ignoreRules['.gitignore'] = this.ignoreRules['.npmignore'] = null
147    else if (this.ignoreRules['.npmignore'])
148      this.ignoreRules['.gitignore'] = null
149    this.filterEntries = super.filterEntries
150    super.filterEntries()
151  }
152
153  addIgnoreFile (file, then) {
154    const ig = path.resolve(this.path, file)
155    if (this.packageJsonCache.has(ig))
156      this.onPackageJson(ig, this.packageJsonCache.get(ig), then)
157    else
158      super.addIgnoreFile(file, then)
159  }
160
161  onPackageJson (ig, pkg, then) {
162    this.packageJsonCache.set(ig, pkg)
163
164    // if there's a bin, browser or main, make sure we don't ignore it
165    // also, don't ignore the package.json itself!
166    //
167    // Weird side-effect of this: a readme (etc) file will be included
168    // if it exists anywhere within a folder with a package.json file.
169    // The original intent was only to include these files in the root,
170    // but now users in the wild are dependent on that behavior for
171    // localized documentation and other use cases.  Adding a `/` to
172    // these rules, while tempting and arguably more "correct", is a
173    // breaking change.
174    const rules = [
175      pkg.browser ? '!' + pkg.browser : '',
176      pkg.main ? '!' + pkg.main : '',
177      '!package.json',
178      '!npm-shrinkwrap.json',
179      '!@(readme|copying|license|licence|notice|changes|changelog|history){,.*[^~$]}'
180    ]
181    if (pkg.bin) {
182      // always an object, because normalized already
183      for (const key in pkg.bin)
184        rules.push('!' + pkg.bin[key])
185    }
186
187    const data = rules.filter(f => f).join('\n') + '\n'
188    super.onReadIgnoreFile(packageNecessaryRules, data, _=>_)
189
190    if (Array.isArray(pkg.files))
191      super.onReadIgnoreFile('package.json', '*\n' + pkg.files.map(
192        f => '!' + f + '\n!' + f.replace(/\/+$/, '') + '/**'
193      ).join('\n') + '\n', then)
194    else
195      then()
196  }
197
198  // override parent stat function to completely skip any filenames
199  // that will break windows entirely.
200  // XXX(isaacs) Next major version should make this an error instead.
201  stat (entry, file, dir, then) {
202    if (nameIsBadForWindows(entry))
203      then()
204    else
205      super.stat(entry, file, dir, then)
206  }
207
208  // override parent onstat function to nix all symlinks
209  onstat (st, entry, file, dir, then) {
210    if (st.isSymbolicLink())
211      then()
212    else
213      super.onstat(st, entry, file, dir, then)
214  }
215
216  onReadIgnoreFile (file, data, then) {
217    if (file === 'package.json')
218      try {
219        const ig = path.resolve(this.path, file)
220        this.onPackageJson(ig, normalizePackageBin(JSON.parse(data)), then)
221      } catch (er) {
222        // ignore package.json files that are not json
223        then()
224      }
225    else
226      super.onReadIgnoreFile(file, data, then)
227  }
228
229  sort (a, b) {
230    return sort(a, b)
231  }
232}
233
234class Walker extends npmWalker(IgnoreWalker) {
235  walker (entry, then) {
236    new Walker(this.walkerOpt(entry)).on('done', then).start()
237  }
238}
239
240class WalkerSync extends npmWalker(IgnoreWalkerSync) {
241  walker (entry, then) {
242    new WalkerSync(this.walkerOpt(entry)).start()
243    then()
244  }
245}
246
247const walk = (options, callback) => {
248  options = options || {}
249  const p = new Promise((resolve, reject) => {
250    const bw = new BundleWalker(options)
251    bw.on('done', bundled => {
252      options.bundled = bundled
253      options.packageJsonCache = bw.packageJsonCache
254      new Walker(options).on('done', resolve).on('error', reject).start()
255    })
256    bw.start()
257  })
258  return callback ? p.then(res => callback(null, res), callback) : p
259}
260
261const walkSync = options => {
262  options = options || {}
263  const bw = new BundleWalkerSync(options).start()
264  options.bundled = bw.result
265  options.packageJsonCache = bw.packageJsonCache
266  const walker = new WalkerSync(options)
267  walker.start()
268  return walker.result
269}
270
271// optimize for compressibility
272// extname, then basename, then locale alphabetically
273// https://twitter.com/isntitvacant/status/1131094910923231232
274const sort = (a, b) => {
275  const exta = path.extname(a).toLowerCase()
276  const extb = path.extname(b).toLowerCase()
277  const basea = path.basename(a).toLowerCase()
278  const baseb = path.basename(b).toLowerCase()
279
280  return exta.localeCompare(extb) ||
281    basea.localeCompare(baseb) ||
282    a.localeCompare(b)
283}
284
285
286module.exports = walk
287walk.sync = walkSync
288walk.Walker = Walker
289walk.WalkerSync = WalkerSync
290