index.js 8.5 KB

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