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