text: 'use strict'
  
  // Do a two-pass walk, first to get the list of packages that need to be
  // bundled, then again to get the actual files and folders.
  // Keep a cache of node_modules content and package.json data, so that the
  // second walk doesn't have to re-do all the same work.
  
  const bundleWalk = require('npm-bundled')
  const BundleWalker = bundleWalk.BundleWalker
  const BundleWalkerSync = bundleWalk.BundleWalkerSync
  
  const ignoreWalk = require('ignore-walk')
  const IgnoreWalker = ignoreWalk.Walker
  const IgnoreWalkerSync = ignoreWalk.WalkerSync
  
  const rootBuiltinRules = Symbol('root-builtin-rules')
  const packageNecessaryRules = Symbol('package-necessary-rules')
  const path = require('path')
  
  const defaultRules = [
    '.npmignore',
    '.gitignore',
    '**/.git/',
    '**/.svn/',
    '**/.hg/',
    '**/CVS/',
    '/.lock-wscript',
    '/.wafpickle-*',
    '/build/config.gypi',
    'npm-debug.log',
    '**/.npmrc',
    '.*.swp',
    '.DS_Store',
    '._*',
    '*.orig',
    'package-lock.json'
  ]
  
  // a decorator that applies our custom rules to an ignore walker
  const npmWalker = Class => class Walker extends Class {
    constructor (opt) {
      opt = opt || {}
  
      // the order in which rules are applied.
      opt.ignoreFiles = [
        rootBuiltinRules,
        'package.json',
        '.npmignore',
        '.gitignore',
        packageNecessaryRules
      ]
  
      opt.includeEmpty = false
      opt.path = opt.path || process.cwd()
      opt.follow = path.basename(opt.path) === 'node_modules'
      super(opt)
  
      // ignore a bunch of things by default at the root level.
      // also ignore anything in node_modules, except bundled dependencies
      if (!this.parent) {
        this.bundled = opt.bundled || []
        this.bundledScopes = Array.from(new Set(
          this.bundled.filter(f => /^@/.test(f))
          .map(f => f.split('/')[0])))
        const rules = defaultRules.join('\n') + '\n'
        this.packageJsonCache = opt.packageJsonCache || new Map()
        super.onReadIgnoreFile(rootBuiltinRules, rules, _=>_)
      } else {
        this.bundled = []
        this.bundledScopes = []
        this.packageJsonCache = this.parent.packageJsonCache
      }
    }
  
    filterEntry (entry, partial) {
      // get the partial path from the root of the walk
      const p = this.path.substr(this.root.length + 1)
      const pkgre = /^node_modules\/(@[^\/]+\/?[^\/]+|[^\/]+)(\/.*)?$/
      const isRoot = !this.parent
      const pkg = isRoot && pkgre.test(entry) ?
        entry.replace(pkgre, '$1') : null
      const rootNM = isRoot && entry === 'node_modules'
      const rootPJ = isRoot && entry === 'package.json'
  
      return (
        // if we're in a bundled package, check with the parent.
        /^node_modules($|\/)/i.test(p) ? this.parent.filterEntry(
            this.basename + '/' + entry, partial)
  
        // if package is bundled, all files included
        // also include @scope dirs for bundled scoped deps
        // they'll be ignored if no files end up in them.
        // However, this only matters if we're in the root.
        // node_modules folders elsewhere, like lib/node_modules,
        // should be included normally unless ignored.
        : pkg ? -1 !== this.bundled.indexOf(pkg) ||
          -1 !== this.bundledScopes.indexOf(pkg)
  
        // only walk top node_modules if we want to bundle something
        : rootNM ? !!this.bundled.length
  
        // always include package.json at the root.
        : rootPJ ? true
  
        // otherwise, follow ignore-walk's logic
        : super.filterEntry(entry, partial)
      )
    }
  
    filterEntries () {
      if (this.ignoreRules['package.json'])
        this.ignoreRules['.gitignore'] = this.ignoreRules['.npmignore'] = null
      else if (this.ignoreRules['.npmignore'])
        this.ignoreRules['.gitignore'] = null
      this.filterEntries = super.filterEntries
      super.filterEntries()
    }
  
    addIgnoreFile (file, then) {
      const ig = path.resolve(this.path, file)
      if (this.packageJsonCache.has(ig))
        this.onPackageJson(ig, this.packageJsonCache.get(ig), then)
      else
        super.addIgnoreFile(file, then)
    }
  
    onPackageJson (ig, pkg, then) {
      this.packageJsonCache.set(ig, pkg)
  
      // if there's a browser or main, make sure we don't ignore it
      const rules = [
        pkg.browser ? '!' + pkg.browser : '',
        pkg.main ? '!' + pkg.main : '',
        '!@(readme|license|licence|notice|changes|changelog|history){,.*}'
      ].filter(f => f).join('\n') + '\n'
      super.onReadIgnoreFile(packageNecessaryRules, rules, _=>_)
  
      if (Array.isArray(pkg.files))
        super.onReadIgnoreFile('package.json', '*\n' + pkg.files.map(
          f => '!' + f + '\n!' + f.replace(/\/+$/, '') + '/**'
        ).join('\n') + '\n', then)
      else
        then()
    }
  
    // override parent onstat function to nix all symlinks
    onstat (st, entry, file, dir, then) {
      if (st.isSymbolicLink())
        then()
      else
        super.onstat(st, entry, file, dir, then)
    }
  
    onReadIgnoreFile (file, data, then) {
      if (file === 'package.json')
        try {
          this.onPackageJson(file, JSON.parse(data), then)
        } catch (er) {
          // ignore package.json files that are not json
          then()
        }
      else
        super.onReadIgnoreFile(file, data, then)
    }
  
    sort (a, b) {
      return sort(a, b)
    }
  }
  
  class Walker extends npmWalker(IgnoreWalker) {
    walker (entry, then) {
      new Walker(this.walkerOpt(entry)).on('done', then).start()
    }
  }
  
  class WalkerSync extends npmWalker(IgnoreWalkerSync) {
    walker (entry, then) {
      new WalkerSync(this.walkerOpt(entry)).start()
      then()
    }
  }
  
  const walk = (options, callback) => {
    options = options || {}
    const p = new Promise((resolve, reject) => {
      const bw = new BundleWalker(options).start()
      bw.on('done', bundled => {
        options.bundled = bundled
        options.packageJsonCache = bw.packageJsonCache
        new Walker(options).on('done', resolve).on('error', reject).start()
      })
    })
    return callback ? p.then(res => callback(null, res), callback) : p
  }
  
  const walkSync = options => {
    options = options || {}
    const bw = new BundleWalkerSync(options).start()
    options.bundled = bw.result
    options.packageJsonCache = bw.packageJsonCache
    const walker = new WalkerSync(options)
    walker.start()
    return walker.result
  }
  
  // package.json first, node_modules last, files before folders, alphasort
  const sort = (a, b) =>
    a === 'package.json' ? -1
    : b === 'package.json' ? 1
    : /^node_modules/.test(a) && !/^node_modules/.test(b) ? 1
    : /^node_modules/.test(b) && !/^node_modules/.test(a) ? -1
    : path.dirname(a) === '.' && path.dirname(b) !== '.' ? -1
    : path.dirname(b) === '.' && path.dirname(a) !== '.' ? 1
    : a.localeCompare(b)
  
  module.exports = walk
  walk.sync = walkSync
  walk.Walker = Walker
  walk.WalkerSync = WalkerSync
  