index.js 16 KB

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