index.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. // A simple implementation of make-array
  2. function makeArray (subject) {
  3. return Array.isArray(subject)
  4. ? subject
  5. : [subject]
  6. }
  7. const EMPTY = ''
  8. const SPACE = ' '
  9. const ESCAPE = '\\'
  10. const REGEX_TEST_BLANK_LINE = /^\s+$/
  11. const REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/
  12. const REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/
  13. const REGEX_SPLITALL_CRLF = /\r?\n/g
  14. // /foo,
  15. // ./foo,
  16. // ../foo,
  17. // .
  18. // ..
  19. const REGEX_TEST_INVALID_PATH = /^\.*\/|^\.+$/
  20. const SLASH = '/'
  21. const KEY_IGNORE = typeof Symbol !== 'undefined'
  22. ? Symbol.for('node-ignore')
  23. /* istanbul ignore next */
  24. : 'node-ignore'
  25. const define = (object, key, value) =>
  26. Object.defineProperty(object, key, {value})
  27. const REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g
  28. const RETURN_FALSE = () => false
  29. // Sanitize the range of a regular expression
  30. // The cases are complicated, see test cases for details
  31. const sanitizeRange = range => range.replace(
  32. REGEX_REGEXP_RANGE,
  33. (match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0)
  34. ? match
  35. // Invalid range (out of order) which is ok for gitignore rules but
  36. // fatal for JavaScript regular expression, so eliminate it.
  37. : EMPTY
  38. )
  39. // See fixtures #59
  40. const cleanRangeBackSlash = slashes => {
  41. const {length} = slashes
  42. return slashes.slice(0, length - length % 2)
  43. }
  44. // > If the pattern ends with a slash,
  45. // > it is removed for the purpose of the following description,
  46. // > but it would only find a match with a directory.
  47. // > In other words, foo/ will match a directory foo and paths underneath it,
  48. // > but will not match a regular file or a symbolic link foo
  49. // > (this is consistent with the way how pathspec works in general in Git).
  50. // '`foo/`' will not match regular file '`foo`' or symbolic link '`foo`'
  51. // -> ignore-rules will not deal with it, because it costs extra `fs.stat` call
  52. // you could use option `mark: true` with `glob`
  53. // '`foo/`' should not continue with the '`..`'
  54. const REPLACERS = [
  55. // > Trailing spaces are ignored unless they are quoted with backslash ("\")
  56. [
  57. // (a\ ) -> (a )
  58. // (a ) -> (a)
  59. // (a \ ) -> (a )
  60. /\\?\s+$/,
  61. match => match.indexOf('\\') === 0
  62. ? SPACE
  63. : EMPTY
  64. ],
  65. // replace (\ ) with ' '
  66. [
  67. /\\\s/g,
  68. () => SPACE
  69. ],
  70. // Escape metacharacters
  71. // which is written down by users but means special for regular expressions.
  72. // > There are 12 characters with special meanings:
  73. // > - the backslash \,
  74. // > - the caret ^,
  75. // > - the dollar sign $,
  76. // > - the period or dot .,
  77. // > - the vertical bar or pipe symbol |,
  78. // > - the question mark ?,
  79. // > - the asterisk or star *,
  80. // > - the plus sign +,
  81. // > - the opening parenthesis (,
  82. // > - the closing parenthesis ),
  83. // > - and the opening square bracket [,
  84. // > - the opening curly brace {,
  85. // > These special characters are often called "metacharacters".
  86. [
  87. /[\\$.|*+(){^]/g,
  88. match => `\\${match}`
  89. ],
  90. [
  91. // > a question mark (?) matches a single character
  92. /(?!\\)\?/g,
  93. () => '[^/]'
  94. ],
  95. // leading slash
  96. [
  97. // > A leading slash matches the beginning of the pathname.
  98. // > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
  99. // A leading slash matches the beginning of the pathname
  100. /^\//,
  101. () => '^'
  102. ],
  103. // replace special metacharacter slash after the leading slash
  104. [
  105. /\//g,
  106. () => '\\/'
  107. ],
  108. [
  109. // > A leading "**" followed by a slash means match in all directories.
  110. // > For example, "**/foo" matches file or directory "foo" anywhere,
  111. // > the same as pattern "foo".
  112. // > "**/foo/bar" matches file or directory "bar" anywhere that is directly
  113. // > under directory "foo".
  114. // Notice that the '*'s have been replaced as '\\*'
  115. /^\^*\\\*\\\*\\\//,
  116. // '**/foo' <-> 'foo'
  117. () => '^(?:.*\\/)?'
  118. ],
  119. // starting
  120. [
  121. // there will be no leading '/'
  122. // (which has been replaced by section "leading slash")
  123. // If starts with '**', adding a '^' to the regular expression also works
  124. /^(?=[^^])/,
  125. function startingReplacer () {
  126. // If has a slash `/` at the beginning or middle
  127. return !/\/(?!$)/.test(this)
  128. // > Prior to 2.22.1
  129. // > If the pattern does not contain a slash /,
  130. // > Git treats it as a shell glob pattern
  131. // Actually, if there is only a trailing slash,
  132. // git also treats it as a shell glob pattern
  133. // After 2.22.1 (compatible but clearer)
  134. // > If there is a separator at the beginning or middle (or both)
  135. // > of the pattern, then the pattern is relative to the directory
  136. // > level of the particular .gitignore file itself.
  137. // > Otherwise the pattern may also match at any level below
  138. // > the .gitignore level.
  139. ? '(?:^|\\/)'
  140. // > Otherwise, Git treats the pattern as a shell glob suitable for
  141. // > consumption by fnmatch(3)
  142. : '^'
  143. }
  144. ],
  145. // two globstars
  146. [
  147. // Use lookahead assertions so that we could match more than one `'/**'`
  148. /\\\/\\\*\\\*(?=\\\/|$)/g,
  149. // Zero, one or several directories
  150. // should not use '*', or it will be replaced by the next replacer
  151. // Check if it is not the last `'/**'`
  152. (_, index, str) => index + 6 < str.length
  153. // case: /**/
  154. // > A slash followed by two consecutive asterisks then a slash matches
  155. // > zero or more directories.
  156. // > For example, "a/**/b" matches "a/b", "a/x/b", "a/x/y/b" and so on.
  157. // '/**/'
  158. ? '(?:\\/[^\\/]+)*'
  159. // case: /**
  160. // > A trailing `"/**"` matches everything inside.
  161. // #21: everything inside but it should not include the current folder
  162. : '\\/.+'
  163. ],
  164. // intermediate wildcards
  165. [
  166. // Never replace escaped '*'
  167. // ignore rule '\*' will match the path '*'
  168. // 'abc.*/' -> go
  169. // 'abc.*' -> skip this rule
  170. /(^|[^\\]+)\\\*(?=.+)/g,
  171. // '*.js' matches '.js'
  172. // '*.js' doesn't match 'abc'
  173. (_, p1) => `${p1}[^\\/]*`
  174. ],
  175. [
  176. // unescape, revert step 3 except for back slash
  177. // For example, if a user escape a '\\*',
  178. // after step 3, the result will be '\\\\\\*'
  179. /\\\\\\(?=[$.|*+(){^])/g,
  180. () => ESCAPE
  181. ],
  182. [
  183. // '\\\\' -> '\\'
  184. /\\\\/g,
  185. () => ESCAPE
  186. ],
  187. [
  188. // > The range notation, e.g. [a-zA-Z],
  189. // > can be used to match one of the characters in a range.
  190. // `\` is escaped by step 3
  191. /(\\)?\[([^\]/]*?)(\\*)($|\])/g,
  192. (match, leadEscape, range, endEscape, close) => leadEscape === ESCAPE
  193. // '\\[bar]' -> '\\\\[bar\\]'
  194. ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}`
  195. : close === ']'
  196. ? endEscape.length % 2 === 0
  197. // A normal case, and it is a range notation
  198. // '[bar]'
  199. // '[bar\\\\]'
  200. ? `[${sanitizeRange(range)}${endEscape}]`
  201. // Invalid range notaton
  202. // '[bar\\]' -> '[bar\\\\]'
  203. : '[]'
  204. : '[]'
  205. ],
  206. // ending
  207. [
  208. // 'js' will not match 'js.'
  209. // 'ab' will not match 'abc'
  210. /(?:[^*])$/,
  211. // WTF!
  212. // https://git-scm.com/docs/gitignore
  213. // changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1)
  214. // which re-fixes #24, #38
  215. // > If there is a separator at the end of the pattern then the pattern
  216. // > will only match directories, otherwise the pattern can match both
  217. // > files and directories.
  218. // 'js*' will not match 'a.js'
  219. // 'js/' will not match 'a.js'
  220. // 'js' will match 'a.js' and 'a.js/'
  221. match => /\/$/.test(match)
  222. // foo/ will not match 'foo'
  223. ? `${match}$`
  224. // foo matches 'foo' and 'foo/'
  225. : `${match}(?=$|\\/$)`
  226. ],
  227. // trailing wildcard
  228. [
  229. /(\^|\\\/)?\\\*$/,
  230. (_, p1) => {
  231. const prefix = p1
  232. // '\^':
  233. // '/*' does not match EMPTY
  234. // '/*' does not match everything
  235. // '\\\/':
  236. // 'abc/*' does not match 'abc/'
  237. ? `${p1}[^/]+`
  238. // 'a*' matches 'a'
  239. // 'a*' matches 'aa'
  240. : '[^/]*'
  241. return `${prefix}(?=$|\\/$)`
  242. }
  243. ],
  244. ]
  245. // A simple cache, because an ignore rule only has only one certain meaning
  246. const regexCache = Object.create(null)
  247. // @param {pattern}
  248. const makeRegex = (pattern, ignoreCase) => {
  249. let source = regexCache[pattern]
  250. if (!source) {
  251. source = REPLACERS.reduce(
  252. (prev, current) => prev.replace(current[0], current[1].bind(pattern)),
  253. pattern
  254. )
  255. regexCache[pattern] = source
  256. }
  257. return ignoreCase
  258. ? new RegExp(source, 'i')
  259. : new RegExp(source)
  260. }
  261. const isString = subject => typeof subject === 'string'
  262. // > A blank line matches no files, so it can serve as a separator for readability.
  263. const checkPattern = pattern => pattern
  264. && isString(pattern)
  265. && !REGEX_TEST_BLANK_LINE.test(pattern)
  266. // > A line starting with # serves as a comment.
  267. && pattern.indexOf('#') !== 0
  268. const splitPattern = pattern => pattern.split(REGEX_SPLITALL_CRLF)
  269. class IgnoreRule {
  270. constructor (
  271. origin,
  272. pattern,
  273. negative,
  274. regex
  275. ) {
  276. this.origin = origin
  277. this.pattern = pattern
  278. this.negative = negative
  279. this.regex = regex
  280. }
  281. }
  282. const createRule = (pattern, ignoreCase) => {
  283. const origin = pattern
  284. let negative = false
  285. // > An optional prefix "!" which negates the pattern;
  286. if (pattern.indexOf('!') === 0) {
  287. negative = true
  288. pattern = pattern.substr(1)
  289. }
  290. pattern = pattern
  291. // > Put a backslash ("\") in front of the first "!" for patterns that
  292. // > begin with a literal "!", for example, `"\!important!.txt"`.
  293. .replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, '!')
  294. // > Put a backslash ("\") in front of the first hash for patterns that
  295. // > begin with a hash.
  296. .replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, '#')
  297. const regex = makeRegex(pattern, ignoreCase)
  298. return new IgnoreRule(
  299. origin,
  300. pattern,
  301. negative,
  302. regex
  303. )
  304. }
  305. const throwError = (message, Ctor) => {
  306. throw new Ctor(message)
  307. }
  308. const checkPath = (path, originalPath, doThrow) => {
  309. if (!isString(path)) {
  310. return doThrow(
  311. `path must be a string, but got \`${originalPath}\``,
  312. TypeError
  313. )
  314. }
  315. // We don't know if we should ignore EMPTY, so throw
  316. if (!path) {
  317. return doThrow(`path must not be empty`, TypeError)
  318. }
  319. // Check if it is a relative path
  320. if (checkPath.isNotRelative(path)) {
  321. const r = '`path.relative()`d'
  322. return doThrow(
  323. `path should be a ${r} string, but got "${originalPath}"`,
  324. RangeError
  325. )
  326. }
  327. return true
  328. }
  329. const isNotRelative = path => REGEX_TEST_INVALID_PATH.test(path)
  330. checkPath.isNotRelative = isNotRelative
  331. checkPath.convert = p => p
  332. class Ignore {
  333. constructor ({
  334. ignorecase = true,
  335. ignoreCase = ignorecase,
  336. allowRelativePaths = false
  337. } = {}) {
  338. define(this, KEY_IGNORE, true)
  339. this._rules = []
  340. this._ignoreCase = ignoreCase
  341. this._allowRelativePaths = allowRelativePaths
  342. this._initCache()
  343. }
  344. _initCache () {
  345. this._ignoreCache = Object.create(null)
  346. this._testCache = Object.create(null)
  347. }
  348. _addPattern (pattern) {
  349. // #32
  350. if (pattern && pattern[KEY_IGNORE]) {
  351. this._rules = this._rules.concat(pattern._rules)
  352. this._added = true
  353. return
  354. }
  355. if (checkPattern(pattern)) {
  356. const rule = createRule(pattern, this._ignoreCase)
  357. this._added = true
  358. this._rules.push(rule)
  359. }
  360. }
  361. // @param {Array<string> | string | Ignore} pattern
  362. add (pattern) {
  363. this._added = false
  364. makeArray(
  365. isString(pattern)
  366. ? splitPattern(pattern)
  367. : pattern
  368. ).forEach(this._addPattern, this)
  369. // Some rules have just added to the ignore,
  370. // making the behavior changed.
  371. if (this._added) {
  372. this._initCache()
  373. }
  374. return this
  375. }
  376. // legacy
  377. addPattern (pattern) {
  378. return this.add(pattern)
  379. }
  380. // | ignored : unignored
  381. // negative | 0:0 | 0:1 | 1:0 | 1:1
  382. // -------- | ------- | ------- | ------- | --------
  383. // 0 | TEST | TEST | SKIP | X
  384. // 1 | TESTIF | SKIP | TEST | X
  385. // - SKIP: always skip
  386. // - TEST: always test
  387. // - TESTIF: only test if checkUnignored
  388. // - X: that never happen
  389. // @param {boolean} whether should check if the path is unignored,
  390. // setting `checkUnignored` to `false` could reduce additional
  391. // path matching.
  392. // @returns {TestResult} true if a file is ignored
  393. _testOne (path, checkUnignored) {
  394. let ignored = false
  395. let unignored = false
  396. this._rules.forEach(rule => {
  397. const {negative} = rule
  398. if (
  399. unignored === negative && ignored !== unignored
  400. || negative && !ignored && !unignored && !checkUnignored
  401. ) {
  402. return
  403. }
  404. const matched = rule.regex.test(path)
  405. if (matched) {
  406. ignored = !negative
  407. unignored = negative
  408. }
  409. })
  410. return {
  411. ignored,
  412. unignored
  413. }
  414. }
  415. // @returns {TestResult}
  416. _test (originalPath, cache, checkUnignored, slices) {
  417. const path = originalPath
  418. // Supports nullable path
  419. && checkPath.convert(originalPath)
  420. checkPath(
  421. path,
  422. originalPath,
  423. this._allowRelativePaths
  424. ? RETURN_FALSE
  425. : throwError
  426. )
  427. return this._t(path, cache, checkUnignored, slices)
  428. }
  429. _t (path, cache, checkUnignored, slices) {
  430. if (path in cache) {
  431. return cache[path]
  432. }
  433. if (!slices) {
  434. // path/to/a.js
  435. // ['path', 'to', 'a.js']
  436. slices = path.split(SLASH)
  437. }
  438. slices.pop()
  439. // If the path has no parent directory, just test it
  440. if (!slices.length) {
  441. return cache[path] = this._testOne(path, checkUnignored)
  442. }
  443. const parent = this._t(
  444. slices.join(SLASH) + SLASH,
  445. cache,
  446. checkUnignored,
  447. slices
  448. )
  449. // If the path contains a parent directory, check the parent first
  450. return cache[path] = parent.ignored
  451. // > It is not possible to re-include a file if a parent directory of
  452. // > that file is excluded.
  453. ? parent
  454. : this._testOne(path, checkUnignored)
  455. }
  456. ignores (path) {
  457. return this._test(path, this._ignoreCache, false).ignored
  458. }
  459. createFilter () {
  460. return path => !this.ignores(path)
  461. }
  462. filter (paths) {
  463. return makeArray(paths).filter(this.createFilter())
  464. }
  465. // @returns {TestResult}
  466. test (path) {
  467. return this._test(path, this._testCache, true)
  468. }
  469. }
  470. const factory = options => new Ignore(options)
  471. const isPathValid = path =>
  472. checkPath(path && checkPath.convert(path), path, RETURN_FALSE)
  473. factory.isPathValid = isPathValid
  474. // Fixes typescript
  475. factory.default = factory
  476. module.exports = factory
  477. // Windows
  478. // --------------------------------------------------------------
  479. /* istanbul ignore if */
  480. if (
  481. // Detect `process` so that it can run in browsers.
  482. typeof process !== 'undefined'
  483. && (
  484. process.env && process.env.IGNORE_TEST_WIN32
  485. || process.platform === 'win32'
  486. )
  487. ) {
  488. /* eslint no-control-regex: "off" */
  489. const makePosix = str => /^\\\\\?\\/.test(str)
  490. || /["<>|\u0000-\u001F]+/u.test(str)
  491. ? str
  492. : str.replace(/\\/g, '/')
  493. checkPath.convert = makePosix
  494. // 'C:\\foo' <- 'C:\\foo' has been converted to 'C:/'
  495. // 'd:\\foo'
  496. const REGIX_IS_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i
  497. checkPath.isNotRelative = path =>
  498. REGIX_IS_WINDOWS_PATH_ABSOLUTE.test(path)
  499. || isNotRelative(path)
  500. }