1// Alchemy modifications are licensed under Apache-2.0.
2// This file includes third-party code; see /THIRD_PARTY_LICENSES.md.
3//
4// TypeScript port of `ignore` (https://github.com/kaelzhang/node-ignore) v7.0.5.
5// Copyright (c) 2013 Kael Zhang <i@kael.me>, contributors. MIT License.
6
7function makeArray<T>(subject: T | readonly T[]): T[] {
8 return Array.isArray(subject) ? [...subject] : [subject as T];
9}
10
11const UNDEFINED = undefined;
12const EMPTY = "";
13const SPACE = " ";
14const ESCAPE = "\\";
15const REGEX_TEST_BLANK_LINE = /^\s+$/;
16const REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/;
17const REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/;
18const REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/;
19const REGEX_SPLITALL_CRLF = /\r?\n/g;
20
21// Invalid:
22// - /foo,
23// - ./foo,
24// - ../foo,
25// - .
26// - ..
27// Valid:
28// - .foo
29const REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/;
30
31const REGEX_TEST_TRAILING_SLASH = /\/$/;
32
33const SLASH = "/";
34
35// Do not use ternary expression here, since "istanbul ignore next" is buggy
36let TMP_KEY_IGNORE: string | symbol = "node-ignore";
37/* istanbul ignore else */
38if (typeof Symbol !== "undefined") {
39 TMP_KEY_IGNORE = Symbol.for("node-ignore");
40}
41const KEY_IGNORE = TMP_KEY_IGNORE;
42
43const define = <T>(object: object, key: PropertyKey, value: T): T => {
44 Object.defineProperty(object, key, { value });
45 return value;
46};
47
48const REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g;
49
50const RETURN_FALSE = () => false;
51
52// Sanitize the range of a regular expression
53// The cases are complicated, see test cases for details
54const sanitizeRange = (range: string): string =>
55 range.replace(REGEX_REGEXP_RANGE, (match, from, to) =>
56 from.charCodeAt(0) <= to.charCodeAt(0)
57 ? match
58 : // Invalid range (out of order) which is ok for gitignore rules but
59 // fatal for JavaScript regular expression, so eliminate it.
60 EMPTY,
61 );
62
63// > An optional `!` or `^` at the start of a class negates it, so that it
64// > matches any character not in the set. (gitignore(5), fnmatch(3))
65// The leading `^` has already been escaped to `\^` by the metacharacter
66// escaper, so we strip the literal `!` or escaped `^` and emit a single
67// regex `^` which is the JavaScript negation token.
68const negateRange = (range: string): string =>
69 range.startsWith("!") || range.startsWith("\\^")
70 ? `^${range.slice(range[0] === "!" ? 1 : 2)}`
71 : range;
72
73// See fixtures #59
74const cleanRangeBackSlash = (slashes: string): string => {
75 const { length } = slashes;
76 return slashes.slice(0, length - (length % 2));
77};
78
79// > If the pattern ends with a slash,
80// > it is removed for the purpose of the following description,
81// > but it would only find a match with a directory.
82// > In other words, foo/ will match a directory foo and paths underneath it,
83// > but will not match a regular file or a symbolic link foo
84// > (this is consistent with the way how pathspec works in general in Git).
85// '`foo/`' will not match regular file '`foo`' or symbolic link '`foo`'
86// -> ignore-rules will not deal with it, because it costs extra `fs.stat` call
87// you could use option `mark: true` with `glob`
88
89// '`foo/`' should not continue with the '`..`'
90type Replacer = (this: string, ...args: any[]) => string;
91
92const REPLACERS: Array<readonly [RegExp, Replacer]> = [
93 [
94 // Remove BOM
95 // TODO:
96 // Other similar zero-width characters?
97 /^\uFEFF/,
98 () => EMPTY,
99 ],
100
101 // > Trailing spaces are ignored unless they are quoted with backslash ("\")
102 [
103 // (a\ ) -> (a )
104 // (a ) -> (a)
105 // (a ) -> (a)
106 // (a \ ) -> (a )
107 /((?:\\\\)*?)(\\?\s+)$/,
108 (_, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY),
109 ],
110
111 // Replace (\ ) with ' '
112 // (\ ) -> ' '
113 // (\\ ) -> '\\ '
114 // (\\\ ) -> '\\ '
115 [
116 /(\\+?)\s/g,
117 (_, m1) => {
118 const { length } = m1;
119 return m1.slice(0, length - (length % 2)) + SPACE;
120 },
121 ],
122
123 // Escape metacharacters
124 // which is written down by users but means special for regular expressions.
125
126 // > There are 12 characters with special meanings:
127 // > - the backslash \,
128 // > - the caret ^,
129 // > - the dollar sign $,
130 // > - the period or dot .,
131 // > - the vertical bar or pipe symbol |,
132 // > - the question mark ?,
133 // > - the asterisk or star *,
134 // > - the plus sign +,
135 // > - the opening parenthesis (,
136 // > - the closing parenthesis ),
137 // > - and the opening square bracket [,
138 // > - the opening curly brace {,
139 // > These special characters are often called "metacharacters".
140 [/[\\$.|*+(){^]/g, (match) => `\\${match}`],
141
142 [
143 // > a question mark (?) matches a single character
144 /(?!\\)\?/g,
145 () => "[^/]",
146 ],
147
148 // leading slash
149 [
150 // > A leading slash matches the beginning of the pathname.
151 // > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c".
152 // A leading slash matches the beginning of the pathname
153 /^\//,
154 () => "^",
155 ],
156
157 // replace special metacharacter slash after the leading slash
158 [/\//g, () => "\\/"],
159
160 [
161 // > A leading "**" followed by a slash means match in all directories.
162 // > For example, "**/foo" matches file or directory "foo" anywhere,
163 // > the same as pattern "foo".
164 // > "**/foo/bar" matches file or directory "bar" anywhere that is directly
165 // > under directory "foo".
166 // Notice that the '*'s have been replaced as '\\*'
167 /^\^*(?:\\\*\\\*\\\/)+/,
168
169 // '**/foo' <-> 'foo'
170 () => "^(?:.*\\/)?",
171 ],
172
173 // starting
174 [
175 // there will be no leading '/'
176 // (which has been replaced by section "leading slash")
177 // If starts with '**', adding a '^' to the regular expression also works
178 /^(?=[^^])/,
179 function startingReplacer(this: string) {
180 // If has a slash `/` at the beginning or middle
181 return !/\/(?!$)/.test(this)
182 ? // > Prior to 2.22.1
183 // > If the pattern does not contain a slash /,
184 // > Git treats it as a shell glob pattern
185 // Actually, if there is only a trailing slash,
186 // git also treats it as a shell glob pattern
187
188 // After 2.22.1 (compatible but clearer)
189 // > If there is a separator at the beginning or middle (or both)
190 // > of the pattern, then the pattern is relative to the directory
191 // > level of the particular .gitignore file itself.
192 // > Otherwise the pattern may also match at any level below
193 // > the .gitignore level.
194 "(?:^|\\/)"
195 : // > Otherwise, Git treats the pattern as a shell glob suitable for
196 // > consumption by fnmatch(3)
197 "^";
198 },
199 ],
200
201 // two globstars
202 [
203 // Use lookahead assertions so that we could match more than one `'/**'`
204 /\\\/\\\*\\\*(?=\\\/|$)/g,
205
206 // Zero, one or several directories
207 // should not use '*', or it will be replaced by the next replacer
208
209 // Check if it is not the last `'/**'`
210 (_, index, str) =>
211 index + 6 < str.length
212 ? // case: /**/
213 // > A slash followed by two consecutive asterisks then a slash matches
214 // > zero or more directories.
215 // > For example, "a/**/b" matches "a/b", "a/x/b", "a/x/y/b" and so on.
216 // '/**/'
217 "(?:\\/[^\\/]+)*"
218 : // case: /**
219 // > A trailing `"/**"` matches everything inside.
220
221 // #21: everything inside but it should not include the current folder
222 "\\/.+",
223 ],
224
225 // normal intermediate wildcards
226 [
227 // Never replace escaped '*'
228 // ignore rule '\*' will match the path '*'
229
230 // 'abc.*/' -> go
231 // 'abc.*' -> skip this rule,
232 // coz trailing single wildcard will be handed by [trailing wildcard]
233 /(^|[^\\]+)(\\\*)+(?=.+)/g,
234
235 // '*.js' matches '.js'
236 // '*.js' doesn't match 'abc'
237 (_, p1, p2) => {
238 // 1.
239 // > An asterisk "*" matches anything except a slash.
240 // 2.
241 // > Other consecutive asterisks are considered regular asterisks
242 // > and will match according to the previous rules.
243 const unescaped = p2.replace(/\\\*/g, "[^\\/]*");
244 return p1 + unescaped;
245 },
246 ],
247
248 [
249 // unescape, revert step 3 except for back slash
250 // For example, if a user escape a '\\*',
251 // after step 3, the result will be '\\\\\\*'
252 /\\\\\\(?=[$.|*+(){^])/g,
253 () => ESCAPE,
254 ],
255
256 [
257 // '\\\\' -> '\\'
258 /\\\\/g,
259 () => ESCAPE,
260 ],
261
262 [
263 // > The range notation, e.g. [a-zA-Z],
264 // > can be used to match one of the characters in a range.
265
266 // `\` is escaped by step 3
267 /(\\)?\[([^\]/]*?)(\\*)($|\])/g,
268 (match, leadEscape, range, endEscape, close) =>
269 leadEscape === ESCAPE
270 ? // '\\[bar]' -> '\\\\[bar\\]'
271 `\\[${range}${cleanRangeBackSlash(endEscape)}${close}`
272 : close === "]"
273 ? endEscape.length % 2 === 0
274 ? // A normal case, and it is a range notation
275 // '[bar]'
276 // '[bar\\\\]'
277 `[${negateRange(sanitizeRange(range))}${endEscape}]`
278 : // Invalid range notaton
279 // '[bar\\]' -> '[bar\\\\]'
280 "[]"
281 : "[]",
282 ],
283
284 // ending
285 [
286 // 'js' will not match 'js.'
287 // 'ab' will not match 'abc'
288 /(?:[^*])$/,
289
290 // WTF!
291 // https://git-scm.com/docs/gitignore
292 // changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1)
293 // which re-fixes #24, #38
294
295 // > If there is a separator at the end of the pattern then the pattern
296 // > will only match directories, otherwise the pattern can match both
297 // > files and directories.
298
299 // 'js*' will not match 'a.js'
300 // 'js/' will not match 'a.js'
301 // 'js' will match 'a.js' and 'a.js/'
302 (match) =>
303 /\/$/.test(match)
304 ? // foo/ will not match 'foo'
305 `${match}$`
306 : // foo matches 'foo' and 'foo/'
307 `${match}(?=$|\\/$)`,
308 ],
309];
310
311const REGEX_REPLACE_TRAILING_WILDCARD = /(^|\\\/)?\\\*$/;
312const MODE_IGNORE = "regex";
313const MODE_CHECK_IGNORE = "checkRegex";
314
315type MatcherMode = typeof MODE_IGNORE | typeof MODE_CHECK_IGNORE;
316
317const TRAILING_WILD_CARD_REPLACERS: Record<MatcherMode, Replacer> = {
318 [MODE_IGNORE](_, p1: string | undefined) {
319 const prefix = p1
320 ? // '\^':
321 // '/*' does not match EMPTY
322 // '/*' does not match everything
323
324 // '\\\/':
325 // 'abc/*' does not match 'abc/'
326 `${p1}[^/]+`
327 : // 'a*' matches 'a'
328 // 'a*' matches 'aa'
329 "[^/]*";
330
331 return `${prefix}(?=$|\\/$)`;
332 },
333
334 [MODE_CHECK_IGNORE](_, p1: string | undefined) {
335 // When doing `git check-ignore`
336 const prefix = p1
337 ? // '\\\/':
338 // 'abc/*' DOES match 'abc/' !
339 `${p1}[^/]*`
340 : // 'a*' matches 'a'
341 // 'a*' matches 'aa'
342 "[^/]*";
343
344 return `${prefix}(?=$|\\/$)`;
345 },
346};
347
348// @param {pattern}
349const makeRegexPrefix = (pattern: string): string =>
350 REPLACERS.reduce(
351 (prev, [matcher, replacer]) =>
352 prev.replace(matcher, replacer.bind(pattern)),
353 pattern,
354 );
355
356const isString = (subject: unknown): subject is string =>
357 typeof subject === "string";
358
359// > A blank line matches no files, so it can serve as a separator for readability.
360const checkPattern = (pattern: string): boolean =>
361 Boolean(
362 pattern &&
363 !REGEX_TEST_BLANK_LINE.test(pattern) &&
364 !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern) &&
365 // > A line starting with # serves as a comment.
366 pattern.indexOf("#") !== 0,
367 );
368
369const splitPattern = (pattern: string): string[] =>
370 pattern.split(REGEX_SPLITALL_CRLF).filter(Boolean);
371
372class IgnoreRule {
373 readonly pattern: string;
374 readonly mark: string | undefined;
375 readonly negative: boolean;
376 readonly body!: string;
377 readonly ignoreCase!: boolean;
378 readonly regexPrefix!: string;
379 private _regex?: RegExp;
380 private _checkRegex?: RegExp;
381
382 constructor(
383 pattern: string,
384 mark: string | undefined,
385 body: string,
386 ignoreCase: boolean,
387 negative: boolean,
388 prefix: string,
389 ) {
390 this.pattern = pattern;
391 this.mark = mark;
392 this.negative = negative;
393
394 define(this, "body", body);
395 define(this, "ignoreCase", ignoreCase);
396 define(this, "regexPrefix", prefix);
397 }
398
399 get regex() {
400 const key = `_${MODE_IGNORE}` as const;
401
402 if (this[key]) {
403 return this[key];
404 }
405
406 return this._make(MODE_IGNORE, key);
407 }
408
409 get checkRegex() {
410 const key = `_${MODE_CHECK_IGNORE}` as const;
411
412 if (this[key]) {
413 return this[key];
414 }
415
416 return this._make(MODE_CHECK_IGNORE, key);
417 }
418
419 _make(mode: MatcherMode, key: "_regex" | "_checkRegex"): RegExp {
420 const str = this.regexPrefix.replace(
421 REGEX_REPLACE_TRAILING_WILDCARD,
422
423 // It does not need to bind pattern
424 TRAILING_WILD_CARD_REPLACERS[mode],
425 );
426
427 const regex = this.ignoreCase ? new RegExp(str, "i") : new RegExp(str);
428
429 return define(this, key, regex);
430 }
431}
432
433const createRule = (
434 { pattern, mark }: PatternParams,
435 ignoreCase: boolean,
436): IgnoreRule => {
437 let negative = false;
438 let body = pattern;
439
440 // > An optional prefix "!" which negates the pattern;
441 if (body.indexOf("!") === 0) {
442 negative = true;
443 body = body.substr(1);
444 }
445
446 body = body
447 // > Put a backslash ("\") in front of the first "!" for patterns that
448 // > begin with a literal "!", for example, `"\!important!.txt"`.
449 .replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, "!")
450 // > Put a backslash ("\") in front of the first hash for patterns that
451 // > begin with a hash.
452 .replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, "#");
453
454 const regexPrefix = makeRegexPrefix(body);
455
456 return new IgnoreRule(pattern, mark, body, ignoreCase, negative, regexPrefix);
457};
458
459class RuleManager {
460 private readonly _ignoreCase: boolean;
461 _rules: IgnoreRule[] = [];
462 private _added = false;
463
464 constructor(ignoreCase: boolean) {
465 this._ignoreCase = ignoreCase;
466 }
467
468 _add(pattern: string | Ignore | PatternParams): void {
469 // #32
470 const ignorePattern = pattern as Ignore & Record<PropertyKey, unknown>;
471 if (pattern && ignorePattern[KEY_IGNORE]) {
472 this._rules = this._rules.concat(ignorePattern._rules._rules);
473 this._added = true;
474 return;
475 }
476
477 if (isString(pattern)) {
478 pattern = {
479 pattern,
480 };
481 }
482
483 const patternParams = pattern as PatternParams;
484 if (checkPattern(patternParams.pattern)) {
485 const rule = createRule(patternParams, this._ignoreCase);
486 this._added = true;
487 this._rules.push(rule);
488 }
489 }
490
491 // @param {Array<string> | string | Ignore} pattern
492 add(
493 pattern: string | Ignore | PatternParams | readonly (string | Ignore)[],
494 ): boolean {
495 this._added = false;
496
497 const patterns: readonly (string | Ignore | PatternParams)[] = isString(
498 pattern,
499 )
500 ? splitPattern(pattern)
501 : Array.isArray(pattern)
502 ? pattern
503 : [pattern as Ignore | PatternParams];
504 patterns.forEach(this._add, this);
505
506 return this._added;
507 }
508
509 // Test one single path without recursively checking parent directories
510 //
511 // - checkUnignored `boolean` whether should check if the path is unignored,
512 // setting `checkUnignored` to `false` could reduce additional
513 // path matching.
514 // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
515
516 // @returns {TestResult} true if a file is ignored
517 test(
518 path: string,
519 checkUnignored: boolean,
520 mode: MatcherMode,
521 ): IgnoreTestResult {
522 let ignored = false;
523 let unignored = false;
524 let matchedRule: IgnoreRule | undefined;
525
526 this._rules.forEach((rule) => {
527 const { negative } = rule;
528
529 // | ignored : unignored
530 // -------- | ---------------------------------------
531 // negative | 0:0 | 0:1 | 1:0 | 1:1
532 // -------- | ------- | ------- | ------- | --------
533 // 0 | TEST | TEST | SKIP | X
534 // 1 | TESTIF | SKIP | TEST | X
535
536 // - SKIP: always skip
537 // - TEST: always test
538 // - TESTIF: only test if checkUnignored
539 // - X: that never happen
540 if (
541 (unignored === negative && ignored !== unignored) ||
542 (negative && !ignored && !unignored && !checkUnignored)
543 ) {
544 return;
545 }
546
547 const matched = rule[mode].test(path);
548
549 if (!matched) {
550 return;
551 }
552
553 ignored = !negative;
554 unignored = negative;
555
556 matchedRule = negative ? UNDEFINED : rule;
557 });
558
559 const ret: IgnoreTestResult = {
560 ignored,
561 unignored,
562 };
563
564 if (matchedRule) {
565 ret.rule = matchedRule;
566 }
567
568 return ret;
569 }
570}
571
572type ErrorConstructor = new (message: string) => Error;
573type InvalidPathHandler = (
574 message: string,
575 constructor: ErrorConstructor,
576) => boolean;
577
578const throwError: InvalidPathHandler = (message, Ctor) => {
579 throw new Ctor(message);
580};
581
582interface CheckPath {
583 (path: unknown, originalPath: unknown, doThrow: InvalidPathHandler): boolean;
584 isNotRelative: (path: string) => boolean;
585 convert: (path: string) => string;
586}
587
588const checkPath: CheckPath = (path, originalPath, doThrow) => {
589 if (!isString(path)) {
590 return doThrow(
591 `path must be a string, but got \`${originalPath}\``,
592 TypeError,
593 );
594 }
595
596 // We don't know if we should ignore EMPTY, so throw
597 if (!path) {
598 return doThrow(`path must not be empty`, TypeError);
599 }
600
601 // Check if it is a relative path
602 if (checkPath.isNotRelative(path)) {
603 const r = "`path.relative()`d";
604 return doThrow(
605 `path should be a ${r} string, but got "${originalPath}"`,
606 RangeError,
607 );
608 }
609
610 return true;
611};
612
613const isNotRelative = (path: string): boolean =>
614 REGEX_TEST_INVALID_PATH.test(path);
615
616checkPath.isNotRelative = isNotRelative;
617
618// On windows, the following function will be replaced
619/* istanbul ignore next */
620checkPath.convert = (path) => path;
621
622class Ignore {
623 readonly [KEY_IGNORE]?: true;
624 readonly _rules: RuleManager;
625 private readonly _strictPathCheck: boolean;
626 private _ignoreCache!: Record<string, IgnoreTestResult>;
627 private _testCache!: Record<string, IgnoreTestResult>;
628
629 constructor({
630 ignorecase = true,
631 ignoreCase = ignorecase,
632 allowRelativePaths = false,
633 }: IgnoreOptions = {}) {
634 define(this, KEY_IGNORE, true);
635
636 this._rules = new RuleManager(ignoreCase);
637 this._strictPathCheck = !allowRelativePaths;
638 this._initCache();
639 }
640
641 _initCache(): void {
642 // A cache for the result of `.ignores()`
643 this._ignoreCache = Object.create(null);
644
645 // A cache for the result of `.test()`
646 this._testCache = Object.create(null);
647 }
648
649 add(
650 pattern: string | Ignore | readonly (string | Ignore)[] | PatternParams,
651 ): this {
652 if (this._rules.add(pattern)) {
653 // Some rules have just added to the ignore,
654 // making the behavior changed,
655 // so we need to re-initialize the result cache
656 this._initCache();
657 }
658
659 return this;
660 }
661
662 // legacy
663 addPattern(pattern: string | readonly string[]): this {
664 return this.add(pattern);
665 }
666
667 // @returns {TestResult}
668 _test(
669 originalPath: string,
670 cache: Record<string, IgnoreTestResult>,
671 checkUnignored: boolean,
672 slices?: string[],
673 ): IgnoreTestResult {
674 const path =
675 originalPath &&
676 // Supports nullable path
677 checkPath.convert(originalPath);
678
679 checkPath(
680 path,
681 originalPath,
682 this._strictPathCheck ? throwError : RETURN_FALSE,
683 );
684
685 return this._t(path, cache, checkUnignored, slices);
686 }
687
688 checkIgnore(path: string): IgnoreTestResult {
689 // If the path doest not end with a slash, `.ignores()` is much equivalent
690 // to `git check-ignore`
691 if (!REGEX_TEST_TRAILING_SLASH.test(path)) {
692 return this.test(path);
693 }
694
695 const slices = path.split(SLASH).filter(Boolean);
696 slices.pop();
697
698 if (slices.length) {
699 const parent = this._t(
700 slices.join(SLASH) + SLASH,
701 this._testCache,
702 true,
703 slices,
704 );
705
706 if (parent.ignored) {
707 return parent;
708 }
709 }
710
711 return this._rules.test(path, false, MODE_CHECK_IGNORE);
712 }
713
714 _t(
715 // The path to be tested
716 path: string,
717
718 // The cache for the result of a certain checking
719 cache: Record<string, IgnoreTestResult>,
720
721 // Whether should check if the path is unignored
722 checkUnignored: boolean,
723
724 // The path slices
725 slices?: string[],
726 ): IgnoreTestResult {
727 if (path in cache) {
728 return cache[path];
729 }
730
731 if (!slices) {
732 // path/to/a.js
733 // ['path', 'to', 'a.js']
734 slices = path.split(SLASH).filter(Boolean);
735 }
736
737 slices.pop();
738
739 // If the path has no parent directory, just test it
740 if (!slices.length) {
741 return (cache[path] = this._rules.test(
742 path,
743 checkUnignored,
744 MODE_IGNORE,
745 ));
746 }
747
748 const parent = this._t(
749 slices.join(SLASH) + SLASH,
750 cache,
751 checkUnignored,
752 slices,
753 );
754
755 // If the path contains a parent directory, check the parent first
756 return (cache[path] = parent.ignored
757 ? // > It is not possible to re-include a file if a parent directory of
758 // > that file is excluded.
759 parent
760 : this._rules.test(path, checkUnignored, MODE_IGNORE));
761 }
762
763 ignores(path: string): boolean {
764 return this._test(path, this._ignoreCache, false).ignored;
765 }
766
767 createFilter(): (path: string) => boolean {
768 return (path: string) => !this.ignores(path);
769 }
770
771 filter(paths: readonly string[]): string[] {
772 return makeArray(paths).filter(this.createFilter());
773 }
774
775 // @returns {TestResult}
776 test(path: string): IgnoreTestResult {
777 return this._test(path, this._testCache, true);
778 }
779}
780
781const factory = (options?: IgnoreOptions): Ignore => new Ignore(options);
782
783const isPathValid = (path: string): boolean =>
784 checkPath(path && checkPath.convert(path), path, RETURN_FALSE);
785
786/* istanbul ignore next */
787const setupWindows = () => {
788 /* eslint no-control-regex: "off" */
789 const makePosix = (str: string): string =>
790 /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str)
791 ? str
792 : str.replace(/\\/g, "/");
793
794 checkPath.convert = makePosix;
795
796 // 'C:\\foo' <- 'C:\\foo' has been converted to 'C:/'
797 // 'd:\\foo'
798 const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
799 checkPath.isNotRelative = (path: string) =>
800 REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path) || isNotRelative(path);
801};
802
803// Windows
804// --------------------------------------------------------------
805/* istanbul ignore next */
806if (
807 // Detect `process` so that it can run in browsers.
808 typeof process !== "undefined" &&
809 process.platform === "win32"
810) {
811 setupWindows();
812}
813
814// ES module exports (upstream uses `module.exports`). Upstream also exposes
815// `setupWindows` via `Symbol.for("setupWindows")` for its own test suite; we
816// drop that hook since it's unused.
817
818export default factory as unknown as (
819 options?: IgnoreOptions,
820) => IgnoreInstance;
821export { Ignore, isPathValid };
822
823// --- Type declarations -----------------------------------------------------
824// (not part of the verbatim upstream source; mirrors upstream `index.d.ts`)
825
826type Pathname = string;
827
828interface PatternParams {
829 pattern: string;
830 mark?: string;
831}
832
833interface IgnoreTestResult {
834 ignored: boolean;
835 unignored: boolean;
836 rule?: IgnoreRule;
837}
838
839export interface IgnoreOptions {
840 ignorecase?: boolean;
841 /** For compatibility. */
842 ignoreCase?: boolean;
843 allowRelativePaths?: boolean;
844}
845
846export interface IgnoreInstance {
847 add(
848 patterns:
849 | string
850 | IgnoreInstance
851 | readonly (string | IgnoreInstance)[]
852 | PatternParams,
853 ): this;
854 filter(pathnames: readonly Pathname[]): Pathname[];
855 createFilter(): (pathname: Pathname) => boolean;
856 ignores(pathname: Pathname): boolean;
857 test(pathname: Pathname): {
858 ignored: boolean;
859 unignored: boolean;
860 rule?: { pattern: string; mark?: string; negative: boolean };
861 };
862 checkIgnore(pathname: Pathname): {
863 ignored: boolean;
864 unignored: boolean;
865 rule?: { pattern: string; mark?: string; negative: boolean };
866 };
867}