get-npmignore.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. /**
  2. * @author Toru Nagashima
  3. * See LICENSE file in root directory for full license.
  4. */
  5. "use strict"
  6. const fs = require("fs")
  7. const path = require("path")
  8. const ignore = require("ignore")
  9. const Cache = require("./cache")
  10. const exists = require("./exists")
  11. const getPackageJson = require("./get-package-json")
  12. const cache = new Cache()
  13. const SLASH_AT_BEGIN_AND_END = /^!?\/+|^!|\/+$/gu
  14. const PARENT_RELATIVE_PATH = /^\.\./u
  15. const NEVER_IGNORED = /^(?:readme\.[^.]*|(?:licen[cs]e|changes|changelog|history)(?:\.[^.]*)?)$/iu
  16. /**
  17. * Checks whether or not a given file name is a relative path to a ancestor
  18. * directory.
  19. *
  20. * @param {string} filePath - A file name to check.
  21. * @returns {boolean} `true` if the file name is a relative path to a ancestor
  22. * directory.
  23. */
  24. function isAncestorFiles(filePath) {
  25. return PARENT_RELATIVE_PATH.test(filePath)
  26. }
  27. /**
  28. * @param {function} f - A function.
  29. * @param {function} g - A function.
  30. * @returns {function} A logical-and function of `f` and `g`.
  31. */
  32. function and(f, g) {
  33. return filePath => f(filePath) && g(filePath)
  34. }
  35. /**
  36. * @param {function} f - A function.
  37. * @param {function} g - A function.
  38. * @param {function|null} h - A function.
  39. * @returns {function} A logical-or function of `f`, `g`, and `h`.
  40. */
  41. function or(f, g, h) {
  42. return filePath => f(filePath) || g(filePath) || (h && h(filePath))
  43. }
  44. /**
  45. * @param {function} f - A function.
  46. * @returns {function} A logical-not function of `f`.
  47. */
  48. function not(f) {
  49. return filePath => !f(filePath)
  50. }
  51. /**
  52. * Creates a function which checks whether or not a given file is ignoreable.
  53. *
  54. * @param {object} p - An object of package.json.
  55. * @returns {function} A function which checks whether or not a given file is ignoreable.
  56. */
  57. function filterNeverIgnoredFiles(p) {
  58. const basedir = path.dirname(p.filePath)
  59. const mainFilePath =
  60. typeof p.main === "string" ? path.join(basedir, p.main) : null
  61. return filePath =>
  62. path.join(basedir, filePath) !== mainFilePath &&
  63. filePath !== "package.json" &&
  64. !NEVER_IGNORED.test(path.relative(basedir, filePath))
  65. }
  66. /**
  67. * Creates a function which checks whether or not a given file should be ignored.
  68. *
  69. * @param {string[]|null} files - File names of whitelist.
  70. * @returns {function|null} A function which checks whether or not a given file should be ignored.
  71. */
  72. function parseWhiteList(files) {
  73. if (!files || !Array.isArray(files)) {
  74. return null
  75. }
  76. const ig = ignore()
  77. const igN = ignore()
  78. let hasN = false
  79. for (const file of files) {
  80. if (typeof file === "string" && file) {
  81. const body = file.replace(SLASH_AT_BEGIN_AND_END, "")
  82. if (file.startsWith("!")) {
  83. igN.add(`${body}`)
  84. igN.add(`${body}/**`)
  85. hasN = true
  86. } else {
  87. ig.add(`/${body}`)
  88. ig.add(`/${body}/**`)
  89. }
  90. }
  91. }
  92. return hasN
  93. ? or(ig.createFilter(), not(igN.createFilter()))
  94. : ig.createFilter()
  95. }
  96. /**
  97. * Creates a function which checks whether or not a given file should be ignored.
  98. *
  99. * @param {string} basedir - The directory path "package.json" exists.
  100. * @param {boolean} filesFieldExists - `true` if `files` field of `package.json` exists.
  101. * @returns {function|null} A function which checks whether or not a given file should be ignored.
  102. */
  103. function parseNpmignore(basedir, filesFieldExists) {
  104. let filePath = path.join(basedir, ".npmignore")
  105. if (!exists(filePath)) {
  106. if (filesFieldExists) {
  107. return null
  108. }
  109. filePath = path.join(basedir, ".gitignore")
  110. if (!exists(filePath)) {
  111. return null
  112. }
  113. }
  114. const ig = ignore()
  115. ig.add(fs.readFileSync(filePath, "utf8"))
  116. return not(ig.createFilter())
  117. }
  118. /**
  119. * Gets an object to check whether a given path should be ignored or not.
  120. * The object is created from:
  121. *
  122. * - `files` field of `package.json`
  123. * - `.npmignore`
  124. *
  125. * @param {string} startPath - A file path to lookup.
  126. * @returns {object}
  127. * An object to check whther or not a given path should be ignored.
  128. * The object has a method `match`.
  129. * `match` returns `true` if a given file path should be ignored.
  130. */
  131. module.exports = function getNpmignore(startPath) {
  132. const retv = { match: isAncestorFiles }
  133. const p = getPackageJson(startPath)
  134. if (p) {
  135. const data = cache.get(p.filePath)
  136. if (data) {
  137. return data
  138. }
  139. const filesIgnore = parseWhiteList(p.files)
  140. const npmignoreIgnore = parseNpmignore(
  141. path.dirname(p.filePath),
  142. Boolean(filesIgnore)
  143. )
  144. if (filesIgnore && npmignoreIgnore) {
  145. retv.match = and(
  146. filterNeverIgnoredFiles(p),
  147. or(isAncestorFiles, filesIgnore, npmignoreIgnore)
  148. )
  149. } else if (filesIgnore) {
  150. retv.match = and(
  151. filterNeverIgnoredFiles(p),
  152. or(isAncestorFiles, filesIgnore)
  153. )
  154. } else if (npmignoreIgnore) {
  155. retv.match = and(
  156. filterNeverIgnoredFiles(p),
  157. or(isAncestorFiles, npmignoreIgnore)
  158. )
  159. }
  160. cache.set(p.filePath, retv)
  161. }
  162. return retv
  163. }