blob: 777b58590d11e75381c3127ddbfb02342bc40ddc [file] [log] [blame]
'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',
'**/.git/**',
'**/.svn/**',
'**/.hg/**',
'**/CVS/**',
'/.lock-wscript',
'/.wafpickle-*',
'/build/config.gypi',
'npm-debug.log',
'**/.npmrc',
'.*.swp',
'**/.DS_Store/**',
'._*',
'**/._*/**',
'*.orig',
'/package-lock.json',
'/yarn.lock',
'archived-packages/**',
'core',
'!core/',
'!**/core/',
'*.core',
'*.vgcore',
'vgcore.*',
'core.+([0-9])',
]
// 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()
const dirName = path.basename(opt.path)
const parentName = path.basename(path.dirname(opt.path))
opt.follow =
dirName === 'node_modules' ||
(parentName === 'node_modules' && /^@/.test(dirName))
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 bin, browser or main, make sure we don't ignore it
// also, don't ignore the package.json itself!
const rules = [
pkg.browser ? '!' + pkg.browser : '',
pkg.main ? '!' + pkg.main : '',
'!package.json',
'!@(readme|copying|license|licence|notice|changes|changelog|history){,.*[^~$]}'
]
if (pkg.bin)
if (typeof pkg.bin === "object")
for (const key in pkg.bin)
rules.push('!' + pkg.bin[key])
else
rules.push('!' + pkg.bin)
const data = rules.filter(f => f).join('\n') + '\n'
super.onReadIgnoreFile(packageNecessaryRules, data, _=>_)
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 {
const ig = path.resolve(this.path, file)
this.onPackageJson(ig, 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)
bw.on('done', bundled => {
options.bundled = bundled
options.packageJsonCache = bw.packageJsonCache
new Walker(options).on('done', resolve).on('error', reject).start()
})
bw.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