├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── LICENSE-MIT ├── README.md ├── index.d.ts ├── index.js ├── package.json └── test ├── fixtures ├── .aignore ├── .fakeignore ├── .gitignore-with-BOM ├── .ignore-issue-2 └── cases.js ├── git-check-ignore.test.js ├── ignore.test.js ├── import ├── simple.cjs ├── simple.mjs └── simple.ts ├── others.test.js └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | // Defaults to es2015 4 | "@babel/preset-env" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build 2 | /coverage 3 | /no-track 4 | /test/import/*.js 5 | /legacy.js 6 | /index.mjs 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | const rules = { 4 | 'no-underscore-dangle': ['error', { 5 | allowAfterThis: true, 6 | enforceInMethodNames: false, 7 | // node-ignore only 8 | allow: ['_rules', '_test'] 9 | }], 10 | 11 | 'operator-linebreak': 0, 12 | 13 | indent: ['error', 2, { 14 | MemberExpression: 0, 15 | 16 | // Eslint bug 17 | ignoreComments: true 18 | }] 19 | } 20 | 21 | if (process.platform === 'win32') { 22 | // Ignore linebreak-style on Windows, due to a bug of eslint 23 | rules['linebreak-style'] = 0 24 | } 25 | 26 | module.exports = { 27 | // Uses `require.resolve` to support npm linked eslint-config 28 | extends: require.resolve('eslint-config-ostai'), 29 | root: true, 30 | rules, 31 | overrides: [ 32 | { 33 | files: ['*.ts', '*.cts', '*.mts', '*.cjs', '*.mjs'], 34 | extends: ['plugin:@typescript-eslint/recommended'], 35 | parser: '@typescript-eslint/parser', 36 | plugins: ['@typescript-eslint'], 37 | rules: { 38 | '@typescript-eslint/no-unused-vars': 'off' 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: kaelzhang 4 | patreon: # kaelzhang 5 | open_collective: node-ignore 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest, macos-latest] 16 | node-version: [20.x] 17 | 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - uses: actions/checkout@v1 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - name: npm install, build, and linting 27 | run: | 28 | npm install 29 | npm run build --if-present 30 | npm run lint 31 | npm run test:ts 32 | env: 33 | CI: true 34 | - name: test nodeResolution 16 35 | if: runner.os != 'Windows' 36 | run: | 37 | npm run test:16 38 | - name: cases 39 | run: | 40 | npm run test:cases 41 | - name: upload to codecov 42 | if: runner.os == 'Linux' 43 | uses: codecov/codecov-action@v5 44 | with: 45 | token: ${{ secrets.CODECOV_TOKEN }} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # test 2 | /.tap 3 | /test/import/*.js 4 | 5 | # coverage 6 | coverage 7 | .nyc_output 8 | /legacy.js 9 | 10 | # yarn 11 | *.lock 12 | 13 | # php files 14 | index.php 15 | 16 | # Numerous always-ignore extensions 17 | *.bak 18 | *.patch 19 | *.diff 20 | *.err 21 | *.orig 22 | *.log 23 | *.rej 24 | *.swo 25 | *.swp 26 | *.zip 27 | *.vi 28 | *~ 29 | *.sass-cache 30 | package-lock.json 31 | 32 | # npm package 33 | *.tgz 34 | 35 | # OS or Editor folders 36 | .DS_Store 37 | ._* 38 | .cache 39 | .project 40 | .settings 41 | .tmproj 42 | *.esproj 43 | *.sublime-project 44 | nbproject 45 | thumbs.db 46 | *.*-workspace 47 | 48 | # Folders to ignore 49 | .hg 50 | .svn 51 | .CVS 52 | .idea 53 | node_modules 54 | old/ 55 | *-old/ 56 | *-notrack/ 57 | no-track/ 58 | build/ 59 | combo/ 60 | reference/ 61 | jscoverage_lib/ 62 | temp/ 63 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Kael Zhang , contributors 2 | http://kael.me/ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | | Linux / MacOS / Windows | Coverage | Downloads | 2 | | ----------------------- | -------- | --------- | 3 | | [![build][bb]][bl] | [![coverage][cb]][cl] | [![downloads][db]][dl] | 4 | 5 | [bb]: https://github.com/kaelzhang/node-ignore/actions/workflows/nodejs.yml/badge.svg 6 | [bl]: https://github.com/kaelzhang/node-ignore/actions/workflows/nodejs.yml 7 | 8 | [cb]: https://codecov.io/gh/kaelzhang/node-ignore/branch/master/graph/badge.svg 9 | [cl]: https://codecov.io/gh/kaelzhang/node-ignore 10 | 11 | [db]: http://img.shields.io/npm/dm/ignore.svg 12 | [dl]: https://www.npmjs.org/package/ignore 13 | 14 | # ignore 15 | 16 | `ignore` is a manager, filter and parser which implemented in pure JavaScript according to the [.gitignore spec 2.22.1](http://git-scm.com/docs/gitignore). 17 | 18 | `ignore` is used by eslint, gitbook and [many others](https://www.npmjs.com/browse/depended/ignore). 19 | 20 | Pay **ATTENTION** that [`minimatch`](https://www.npmjs.org/package/minimatch) (which used by `fstream-ignore`) does not follow the gitignore spec. 21 | 22 | To filter filenames according to a .gitignore file, I recommend this npm package, `ignore`. 23 | 24 | To parse an `.npmignore` file, you should use `minimatch`, because an `.npmignore` file is parsed by npm using `minimatch` and it does not work in the .gitignore way. 25 | 26 | ### Tested on 27 | 28 | `ignore` is fully tested, and has more than **five hundreds** of unit tests. 29 | 30 | - Linux + Node: `0.8` - `7.x` 31 | - Windows + Node: `0.10` - `7.x`, node < `0.10` is not tested due to the lack of support of appveyor. 32 | 33 | Actually, `ignore` does not rely on any versions of node specially. 34 | 35 | Since `4.0.0`, ignore will no longer support `node < 6` by default, to use in node < 6, `require('ignore/legacy')`. For details, see [CHANGELOG](https://github.com/kaelzhang/node-ignore/blob/master/CHANGELOG.md). 36 | 37 | ## Table Of Main Contents 38 | 39 | - [Usage](#usage) 40 | - [`Pathname` Conventions](#pathname-conventions) 41 | - See Also: 42 | - [`glob-gitignore`](https://www.npmjs.com/package/glob-gitignore) matches files using patterns and filters them according to gitignore rules. 43 | - [Upgrade Guide](#upgrade-guide) 44 | 45 | ## Install 46 | 47 | ```sh 48 | npm i ignore 49 | ``` 50 | 51 | ## Usage 52 | 53 | ```js 54 | import ignore from 'ignore' 55 | const ig = ignore().add(['.abc/*', '!.abc/d/']) 56 | ``` 57 | 58 | ### Filter the given paths 59 | 60 | ```js 61 | const paths = [ 62 | '.abc/a.js', // filtered out 63 | '.abc/d/e.js' // included 64 | ] 65 | 66 | ig.filter(paths) // ['.abc/d/e.js'] 67 | ig.ignores('.abc/a.js') // true 68 | ``` 69 | 70 | ### As the filter function 71 | 72 | ```js 73 | paths.filter(ig.createFilter()); // ['.abc/d/e.js'] 74 | ``` 75 | 76 | ### Win32 paths will be handled 77 | 78 | ```js 79 | ig.filter(['.abc\\a.js', '.abc\\d\\e.js']) 80 | // if the code above runs on windows, the result will be 81 | // ['.abc\\d\\e.js'] 82 | ``` 83 | 84 | ## Why another ignore? 85 | 86 | - `ignore` is a standalone module, and is much simpler so that it could easy work with other programs, unlike [isaacs](https://npmjs.org/~isaacs)'s [fstream-ignore](https://npmjs.org/package/fstream-ignore) which must work with the modules of the fstream family. 87 | 88 | - `ignore` only contains utility methods to filter paths according to the specified ignore rules, so 89 | - `ignore` never try to find out ignore rules by traversing directories or fetching from git configurations. 90 | - `ignore` don't cares about sub-modules of git projects. 91 | 92 | - Exactly according to [gitignore man page](http://git-scm.com/docs/gitignore), fixes some known matching issues of fstream-ignore, such as: 93 | - '`/*.js`' should only match '`a.js`', but not '`abc/a.js`'. 94 | - '`**/foo`' should match '`foo`' anywhere. 95 | - Prevent re-including a file if a parent directory of that file is excluded. 96 | - Handle trailing whitespaces: 97 | - `'a '`(one space) should not match `'a '`(two spaces). 98 | - `'a \ '` matches `'a '` 99 | - All test cases are verified with the result of `git check-ignore`. 100 | 101 | # Methods 102 | 103 | ## .add(pattern: string | Ignore): this 104 | ## .add(patterns: Array): this 105 | ## .add({pattern: string, mark?: string}): this since 7.0.0 106 | 107 | - **pattern** `string | Ignore` An ignore pattern string, or the `Ignore` instance 108 | - **patterns** `Array` Array of ignore patterns. 109 | - **mark?** `string` Pattern mark, which is used to associate the pattern with a certain marker, such as the line no of the `.gitignore` file. Actually it could be an arbitrary string and is optional. 110 | 111 | Adds a rule or several rules to the current manager. 112 | 113 | Returns `this` 114 | 115 | Notice that a line starting with `'#'`(hash) is treated as a comment. Put a backslash (`'\'`) in front of the first hash for patterns that begin with a hash, if you want to ignore a file with a hash at the beginning of the filename. 116 | 117 | ```js 118 | ignore().add('#abc').ignores('#abc') // false 119 | ignore().add('\\#abc').ignores('#abc') // true 120 | ``` 121 | 122 | `pattern` could either be a line of ignore pattern or a string of multiple ignore patterns, which means we could just `ignore().add()` the content of a ignore file: 123 | 124 | ```js 125 | ignore() 126 | .add(fs.readFileSync(filenameOfGitignore).toString()) 127 | .filter(filenames) 128 | ``` 129 | 130 | `pattern` could also be an `ignore` instance, so that we could easily inherit the rules of another `Ignore` instance. 131 | 132 | ## .ignores(pathname: [Pathname](#pathname-conventions)): boolean 133 | 134 | > new in 3.2.0 135 | 136 | Returns `Boolean` whether `pathname` should be ignored. 137 | 138 | ```js 139 | ig.ignores('.abc/a.js') // true 140 | ``` 141 | 142 | Please **PAY ATTENTION** that `.ignores()` is **NOT** equivalent to `git check-ignore` although in most cases they return equivalent results. 143 | 144 | However, for the purposes of imitating the behavior of `git check-ignore`, please use `.checkIgnore()` instead. 145 | 146 | ### `Pathname` Conventions: 147 | 148 | #### 1. `Pathname` should be a `path.relative()`d pathname 149 | 150 | `Pathname` should be a string that have been `path.join()`ed, or the return value of `path.relative()` to the current directory, 151 | 152 | ```js 153 | // WRONG, an error will be thrown 154 | ig.ignores('./abc') 155 | 156 | // WRONG, for it will never happen, and an error will be thrown 157 | // If the gitignore rule locates at the root directory, 158 | // `'/abc'` should be changed to `'abc'`. 159 | // ``` 160 | // path.relative('/', '/abc') -> 'abc' 161 | // ``` 162 | ig.ignores('/abc') 163 | 164 | // WRONG, that it is an absolute path on Windows, an error will be thrown 165 | ig.ignores('C:\\abc') 166 | 167 | // Right 168 | ig.ignores('abc') 169 | 170 | // Right 171 | ig.ignores(path.join('./abc')) // path.join('./abc') -> 'abc' 172 | ``` 173 | 174 | In other words, each `Pathname` here should be a relative path to the directory of the gitignore rules. 175 | 176 | Suppose the dir structure is: 177 | 178 | ``` 179 | /path/to/your/repo 180 | |-- a 181 | | |-- a.js 182 | | 183 | |-- .b 184 | | 185 | |-- .c 186 | |-- .DS_store 187 | ``` 188 | 189 | Then the `paths` might be like this: 190 | 191 | ```js 192 | [ 193 | 'a/a.js' 194 | '.b', 195 | '.c/.DS_store' 196 | ] 197 | ``` 198 | 199 | #### 2. filenames and dirnames 200 | 201 | `node-ignore` does NO `fs.stat` during path matching, so `node-ignore` treats 202 | - `foo` as a file 203 | - **`foo/` as a directory** 204 | 205 | For the example below: 206 | 207 | ```js 208 | // First, we add a ignore pattern to ignore a directory 209 | ig.add('config/') 210 | 211 | // `ig` does NOT know if 'config', in the real world, 212 | // is a normal file, directory or something. 213 | 214 | ig.ignores('config') 215 | // `ig` treats `config` as a file, so it returns `false` 216 | 217 | ig.ignores('config/') 218 | // returns `true` 219 | ``` 220 | 221 | Specially for people who develop some library based on `node-ignore`, it is important to understand that. 222 | 223 | Usually, you could use [`glob`](http://npmjs.org/package/glob) with `option.mark = true` to fetch the structure of the current directory: 224 | 225 | ```js 226 | import glob from 'glob' 227 | 228 | glob('**', { 229 | // Adds a / character to directory matches. 230 | mark: true 231 | }, (err, files) => { 232 | if (err) { 233 | return console.error(err) 234 | } 235 | 236 | let filtered = ignore().add(patterns).filter(files) 237 | console.log(filtered) 238 | }) 239 | ``` 240 | 241 | 242 | ## .filter(paths: Array<Pathname>): Array<Pathname> 243 | 244 | ```ts 245 | type Pathname = string 246 | ``` 247 | 248 | Filters the given array of pathnames, and returns the filtered array. 249 | 250 | - **paths** `Array.` The array of `pathname`s to be filtered. 251 | 252 | ## .createFilter() 253 | 254 | Creates a filter function which could filter an array of paths with `Array.prototype.filter`. 255 | 256 | Returns `function(path)` the filter function. 257 | 258 | ## .test(pathname: Pathname): TestResult 259 | 260 | > New in 5.0.0 261 | 262 | Returns `TestResult` 263 | 264 | ```ts 265 | // Since 5.0.0 266 | interface TestResult { 267 | ignored: boolean 268 | // true if the `pathname` is finally unignored by some negative pattern 269 | unignored: boolean 270 | // The `IgnoreRule` which ignores the pathname 271 | rule?: IgnoreRule 272 | } 273 | 274 | // Since 7.0.0 275 | interface IgnoreRule { 276 | // The original pattern 277 | pattern: string 278 | // Whether the pattern is a negative pattern 279 | negative: boolean 280 | // Which is used for other packages to build things upon `node-ignore` 281 | mark?: string 282 | } 283 | ``` 284 | 285 | - `{ignored: true, unignored: false}`: the `pathname` is ignored 286 | - `{ignored: false, unignored: true}`: the `pathname` is unignored 287 | - `{ignored: false, unignored: false}`: the `pathname` is never matched by any ignore rules. 288 | 289 | ## .checkIgnore(target: string): TestResult 290 | 291 | > new in 7.0.0 292 | 293 | Debugs gitignore / exclude files, which is equivalent to `git check-ignore -v`. Usually this method is used for other packages to implement the function of `git check-ignore -v` upon `node-ignore` 294 | 295 | - **target** `string` the target to test. 296 | 297 | Returns `TestResult` 298 | 299 | ```js 300 | ig.add({ 301 | pattern: 'foo/*', 302 | mark: '60' 303 | }) 304 | 305 | const { 306 | ignored, 307 | rule 308 | } = checkIgnore('foo/') 309 | 310 | if (ignored) { 311 | console.log(`.gitignore:${result}:${rule.mark}:${rule.pattern} foo/`) 312 | } 313 | 314 | // .gitignore:60:foo/* foo/ 315 | ``` 316 | 317 | Please pay attention that this method does not have a strong built-in cache mechanism. 318 | 319 | The purpose of introducing this method is to make it possible to implement the `git check-ignore` command in JavaScript based on `node-ignore`. 320 | 321 | So do not use this method in those situations where performance is extremely important. 322 | 323 | ## static `isPathValid(pathname): boolean` since 5.0.0 324 | 325 | Check whether the `pathname` is an valid `path.relative()`d path according to the [convention](#1-pathname-should-be-a-pathrelatived-pathname). 326 | 327 | This method is **NOT** used to check if an ignore pattern is valid. 328 | 329 | ```js 330 | import {isPathValid} from 'ignore' 331 | 332 | isPathValid('./foo') // false 333 | ``` 334 | 335 | ## .addIgnoreFile(path) 336 | 337 | REMOVED in `3.x` for now. 338 | 339 | To upgrade `ignore@2.x` up to `3.x`, use 340 | 341 | ```js 342 | import fs from 'fs' 343 | 344 | if (fs.existsSync(filename)) { 345 | ignore().add(fs.readFileSync(filename).toString()) 346 | } 347 | ``` 348 | 349 | instead. 350 | 351 | ## ignore(options) 352 | 353 | ### `options.ignorecase` since 4.0.0 354 | 355 | Similar to the `core.ignorecase` option of [git-config](https://git-scm.com/docs/git-config), `node-ignore` will be case insensitive if `options.ignorecase` is set to `true` (the default value), otherwise case sensitive. 356 | 357 | ```js 358 | const ig = ignore({ 359 | ignorecase: false 360 | }) 361 | 362 | ig.add('*.png') 363 | 364 | ig.ignores('*.PNG') // false 365 | ``` 366 | 367 | ### `options.ignoreCase?: boolean` since 5.2.0 368 | 369 | Which is an alternative to `options.ignoreCase` 370 | 371 | ### `options.allowRelativePaths?: boolean` since 5.2.0 372 | 373 | This option brings backward compatibility with projects which based on `ignore@4.x`. If `options.allowRelativePaths` is `true`, `ignore` will not check whether the given path to be tested is [`path.relative()`d](#pathname-conventions). 374 | 375 | However, passing a relative path, such as `'./foo'` or `'../foo'`, to test if it is ignored or not is not a good practise, which might lead to unexpected behavior 376 | 377 | ```js 378 | ignore({ 379 | allowRelativePaths: true 380 | }).ignores('../foo/bar.js') // And it will not throw 381 | ``` 382 | 383 | **** 384 | 385 | # Upgrade Guide 386 | 387 | ## Upgrade 4.x -> 5.x 388 | 389 | Since `5.0.0`, if an invalid `Pathname` passed into `ig.ignores()`, an error will be thrown, unless `options.allowRelative = true` is passed to the `Ignore` factory. 390 | 391 | While `ignore < 5.0.0` did not make sure what the return value was, as well as 392 | 393 | ```ts 394 | .ignores(pathname: Pathname): boolean 395 | 396 | .filter(pathnames: Array): Array 397 | 398 | .createFilter(): (pathname: Pathname) => boolean 399 | 400 | .test(pathname: Pathname): {ignored: boolean, unignored: boolean} 401 | ``` 402 | 403 | See the convention [here](#1-pathname-should-be-a-pathrelatived-pathname) for details. 404 | 405 | If there are invalid pathnames, the conversion and filtration should be done by users. 406 | 407 | ```js 408 | import {isPathValid} from 'ignore' // introduced in 5.0.0 409 | 410 | const paths = [ 411 | // invalid 412 | ////////////////// 413 | '', 414 | false, 415 | '../foo', 416 | '.', 417 | ////////////////// 418 | 419 | // valid 420 | 'foo' 421 | ] 422 | .filter(isPathValid) 423 | 424 | ig.filter(paths) 425 | ``` 426 | 427 | ## Upgrade 3.x -> 4.x 428 | 429 | Since `4.0.0`, `ignore` will no longer support node < 6, to use `ignore` in node < 6: 430 | 431 | ```js 432 | var ignore = require('ignore/legacy') 433 | ``` 434 | 435 | ## Upgrade 2.x -> 3.x 436 | 437 | - All `options` of 2.x are unnecessary and removed, so just remove them. 438 | - `ignore()` instance is no longer an [`EventEmitter`](nodejs.org/api/events.html), and all events are unnecessary and removed. 439 | - `.addIgnoreFile()` is removed, see the [.addIgnoreFile](#addignorefilepath) section for details. 440 | 441 | **** 442 | 443 | # Collaborators 444 | 445 | - [@whitecolor](https://github.com/whitecolor) *Alex* 446 | - [@SamyPesse](https://github.com/SamyPesse) *Samy Pessé* 447 | - [@azproduction](https://github.com/azproduction) *Mikhail Davydov* 448 | - [@TrySound](https://github.com/TrySound) *Bogdan Chadkin* 449 | - [@JanMattner](https://github.com/JanMattner) *Jan Mattner* 450 | - [@ntwb](https://github.com/ntwb) *Stephen Edgar* 451 | - [@kasperisager](https://github.com/kasperisager) *Kasper Isager* 452 | - [@sandersn](https://github.com/sandersn) *Nathan Shively-Sanders* 453 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | type Pathname = string 2 | 3 | interface IgnoreRule { 4 | pattern: string 5 | mark?: string 6 | negative: boolean 7 | } 8 | 9 | interface TestResult { 10 | ignored: boolean 11 | unignored: boolean 12 | rule?: IgnoreRule 13 | } 14 | 15 | interface PatternParams { 16 | pattern: string 17 | mark?: string 18 | } 19 | 20 | /** 21 | * Creates new ignore manager. 22 | */ 23 | declare function ignore(options?: ignore.Options): ignore.Ignore 24 | declare namespace ignore { 25 | interface Ignore { 26 | /** 27 | * Adds one or several rules to the current manager. 28 | * @param {string[]} patterns 29 | * @returns IgnoreBase 30 | */ 31 | add( 32 | patterns: string | Ignore | readonly (string | Ignore)[] | PatternParams 33 | ): this 34 | 35 | /** 36 | * Filters the given array of pathnames, and returns the filtered array. 37 | * NOTICE that each path here should be a relative path to the root of your repository. 38 | * @param paths the array of paths to be filtered. 39 | * @returns The filtered array of paths 40 | */ 41 | filter(pathnames: readonly Pathname[]): Pathname[] 42 | 43 | /** 44 | * Creates a filter function which could filter 45 | * an array of paths with Array.prototype.filter. 46 | */ 47 | createFilter(): (pathname: Pathname) => boolean 48 | 49 | /** 50 | * Returns Boolean whether pathname should be ignored. 51 | * @param {string} pathname a path to check 52 | * @returns boolean 53 | */ 54 | ignores(pathname: Pathname): boolean 55 | 56 | /** 57 | * Returns whether pathname should be ignored or unignored 58 | * @param {string} pathname a path to check 59 | * @returns TestResult 60 | */ 61 | test(pathname: Pathname): TestResult 62 | 63 | /** 64 | * Debugs ignore rules and returns the checking result, which is 65 | * equivalent to `git check-ignore -v`. 66 | * @returns TestResult 67 | */ 68 | checkIgnore(pathname: Pathname): TestResult 69 | } 70 | 71 | interface Options { 72 | ignorecase?: boolean 73 | // For compatibility 74 | ignoreCase?: boolean 75 | allowRelativePaths?: boolean 76 | } 77 | 78 | function isPathValid(pathname: string): boolean 79 | } 80 | 81 | export = ignore 82 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // A simple implementation of make-array 2 | function makeArray (subject) { 3 | return Array.isArray(subject) 4 | ? subject 5 | : [subject] 6 | } 7 | 8 | const UNDEFINED = undefined 9 | const EMPTY = '' 10 | const SPACE = ' ' 11 | const ESCAPE = '\\' 12 | const REGEX_TEST_BLANK_LINE = /^\s+$/ 13 | const REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/ 14 | const REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/ 15 | const REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/ 16 | const REGEX_SPLITALL_CRLF = /\r?\n/g 17 | 18 | // Invalid: 19 | // - /foo, 20 | // - ./foo, 21 | // - ../foo, 22 | // - . 23 | // - .. 24 | // Valid: 25 | // - .foo 26 | const REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/ 27 | 28 | const REGEX_TEST_TRAILING_SLASH = /\/$/ 29 | 30 | const SLASH = '/' 31 | 32 | // Do not use ternary expression here, since "istanbul ignore next" is buggy 33 | let TMP_KEY_IGNORE = 'node-ignore' 34 | /* istanbul ignore else */ 35 | if (typeof Symbol !== 'undefined') { 36 | TMP_KEY_IGNORE = Symbol.for('node-ignore') 37 | } 38 | const KEY_IGNORE = TMP_KEY_IGNORE 39 | 40 | const define = (object, key, value) => { 41 | Object.defineProperty(object, key, {value}) 42 | return value 43 | } 44 | 45 | const REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g 46 | 47 | const RETURN_FALSE = () => false 48 | 49 | // Sanitize the range of a regular expression 50 | // The cases are complicated, see test cases for details 51 | const sanitizeRange = range => range.replace( 52 | REGEX_REGEXP_RANGE, 53 | (match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0) 54 | ? match 55 | // Invalid range (out of order) which is ok for gitignore rules but 56 | // fatal for JavaScript regular expression, so eliminate it. 57 | : EMPTY 58 | ) 59 | 60 | // See fixtures #59 61 | const cleanRangeBackSlash = slashes => { 62 | const {length} = slashes 63 | return slashes.slice(0, length - length % 2) 64 | } 65 | 66 | // > If the pattern ends with a slash, 67 | // > it is removed for the purpose of the following description, 68 | // > but it would only find a match with a directory. 69 | // > In other words, foo/ will match a directory foo and paths underneath it, 70 | // > but will not match a regular file or a symbolic link foo 71 | // > (this is consistent with the way how pathspec works in general in Git). 72 | // '`foo/`' will not match regular file '`foo`' or symbolic link '`foo`' 73 | // -> ignore-rules will not deal with it, because it costs extra `fs.stat` call 74 | // you could use option `mark: true` with `glob` 75 | 76 | // '`foo/`' should not continue with the '`..`' 77 | const REPLACERS = [ 78 | 79 | [ 80 | // Remove BOM 81 | // TODO: 82 | // Other similar zero-width characters? 83 | /^\uFEFF/, 84 | () => EMPTY 85 | ], 86 | 87 | // > Trailing spaces are ignored unless they are quoted with backslash ("\") 88 | [ 89 | // (a\ ) -> (a ) 90 | // (a ) -> (a) 91 | // (a ) -> (a) 92 | // (a \ ) -> (a ) 93 | /((?:\\\\)*?)(\\?\s+)$/, 94 | (_, m1, m2) => m1 + ( 95 | m2.indexOf('\\') === 0 96 | ? SPACE 97 | : EMPTY 98 | ) 99 | ], 100 | 101 | // Replace (\ ) with ' ' 102 | // (\ ) -> ' ' 103 | // (\\ ) -> '\\ ' 104 | // (\\\ ) -> '\\ ' 105 | [ 106 | /(\\+?)\s/g, 107 | (_, m1) => { 108 | const {length} = m1 109 | return m1.slice(0, length - length % 2) + SPACE 110 | } 111 | ], 112 | 113 | // Escape metacharacters 114 | // which is written down by users but means special for regular expressions. 115 | 116 | // > There are 12 characters with special meanings: 117 | // > - the backslash \, 118 | // > - the caret ^, 119 | // > - the dollar sign $, 120 | // > - the period or dot ., 121 | // > - the vertical bar or pipe symbol |, 122 | // > - the question mark ?, 123 | // > - the asterisk or star *, 124 | // > - the plus sign +, 125 | // > - the opening parenthesis (, 126 | // > - the closing parenthesis ), 127 | // > - and the opening square bracket [, 128 | // > - the opening curly brace {, 129 | // > These special characters are often called "metacharacters". 130 | [ 131 | /[\\$.|*+(){^]/g, 132 | match => `\\${match}` 133 | ], 134 | 135 | [ 136 | // > a question mark (?) matches a single character 137 | /(?!\\)\?/g, 138 | () => '[^/]' 139 | ], 140 | 141 | // leading slash 142 | [ 143 | 144 | // > A leading slash matches the beginning of the pathname. 145 | // > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c". 146 | // A leading slash matches the beginning of the pathname 147 | /^\//, 148 | () => '^' 149 | ], 150 | 151 | // replace special metacharacter slash after the leading slash 152 | [ 153 | /\//g, 154 | () => '\\/' 155 | ], 156 | 157 | [ 158 | // > A leading "**" followed by a slash means match in all directories. 159 | // > For example, "**/foo" matches file or directory "foo" anywhere, 160 | // > the same as pattern "foo". 161 | // > "**/foo/bar" matches file or directory "bar" anywhere that is directly 162 | // > under directory "foo". 163 | // Notice that the '*'s have been replaced as '\\*' 164 | /^\^*\\\*\\\*\\\//, 165 | 166 | // '**/foo' <-> 'foo' 167 | () => '^(?:.*\\/)?' 168 | ], 169 | 170 | // starting 171 | [ 172 | // there will be no leading '/' 173 | // (which has been replaced by section "leading slash") 174 | // If starts with '**', adding a '^' to the regular expression also works 175 | /^(?=[^^])/, 176 | function startingReplacer () { 177 | // If has a slash `/` at the beginning or middle 178 | return !/\/(?!$)/.test(this) 179 | // > Prior to 2.22.1 180 | // > If the pattern does not contain a slash /, 181 | // > Git treats it as a shell glob pattern 182 | // Actually, if there is only a trailing slash, 183 | // git also treats it as a shell glob pattern 184 | 185 | // After 2.22.1 (compatible but clearer) 186 | // > If there is a separator at the beginning or middle (or both) 187 | // > of the pattern, then the pattern is relative to the directory 188 | // > level of the particular .gitignore file itself. 189 | // > Otherwise the pattern may also match at any level below 190 | // > the .gitignore level. 191 | ? '(?:^|\\/)' 192 | 193 | // > Otherwise, Git treats the pattern as a shell glob suitable for 194 | // > consumption by fnmatch(3) 195 | : '^' 196 | } 197 | ], 198 | 199 | // two globstars 200 | [ 201 | // Use lookahead assertions so that we could match more than one `'/**'` 202 | /\\\/\\\*\\\*(?=\\\/|$)/g, 203 | 204 | // Zero, one or several directories 205 | // should not use '*', or it will be replaced by the next replacer 206 | 207 | // Check if it is not the last `'/**'` 208 | (_, index, str) => index + 6 < str.length 209 | 210 | // case: /**/ 211 | // > A slash followed by two consecutive asterisks then a slash matches 212 | // > zero or more directories. 213 | // > For example, "a/**/b" matches "a/b", "a/x/b", "a/x/y/b" and so on. 214 | // '/**/' 215 | ? '(?:\\/[^\\/]+)*' 216 | 217 | // case: /** 218 | // > A trailing `"/**"` matches everything inside. 219 | 220 | // #21: everything inside but it should not include the current folder 221 | : '\\/.+' 222 | ], 223 | 224 | // normal intermediate wildcards 225 | [ 226 | // Never replace escaped '*' 227 | // ignore rule '\*' will match the path '*' 228 | 229 | // 'abc.*/' -> go 230 | // 'abc.*' -> skip this rule, 231 | // coz trailing single wildcard will be handed by [trailing wildcard] 232 | /(^|[^\\]+)(\\\*)+(?=.+)/g, 233 | 234 | // '*.js' matches '.js' 235 | // '*.js' doesn't match 'abc' 236 | (_, p1, p2) => { 237 | // 1. 238 | // > An asterisk "*" matches anything except a slash. 239 | // 2. 240 | // > Other consecutive asterisks are considered regular asterisks 241 | // > and will match according to the previous rules. 242 | const unescaped = p2.replace(/\\\*/g, '[^\\/]*') 243 | return p1 + unescaped 244 | } 245 | ], 246 | 247 | [ 248 | // unescape, revert step 3 except for back slash 249 | // For example, if a user escape a '\\*', 250 | // after step 3, the result will be '\\\\\\*' 251 | /\\\\\\(?=[$.|*+(){^])/g, 252 | () => ESCAPE 253 | ], 254 | 255 | [ 256 | // '\\\\' -> '\\' 257 | /\\\\/g, 258 | () => ESCAPE 259 | ], 260 | 261 | [ 262 | // > The range notation, e.g. [a-zA-Z], 263 | // > can be used to match one of the characters in a range. 264 | 265 | // `\` is escaped by step 3 266 | /(\\)?\[([^\]/]*?)(\\*)($|\])/g, 267 | (match, leadEscape, range, endEscape, close) => leadEscape === ESCAPE 268 | // '\\[bar]' -> '\\\\[bar\\]' 269 | ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}` 270 | : close === ']' 271 | ? endEscape.length % 2 === 0 272 | // A normal case, and it is a range notation 273 | // '[bar]' 274 | // '[bar\\\\]' 275 | ? `[${sanitizeRange(range)}${endEscape}]` 276 | // Invalid range notaton 277 | // '[bar\\]' -> '[bar\\\\]' 278 | : '[]' 279 | : '[]' 280 | ], 281 | 282 | // ending 283 | [ 284 | // 'js' will not match 'js.' 285 | // 'ab' will not match 'abc' 286 | /(?:[^*])$/, 287 | 288 | // WTF! 289 | // https://git-scm.com/docs/gitignore 290 | // changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1) 291 | // which re-fixes #24, #38 292 | 293 | // > If there is a separator at the end of the pattern then the pattern 294 | // > will only match directories, otherwise the pattern can match both 295 | // > files and directories. 296 | 297 | // 'js*' will not match 'a.js' 298 | // 'js/' will not match 'a.js' 299 | // 'js' will match 'a.js' and 'a.js/' 300 | match => /\/$/.test(match) 301 | // foo/ will not match 'foo' 302 | ? `${match}$` 303 | // foo matches 'foo' and 'foo/' 304 | : `${match}(?=$|\\/$)` 305 | ] 306 | ] 307 | 308 | const REGEX_REPLACE_TRAILING_WILDCARD = /(^|\\\/)?\\\*$/ 309 | const MODE_IGNORE = 'regex' 310 | const MODE_CHECK_IGNORE = 'checkRegex' 311 | const UNDERSCORE = '_' 312 | 313 | const TRAILING_WILD_CARD_REPLACERS = { 314 | [MODE_IGNORE] (_, p1) { 315 | const prefix = p1 316 | // '\^': 317 | // '/*' does not match EMPTY 318 | // '/*' does not match everything 319 | 320 | // '\\\/': 321 | // 'abc/*' does not match 'abc/' 322 | ? `${p1}[^/]+` 323 | 324 | // 'a*' matches 'a' 325 | // 'a*' matches 'aa' 326 | : '[^/]*' 327 | 328 | return `${prefix}(?=$|\\/$)` 329 | }, 330 | 331 | [MODE_CHECK_IGNORE] (_, p1) { 332 | // When doing `git check-ignore` 333 | const prefix = p1 334 | // '\\\/': 335 | // 'abc/*' DOES match 'abc/' ! 336 | ? `${p1}[^/]*` 337 | 338 | // 'a*' matches 'a' 339 | // 'a*' matches 'aa' 340 | : '[^/]*' 341 | 342 | return `${prefix}(?=$|\\/$)` 343 | } 344 | } 345 | 346 | // @param {pattern} 347 | const makeRegexPrefix = pattern => REPLACERS.reduce( 348 | (prev, [matcher, replacer]) => 349 | prev.replace(matcher, replacer.bind(pattern)), 350 | pattern 351 | ) 352 | 353 | const isString = subject => typeof subject === 'string' 354 | 355 | // > A blank line matches no files, so it can serve as a separator for readability. 356 | const checkPattern = pattern => pattern 357 | && isString(pattern) 358 | && !REGEX_TEST_BLANK_LINE.test(pattern) 359 | && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern) 360 | 361 | // > A line starting with # serves as a comment. 362 | && pattern.indexOf('#') !== 0 363 | 364 | const splitPattern = pattern => pattern 365 | .split(REGEX_SPLITALL_CRLF) 366 | .filter(Boolean) 367 | 368 | class IgnoreRule { 369 | constructor ( 370 | pattern, 371 | mark, 372 | body, 373 | ignoreCase, 374 | negative, 375 | prefix 376 | ) { 377 | this.pattern = pattern 378 | this.mark = mark 379 | this.negative = negative 380 | 381 | define(this, 'body', body) 382 | define(this, 'ignoreCase', ignoreCase) 383 | define(this, 'regexPrefix', prefix) 384 | } 385 | 386 | get regex () { 387 | const key = UNDERSCORE + MODE_IGNORE 388 | 389 | if (this[key]) { 390 | return this[key] 391 | } 392 | 393 | return this._make(MODE_IGNORE, key) 394 | } 395 | 396 | get checkRegex () { 397 | const key = UNDERSCORE + MODE_CHECK_IGNORE 398 | 399 | if (this[key]) { 400 | return this[key] 401 | } 402 | 403 | return this._make(MODE_CHECK_IGNORE, key) 404 | } 405 | 406 | _make (mode, key) { 407 | const str = this.regexPrefix.replace( 408 | REGEX_REPLACE_TRAILING_WILDCARD, 409 | 410 | // It does not need to bind pattern 411 | TRAILING_WILD_CARD_REPLACERS[mode] 412 | ) 413 | 414 | const regex = this.ignoreCase 415 | ? new RegExp(str, 'i') 416 | : new RegExp(str) 417 | 418 | return define(this, key, regex) 419 | } 420 | } 421 | 422 | const createRule = ({ 423 | pattern, 424 | mark 425 | }, ignoreCase) => { 426 | let negative = false 427 | let body = pattern 428 | 429 | // > An optional prefix "!" which negates the pattern; 430 | if (body.indexOf('!') === 0) { 431 | negative = true 432 | body = body.substr(1) 433 | } 434 | 435 | body = body 436 | // > Put a backslash ("\") in front of the first "!" for patterns that 437 | // > begin with a literal "!", for example, `"\!important!.txt"`. 438 | .replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, '!') 439 | // > Put a backslash ("\") in front of the first hash for patterns that 440 | // > begin with a hash. 441 | .replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, '#') 442 | 443 | const regexPrefix = makeRegexPrefix(body) 444 | 445 | return new IgnoreRule( 446 | pattern, 447 | mark, 448 | body, 449 | ignoreCase, 450 | negative, 451 | regexPrefix 452 | ) 453 | } 454 | 455 | class RuleManager { 456 | constructor (ignoreCase) { 457 | this._ignoreCase = ignoreCase 458 | this._rules = [] 459 | } 460 | 461 | _add (pattern) { 462 | // #32 463 | if (pattern && pattern[KEY_IGNORE]) { 464 | this._rules = this._rules.concat(pattern._rules._rules) 465 | this._added = true 466 | return 467 | } 468 | 469 | if (isString(pattern)) { 470 | pattern = { 471 | pattern 472 | } 473 | } 474 | 475 | if (checkPattern(pattern.pattern)) { 476 | const rule = createRule(pattern, this._ignoreCase) 477 | this._added = true 478 | this._rules.push(rule) 479 | } 480 | } 481 | 482 | // @param {Array | string | Ignore} pattern 483 | add (pattern) { 484 | this._added = false 485 | 486 | makeArray( 487 | isString(pattern) 488 | ? splitPattern(pattern) 489 | : pattern 490 | ).forEach(this._add, this) 491 | 492 | return this._added 493 | } 494 | 495 | // Test one single path without recursively checking parent directories 496 | // 497 | // - checkUnignored `boolean` whether should check if the path is unignored, 498 | // setting `checkUnignored` to `false` could reduce additional 499 | // path matching. 500 | // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE` 501 | 502 | // @returns {TestResult} true if a file is ignored 503 | test (path, checkUnignored, mode) { 504 | let ignored = false 505 | let unignored = false 506 | let matchedRule 507 | 508 | this._rules.forEach(rule => { 509 | const {negative} = rule 510 | 511 | // | ignored : unignored 512 | // -------- | --------------------------------------- 513 | // negative | 0:0 | 0:1 | 1:0 | 1:1 514 | // -------- | ------- | ------- | ------- | -------- 515 | // 0 | TEST | TEST | SKIP | X 516 | // 1 | TESTIF | SKIP | TEST | X 517 | 518 | // - SKIP: always skip 519 | // - TEST: always test 520 | // - TESTIF: only test if checkUnignored 521 | // - X: that never happen 522 | if ( 523 | unignored === negative && ignored !== unignored 524 | || negative && !ignored && !unignored && !checkUnignored 525 | ) { 526 | return 527 | } 528 | 529 | const matched = rule[mode].test(path) 530 | 531 | if (!matched) { 532 | return 533 | } 534 | 535 | ignored = !negative 536 | unignored = negative 537 | 538 | matchedRule = negative 539 | ? UNDEFINED 540 | : rule 541 | }) 542 | 543 | const ret = { 544 | ignored, 545 | unignored 546 | } 547 | 548 | if (matchedRule) { 549 | ret.rule = matchedRule 550 | } 551 | 552 | return ret 553 | } 554 | } 555 | 556 | const throwError = (message, Ctor) => { 557 | throw new Ctor(message) 558 | } 559 | 560 | const checkPath = (path, originalPath, doThrow) => { 561 | if (!isString(path)) { 562 | return doThrow( 563 | `path must be a string, but got \`${originalPath}\``, 564 | TypeError 565 | ) 566 | } 567 | 568 | // We don't know if we should ignore EMPTY, so throw 569 | if (!path) { 570 | return doThrow(`path must not be empty`, TypeError) 571 | } 572 | 573 | // Check if it is a relative path 574 | if (checkPath.isNotRelative(path)) { 575 | const r = '`path.relative()`d' 576 | return doThrow( 577 | `path should be a ${r} string, but got "${originalPath}"`, 578 | RangeError 579 | ) 580 | } 581 | 582 | return true 583 | } 584 | 585 | const isNotRelative = path => REGEX_TEST_INVALID_PATH.test(path) 586 | 587 | checkPath.isNotRelative = isNotRelative 588 | 589 | // On windows, the following function will be replaced 590 | /* istanbul ignore next */ 591 | checkPath.convert = p => p 592 | 593 | 594 | class Ignore { 595 | constructor ({ 596 | ignorecase = true, 597 | ignoreCase = ignorecase, 598 | allowRelativePaths = false 599 | } = {}) { 600 | define(this, KEY_IGNORE, true) 601 | 602 | this._rules = new RuleManager(ignoreCase) 603 | this._strictPathCheck = !allowRelativePaths 604 | this._initCache() 605 | } 606 | 607 | _initCache () { 608 | // A cache for the result of `.ignores()` 609 | this._ignoreCache = Object.create(null) 610 | 611 | // A cache for the result of `.test()` 612 | this._testCache = Object.create(null) 613 | } 614 | 615 | add (pattern) { 616 | if (this._rules.add(pattern)) { 617 | // Some rules have just added to the ignore, 618 | // making the behavior changed, 619 | // so we need to re-initialize the result cache 620 | this._initCache() 621 | } 622 | 623 | return this 624 | } 625 | 626 | // legacy 627 | addPattern (pattern) { 628 | return this.add(pattern) 629 | } 630 | 631 | // @returns {TestResult} 632 | _test (originalPath, cache, checkUnignored, slices) { 633 | const path = originalPath 634 | // Supports nullable path 635 | && checkPath.convert(originalPath) 636 | 637 | checkPath( 638 | path, 639 | originalPath, 640 | this._strictPathCheck 641 | ? throwError 642 | : RETURN_FALSE 643 | ) 644 | 645 | return this._t(path, cache, checkUnignored, slices) 646 | } 647 | 648 | checkIgnore (path) { 649 | // If the path doest not end with a slash, `.ignores()` is much equivalent 650 | // to `git check-ignore` 651 | if (!REGEX_TEST_TRAILING_SLASH.test(path)) { 652 | return this.test(path) 653 | } 654 | 655 | const slices = path.split(SLASH).filter(Boolean) 656 | slices.pop() 657 | 658 | if (slices.length) { 659 | const parent = this._t( 660 | slices.join(SLASH) + SLASH, 661 | this._testCache, 662 | true, 663 | slices 664 | ) 665 | 666 | if (parent.ignored) { 667 | return parent 668 | } 669 | } 670 | 671 | return this._rules.test(path, false, MODE_CHECK_IGNORE) 672 | } 673 | 674 | _t ( 675 | // The path to be tested 676 | path, 677 | 678 | // The cache for the result of a certain checking 679 | cache, 680 | 681 | // Whether should check if the path is unignored 682 | checkUnignored, 683 | 684 | // The path slices 685 | slices 686 | ) { 687 | if (path in cache) { 688 | return cache[path] 689 | } 690 | 691 | if (!slices) { 692 | // path/to/a.js 693 | // ['path', 'to', 'a.js'] 694 | slices = path.split(SLASH).filter(Boolean) 695 | } 696 | 697 | slices.pop() 698 | 699 | // If the path has no parent directory, just test it 700 | if (!slices.length) { 701 | return cache[path] = this._rules.test(path, checkUnignored, MODE_IGNORE) 702 | } 703 | 704 | const parent = this._t( 705 | slices.join(SLASH) + SLASH, 706 | cache, 707 | checkUnignored, 708 | slices 709 | ) 710 | 711 | // If the path contains a parent directory, check the parent first 712 | return cache[path] = parent.ignored 713 | // > It is not possible to re-include a file if a parent directory of 714 | // > that file is excluded. 715 | ? parent 716 | : this._rules.test(path, checkUnignored, MODE_IGNORE) 717 | } 718 | 719 | ignores (path) { 720 | return this._test(path, this._ignoreCache, false).ignored 721 | } 722 | 723 | createFilter () { 724 | return path => !this.ignores(path) 725 | } 726 | 727 | filter (paths) { 728 | return makeArray(paths).filter(this.createFilter()) 729 | } 730 | 731 | // @returns {TestResult} 732 | test (path) { 733 | return this._test(path, this._testCache, true) 734 | } 735 | } 736 | 737 | const factory = options => new Ignore(options) 738 | 739 | const isPathValid = path => 740 | checkPath(path && checkPath.convert(path), path, RETURN_FALSE) 741 | 742 | /* istanbul ignore next */ 743 | const setupWindows = () => { 744 | /* eslint no-control-regex: "off" */ 745 | const makePosix = str => /^\\\\\?\\/.test(str) 746 | || /["<>|\u0000-\u001F]+/u.test(str) 747 | ? str 748 | : str.replace(/\\/g, '/') 749 | 750 | checkPath.convert = makePosix 751 | 752 | // 'C:\\foo' <- 'C:\\foo' has been converted to 'C:/' 753 | // 'd:\\foo' 754 | const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i 755 | checkPath.isNotRelative = path => 756 | REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path) 757 | || isNotRelative(path) 758 | } 759 | 760 | 761 | // Windows 762 | // -------------------------------------------------------------- 763 | /* istanbul ignore next */ 764 | if ( 765 | // Detect `process` so that it can run in browsers. 766 | typeof process !== 'undefined' 767 | && process.platform === 'win32' 768 | ) { 769 | setupWindows() 770 | } 771 | 772 | // COMMONJS_EXPORTS //////////////////////////////////////////////////////////// 773 | 774 | module.exports = factory 775 | 776 | // Although it is an anti-pattern, 777 | // it is still widely misused by a lot of libraries in github 778 | // Ref: https://github.com/search?q=ignore.default%28%29&type=code 779 | factory.default = factory 780 | 781 | module.exports.isPathValid = isPathValid 782 | 783 | // For testing purposes 784 | define(module.exports, Symbol.for('setupWindows'), setupWindows) 785 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ignore", 3 | "version": "7.0.5", 4 | "description": "Ignore is a manager and filter for .gitignore rules, the one used by eslint, gitbook and many others.", 5 | "types": "index.d.ts", 6 | "files": [ 7 | "legacy.js", 8 | "index.js", 9 | "index.d.ts", 10 | "LICENSE-MIT" 11 | ], 12 | "scripts": { 13 | "prepublishOnly": "npm run build", 14 | "build": "babel -o legacy.js index.js", 15 | 16 | "==================== linting ======================": "", 17 | "lint": "eslint .", 18 | 19 | "===================== import ======================": "", 20 | "ts": "npm run test:ts && npm run test:16", 21 | "test:ts": "ts-node ./test/import/simple.ts", 22 | "test:16": "npm run test:ts:16 && npm run test:cjs:16 && npm run test:mjs:16", 23 | "test:ts:16": "ts-node --compilerOptions '{\"moduleResolution\": \"Node16\", \"module\": \"Node16\"}' ./test/import/simple.ts && tsc ./test/import/simple.ts --lib ES6 --moduleResolution Node16 --module Node16 && node ./test/import/simple.js", 24 | "test:cjs:16": "ts-node --compilerOptions '{\"moduleResolution\": \"Node16\", \"module\": \"Node16\"}' ./test/import/simple.cjs", 25 | "test:mjs:16": "ts-node --compilerOptions '{\"moduleResolution\": \"Node16\", \"module\": \"Node16\"}' ./test/import/simple.mjs && babel -o ./test/import/simple-mjs.js ./test/import/simple.mjs && node ./test/import/simple-mjs.js", 26 | 27 | "===================== cases =======================": "", 28 | "test:cases": "npm run tap test/*.test.js -- --coverage", 29 | "tap": "tap --reporter classic", 30 | 31 | "===================== debug =======================": "", 32 | "test:git": "npm run tap test/git-check-ignore.test.js", 33 | "test:ignore": "npm run tap test/ignore.test.js", 34 | "test:ignore:only": "IGNORE_ONLY_IGNORES=1 npm run tap test/ignore.test.js", 35 | "test:others": "npm run tap test/others.test.js", 36 | "test:no-coverage": "npm run tap test/*.test.js -- --no-check-coverage", 37 | 38 | "test": "npm run lint && npm run ts && npm run build && npm run test:cases", 39 | "test:win32": "IGNORE_TEST_WIN32=1 npm run test", 40 | "report": "tap --coverage-report=html" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "git@github.com:kaelzhang/node-ignore.git" 45 | }, 46 | "keywords": [ 47 | "ignore", 48 | ".gitignore", 49 | "gitignore", 50 | "npmignore", 51 | "rules", 52 | "manager", 53 | "filter", 54 | "regexp", 55 | "regex", 56 | "fnmatch", 57 | "glob", 58 | "asterisks", 59 | "regular-expression" 60 | ], 61 | "author": "kael", 62 | "license": "MIT", 63 | "bugs": { 64 | "url": "https://github.com/kaelzhang/node-ignore/issues" 65 | }, 66 | "devDependencies": { 67 | "@babel/cli": "^7.22.9", 68 | "@babel/core": "^7.22.9", 69 | "@babel/preset-env": "^7.22.9", 70 | "@typescript-eslint/eslint-plugin": "^8.19.1", 71 | "debug": "^4.3.4", 72 | "eslint": "^8.46.0", 73 | "eslint-config-ostai": "^3.0.0", 74 | "eslint-plugin-import": "^2.28.0", 75 | "mkdirp": "^3.0.1", 76 | "pre-suf": "^1.1.1", 77 | "rimraf": "^6.0.1", 78 | "spawn-sync": "^2.0.0", 79 | "tap": "^16.3.9", 80 | "tmp": "0.2.3", 81 | "ts-node": "^10.9.2", 82 | "typescript": "^5.6.2" 83 | }, 84 | "engines": { 85 | "node": ">= 4" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/fixtures/.aignore: -------------------------------------------------------------------------------- 1 | abc 2 | !abc/b 3 | #e 4 | \#f 5 | -------------------------------------------------------------------------------- /test/fixtures/.fakeignore: -------------------------------------------------------------------------------- 1 | # Actually, there's nothing -------------------------------------------------------------------------------- /test/fixtures/.gitignore-with-BOM: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /test/fixtures/.ignore-issue-2: -------------------------------------------------------------------------------- 1 | # git-ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | 8 | /.project 9 | 10 | # The same type as `'/.project'` 11 | # /.settings 12 | 13 | /sharedTools/external/* 14 | 15 | thumbs.db 16 | 17 | /packs 18 | 19 | *.pyc 20 | 21 | # /.cache 22 | 23 | # /bigtxt 24 | .metadata/* 25 | 26 | *~ 27 | 28 | /sharedTools/jsApiLua.lua 29 | 30 | ._* 31 | 32 | .DS_Store 33 | 34 | # /DISABLED 35 | 36 | # /.pydevproject 37 | 38 | # /testbox 39 | 40 | *.swp 41 | 42 | /packs/packagesTree 43 | 44 | /packs/*.ini 45 | 46 | # .buildpath 47 | 48 | # The same type as `'/sharedTools/external/*'` 49 | # /resources/hooks/* 50 | 51 | # .idea 52 | 53 | .idea/* 54 | 55 | # /tags 56 | 57 | **.iml 58 | 59 | .sonar/* 60 | 61 | .*.sw? 62 | -------------------------------------------------------------------------------- /test/fixtures/cases.js: -------------------------------------------------------------------------------- 1 | /* eslint quote-props: ["off"] */ 2 | const fs = require('fs') 3 | const path = require('path') 4 | const debug = require('debug')('node-ignore') 5 | const ignore = require('../..') 6 | 7 | function readPatterns (file) { 8 | file = path.join(__dirname, file) 9 | return fs.readFileSync(file).toString() 10 | } 11 | 12 | const IS_WINDOWS = process.platform === 'win32' 13 | const SHOULD_TEST_WINDOWS = IS_WINDOWS || !!process.env.IGNORE_TEST_WIN32 14 | 15 | // Not Windows, but want to mimic tests on Windows 16 | if (!IS_WINDOWS && !!process.env.IGNORE_TEST_WIN32) { 17 | ignore[Symbol.for('setupWindows')]() 18 | } 19 | 20 | const cases = [ 21 | ///////////////////////////////////////////////////////////////////// 22 | // [ 23 | // 'Example', 24 | // [ 25 | // // ignore pattern 26 | // 'a' 27 | // ], 28 | // { 29 | // // 1 indicates 'a' should be ignored 30 | // 'a': 1 31 | // } 32 | // ], 33 | ///////////////////////////////////////////////////////////////////// 34 | [ 35 | '#148', 36 | [ 37 | '/.a/' 38 | ], 39 | { 40 | '.a': 0 41 | } 42 | ], [ 43 | '#77: more cases for coverage', 44 | [ 45 | '/*' 46 | ], 47 | { 48 | 'a': 1, 49 | 'a/': 1, 50 | 'a/b/': 1 51 | } 52 | ], 53 | [ 54 | '#77: directory ending with / not always correctly ignored', 55 | [ 56 | 'c/*', 57 | {pattern: 'foo/bar/*'} 58 | ], 59 | { 60 | 'c/': 1, 61 | 'c': 0, 62 | 'foo/bar/': 1, 63 | 'foo/bar': 0 64 | }, 65 | false, 66 | false, 67 | // Only for checkIgnore 68 | ['checkIgnore'] 69 | ], [ 70 | '#108: gitignore rules with BOM', 71 | [ 72 | readPatterns('.gitignore-with-BOM'), 73 | ], 74 | { 75 | 'node_modules': 1 76 | } 77 | ], 78 | [ 79 | 'charactor ?', 80 | [ 81 | 'foo?bar' 82 | ], 83 | { 84 | 'foo/bar': 0, 85 | 'fooxbar': 1, 86 | 'fooxxbar': 0 87 | } 88 | ], 89 | [ 90 | '#57, normal * and normal consecutive *', 91 | [ 92 | '**foo', 93 | '*bar', 94 | 'ba*z', 95 | 'folder/other-folder/**/**js' 96 | ], 97 | { 98 | 'foo': 1, 99 | 'a/foo': 1, 100 | 'afoo': 1, 101 | 'abfoo': 1, 102 | 'abcfoo': 1, 103 | 'bar': 1, 104 | 'abar': 1, 105 | 'baz': 1, 106 | 'ba/z': 0, 107 | 'baaaaaaz': 1, 108 | 'folder/other-folder/dir/main.js': 1 109 | } 110 | ], 111 | [ 112 | '#76 (invalid), comments with no heading whitespace', 113 | [ 114 | 'node_modules# comments' 115 | ], 116 | { 117 | 'node_modules/a.js': 0 118 | } 119 | ], 120 | [ 121 | '#59 and more cases about range notation', 122 | [ 123 | 'src/\\[foo\\]', // 1 -> 0 124 | 125 | 'src/\\[bar]', 126 | 127 | 'src/[e\\\\]', 128 | 's/[f\\\\\\\\]', 129 | 130 | 's/[a-z0-9]', 131 | 132 | // The following special cases are not described in gitignore manual 133 | 'src/[q', 134 | 'src/\\[u', 135 | 'src/[x\\]' 136 | ], 137 | { 138 | 'src/[foo]': 1, 139 | 140 | 'src/[bar]': 1, 141 | 142 | 'src/e': 1, 143 | 's/f': 1, 144 | 145 | 's/a': 1, 146 | 147 | 's/0': 1, 148 | 149 | 'src/[q': 0, 150 | 'src/[u': 1, 151 | 'src/[x': 0, 152 | 'src/[x]': 0, 153 | 'src/x': 0 154 | } 155 | ], 156 | [ 157 | 'gitignore 2.22.1 example', 158 | [ 159 | 'doc/frotz/' 160 | ], 161 | { 162 | 'doc/frotz/': 1, 163 | 'a/doc/frotz/': 0 164 | } 165 | ], 166 | [ 167 | '#56', 168 | [ 169 | '/*/', 170 | '!/foo/' 171 | ], 172 | { 173 | 'foo/bar.js': 0 174 | } 175 | ], 176 | [ 177 | 'object prototype', 178 | [ 179 | '*', 180 | '!hasOwnProperty', 181 | '!a' 182 | ], 183 | { 184 | 'hasOwnProperty': 0, 185 | 'a/hasOwnProperty': 0, 186 | 'toString': 1, 187 | 'a/toString': 1 188 | } 189 | ], 190 | [ 191 | 'a and a/', 192 | [ 193 | 'a', 194 | 'a2', 195 | 'b/', 196 | 'b2/' 197 | ], 198 | { 199 | 'a': 1, 200 | 'a2/': 1, 201 | 'b': 0, 202 | 'b2/': 1 203 | } 204 | ], 205 | [ 206 | 'ending question mark', 207 | [ 208 | '*.web?' 209 | ], 210 | { 211 | 'a.webp': 1, 212 | 'a.webm': 1, 213 | // only match one characters 214 | 'a.webam': 0, 215 | 'a.png': 0 216 | } 217 | ], 218 | [ 219 | 'intermediate question mark', 220 | [ 221 | 'a?c' 222 | ], 223 | { 224 | 'abc': 1, 225 | 'acc': 1, 226 | 'ac': 0, 227 | 'abbc': 0 228 | } 229 | ], 230 | [ 231 | 'multiple question marks', 232 | [ 233 | 'a?b??' 234 | ], 235 | { 236 | 'acbdd': 1, 237 | 'acbddd': 0 238 | } 239 | ], 240 | [ 241 | 'normal *.[oa]', 242 | [ 243 | '*.[oa]' 244 | ], 245 | { 246 | 'a.js': 0, 247 | 'a.a': 1, 248 | // test ending 249 | 'a.aa': 0, 250 | 'a.o': 1, 251 | 'a.0': 0 252 | } 253 | ], 254 | [ 255 | 'multiple brackets', 256 | [ 257 | '*.[ab][cd][ef]' 258 | ], 259 | { 260 | 'a.ace': 1, 261 | 'a.bdf': 1, 262 | 'a.bce': 1, 263 | 'a.abc': 0, 264 | 'a.aceg': 0 265 | } 266 | ], 267 | [ 268 | 'special case: []', 269 | [ 270 | '*.[]' 271 | ], 272 | { 273 | 'a.[]': 0, 274 | 'a.[]a': 0 275 | } 276 | ], 277 | [ 278 | 'mixed with numbers, characters and symbols: *.[0a_]', 279 | [ 280 | '*.[0a_]' 281 | ], 282 | { 283 | 'a.0': 1, 284 | 'a.1': 0, 285 | 'a.a': 1, 286 | 'a.b': 0, 287 | 'a._': 1, 288 | 'a.=': 0 289 | } 290 | ], 291 | [ 292 | 'range: [a-z]', 293 | [ 294 | '*.pn[a-z]' 295 | ], 296 | { 297 | 'a.pn1': 0, 298 | 'a.pn2': 0, 299 | 'a.png': 1, 300 | 'a.pna': 1 301 | } 302 | ], 303 | [ 304 | 'range: [0-9]', 305 | [ 306 | '*.pn[0-9]' 307 | ], 308 | { 309 | 'a.pn1': 1, 310 | 'a.pn2': 1, 311 | 'a.png': 0, 312 | 'a.pna': 0 313 | } 314 | ], 315 | [ 316 | 'multiple ranges: [0-9a-z]', 317 | [ 318 | '*.pn[0-9a-z]' 319 | ], 320 | { 321 | 'a.pn1': 1, 322 | 'a.pn2': 1, 323 | 'a.png': 1, 324 | 'a.pna': 1, 325 | 'a.pn-': 0 326 | } 327 | ], 328 | [ 329 | // [0-z] represents 0-0A-Za-z 330 | 'special range: [0-z]', 331 | [ 332 | '*.[0-z]' 333 | ], 334 | { 335 | 'a.0': 1, 336 | 'a.9': 1, 337 | 'a.00': 0, 338 | 'a.a': 1, 339 | 'a.z': 1, 340 | 'a.zz': 0 341 | } 342 | ], 343 | [ 344 | // If range is out of order, then omitted 345 | 'special case: range out of order: [a-9]', 346 | [ 347 | '*.[a-9]' 348 | ], 349 | { 350 | 'a.0': 0, 351 | 'a.-': 0, 352 | 'a.9': 0 353 | } 354 | ], 355 | [ 356 | // Just treat it as normal character set 357 | 'special case: range-like character set', 358 | [ 359 | '*.[a-]' 360 | ], 361 | { 362 | 'a.a': 1, 363 | 'a.-': 1, 364 | 'a.b': 0 365 | } 366 | ], 367 | [ 368 | 'special case: the combination of range and set', 369 | [ 370 | '*.[a-z01]' 371 | ], 372 | { 373 | 'a.a': 1, 374 | 'a.b': 1, 375 | 'a.z': 1, 376 | 'a.0': 1, 377 | 'a.1': 1, 378 | 'a.2': 0 379 | } 380 | ], 381 | [ 382 | 'special case: 1 step range', 383 | [ 384 | '*.[0-0]' 385 | ], 386 | { 387 | 'a.0': 1, 388 | 'a.1': 0, 389 | 'a.-': 0 390 | } 391 | ], 392 | [ 393 | 'special case: similar, but not a character set', 394 | [ 395 | '*.[a-' 396 | ], 397 | { 398 | 'a.': 0, 399 | 'a.[': 0, 400 | 'a.a': 0, 401 | 'a.-': 0 402 | } 403 | ], 404 | [ 405 | 'related to #38', 406 | [ 407 | '*', 408 | '!abc*' 409 | ], 410 | { 411 | 'a': 1, 412 | 'abc': 0, 413 | 'abcd': 0 414 | } 415 | ], 416 | [ 417 | '#38', 418 | [ 419 | '*', 420 | '!*/', 421 | '!foo/bar' 422 | ], 423 | { 424 | 'a': 1, 425 | 'b/c': 1, 426 | 'foo/bar': 0, 427 | 'foo/e': 1 428 | } 429 | ], 430 | [ 431 | 'intermediate "\\ " should be unescaped to " "', 432 | [ 433 | 'abc\\ d', 434 | 'abc e', 435 | 'a\\ b\\ c' 436 | ], 437 | { 438 | 'abc d': 1, 439 | 'abc e': 1, 440 | 'abc/abc d': 1, 441 | 'abc/abc e': 1, 442 | 'abc/a b c': 1 443 | } 444 | ], 445 | [ 446 | '#25', 447 | [ 448 | '.git/*', 449 | '!.git/config', 450 | '.ftpconfig' 451 | ], 452 | { 453 | '.ftpconfig': 1, 454 | '.git/config': 0, 455 | '.git/description': 1 456 | } 457 | ], 458 | [ 459 | '#26: .gitignore man page sample', 460 | [ 461 | '# exclude everything except directory foo/bar', 462 | '/*', 463 | '!/foo', 464 | '/foo/*', 465 | '!/foo/bar' 466 | ], 467 | { 468 | 'no.js': 1, 469 | 'foo/no.js': 1, 470 | 'foo/bar/yes.js': 0, 471 | 'foo/bar/baz/yes.js': 0, 472 | 'boo/no.js': 1 473 | } 474 | ], 475 | [ 476 | 'wildcard: special case, escaped wildcard', 477 | [ 478 | '*.html', 479 | '!a/b/\\*/index.html' 480 | ], 481 | { 482 | 'a/b/*/index.html': 0, 483 | 'a/b/index.html': 1 484 | } 485 | ], 486 | [ 487 | 'wildcard: treated as a shell glob suitable for consumption by fnmatch(3)', 488 | [ 489 | '*.html', 490 | '!b/\\*/index.html' 491 | ], 492 | { 493 | 'a/b/*/index.html': 1, 494 | 'a/b/index.html': 1 495 | } 496 | ], 497 | [ 498 | 'wildcard: with no escape', 499 | [ 500 | '*.html', 501 | '!a/b/*/index.html' 502 | ], 503 | { 504 | 'a/b/*/index.html': 0, 505 | 'a/b/index.html': 1 506 | } 507 | ], 508 | [ 509 | '#24: a negative pattern without a trailing wildcard', 510 | [ 511 | '/node_modules/*', 512 | '!/node_modules', 513 | '!/node_modules/package' 514 | ], 515 | { 516 | 'node_modules/a/a.js': 1, 517 | 'node_modules/package/a.js': 0 518 | } 519 | ], 520 | [ 521 | '#21: unignore with 1 globstar, reversed order', 522 | [ 523 | '!foo/bar.js', 524 | 'foo/*' 525 | ], 526 | { 527 | 'foo/bar.js': 1, 528 | 'foo/bar2.js': 1, 529 | 'foo/bar/bar.js': 1 530 | } 531 | ], 532 | 533 | [ 534 | '#21: unignore with 2 globstars, reversed order', 535 | [ 536 | '!foo/bar.js', 537 | 'foo/**' 538 | ], 539 | { 540 | 'foo/bar.js': 1, 541 | 'foo/bar2.js': 1, 542 | 'foo/bar/bar.js': 1 543 | } 544 | ], 545 | 546 | [ 547 | '#21: unignore with several groups of 2 globstars, reversed order', 548 | [ 549 | '!foo/bar.js', 550 | 'foo/**/**' 551 | ], 552 | { 553 | 'foo/bar.js': 1, 554 | 'foo/bar2.js': 1, 555 | 'foo/bar/bar.js': 1 556 | } 557 | ], 558 | 559 | [ 560 | '#21: unignore with 1 globstar', 561 | [ 562 | 'foo/*', 563 | '!foo/bar.js' 564 | ], 565 | { 566 | 'foo/bar.js': 0, 567 | 'foo/bar2.js': 1, 568 | 'foo/bar/bar.js': 1 569 | } 570 | ], 571 | 572 | [ 573 | '#21: unignore with 2 globstars', 574 | [ 575 | 'foo/**', 576 | '!foo/bar.js' 577 | ], 578 | { 579 | 'foo/bar.js': 0, 580 | 'foo/bar2.js': 1, 581 | 'foo/bar/bar.js': 1 582 | } 583 | ], 584 | 585 | [ 586 | 'related to #21: several groups of 2 globstars', 587 | [ 588 | 'foo/**/**', 589 | '!foo/bar.js' 590 | ], 591 | { 592 | 'foo/bar.js': 0, 593 | 'foo/bar2.js': 1, 594 | 'foo/bar/bar.js': 1 595 | } 596 | ], 597 | 598 | // description patterns paths/expect only 599 | [ 600 | 'ignore dot files', 601 | [ 602 | '.*' 603 | ], 604 | { 605 | '.a': 1, 606 | '.gitignore': 1 607 | } 608 | ], 609 | 610 | [ 611 | '#14, README example broken in `3.0.3`', 612 | [ 613 | '.abc/*', 614 | '!.abc/d/' 615 | ], 616 | { 617 | '.abc/a.js': 1, 618 | '.abc/d/e.js': 0 619 | } 620 | ], 621 | 622 | [ 623 | '#14, README example broken in `3.0.3`, not negate parent folder', 624 | [ 625 | '.abc/*', 626 | // .abc/d will be ignored 627 | '!.abc/d/*' 628 | ], 629 | { 630 | '.abc/a.js': 1, 631 | // so '.abc/d/e.js' will be ignored 632 | '.abc/d/e.js': 1 633 | } 634 | ], 635 | 636 | [ 637 | 'A blank line matches no files', 638 | [ 639 | '' 640 | ], 641 | { 642 | 'a': 0, 643 | 'a/b/c': 0 644 | } 645 | ], 646 | [ 647 | 'A line starting with # serves as a comment.', 648 | ['#abc'], 649 | { 650 | '#abc': 0 651 | } 652 | ], 653 | [ 654 | 'Put a backslash ("\\") in front of the first hash for patterns that begin with a hash.', 655 | ['\\#abc'], 656 | { 657 | '#abc': 1 658 | } 659 | ], 660 | [ 661 | 'Trailing spaces are ignored unless they are quoted with backslash ("\\")', 662 | [ 663 | 'abc\\ ', // only one space left -> (abc ) 664 | 'bcd ', // no space left -> (bcd) 665 | 'cde \\ ', // two spaces -> (cde ) 666 | 'def ' // no space left -> (def) 667 | ], 668 | { 669 | // nothing to do with backslashes 670 | 'abc\\ ': 0, 671 | 'abc ': 0, 672 | 'abc ': 1, 673 | 'abc ': 0, 674 | 'bcd': 1, 675 | 'bcd ': 0, 676 | 'bcd ': 0, 677 | 'cde ': 1, 678 | 'cde ': 0, 679 | 'cde ': 0, 680 | 'def': 1, 681 | 'def ': 0 682 | }, 683 | false, 684 | true 685 | ], 686 | [ 687 | 'An optional prefix "!" which negates the pattern; any matching file excluded by a previous pattern will become included again', 688 | [ 689 | 'abc', 690 | '!abc' 691 | ], 692 | { 693 | // the parent folder is included again 694 | 'abc/a.js': 0, 695 | 'abc/': 0 696 | } 697 | ], 698 | [ 699 | 'issue #10: It is not possible to re-include a file if a parent directory of that file is excluded', 700 | [ 701 | '/abc/', 702 | '!/abc/a.js' 703 | ], 704 | { 705 | 'abc/a.js': 1, 706 | 'abc/d/e.js': 1 707 | } 708 | ], 709 | [ 710 | 'we did not know whether the rule is a dir first', 711 | [ 712 | 'abc', 713 | '!bcd/abc/a.js' 714 | ], 715 | { 716 | 'abc/a.js': 1, 717 | 'bcd/abc/a.js': 1 718 | } 719 | ], 720 | [ 721 | 'Put a backslash ("\\") in front of the first "!" for patterns that begin with a literal "!"', 722 | [ 723 | '\\!abc', 724 | '\\!important!.txt' 725 | ], 726 | { 727 | '!abc': 1, 728 | 'abc': 0, 729 | 'b/!important!.txt': 1, 730 | '!important!.txt': 1 731 | } 732 | ], 733 | 734 | [ 735 | 'If the pattern ends with a slash, it is removed for the purpose of the following description, but it would only find a match with a directory', 736 | [ 737 | 'abc/' 738 | ], 739 | { 740 | // actually, node-ignore have no idea about fs.Stat, 741 | // you should `glob({mark: true})` 742 | 'abc': 0, 743 | 'abc/': 1, 744 | 745 | // Actually, if there is only a trailing slash, git also treats it as a shell glob pattern 746 | // 'abc/' should make 'bcd/abc/' ignored. 747 | 'bcd/abc/': 1 748 | } 749 | ], 750 | 751 | [ 752 | 'If the pattern does not contain a slash /, Git treats it as a shell glob pattern', 753 | [ 754 | 'a.js', 755 | 'f/' 756 | ], 757 | { 758 | 'a.js': 1, 759 | 'b/a/a.js': 1, 760 | 'a/a.js': 1, 761 | 'b/a.jsa': 0, 762 | 'f/': 1, 763 | 'g/f/': 1 764 | } 765 | ], 766 | [ 767 | 'Otherwise, Git treats the pattern as a shell glob suitable for consumption by fnmatch(3) with the FNM_PATHNAME flag', 768 | [ 769 | 'a/a.js' 770 | ], 771 | { 772 | 'a/a.js': 1, 773 | 'a/a.jsa': 0, 774 | 'b/a/a.js': 0, 775 | 'c/a/a.js': 0 776 | } 777 | ], 778 | 779 | [ 780 | 'wildcards in the pattern will not match a / in the pathname.', 781 | [ 782 | 'Documentation/*.html' 783 | ], 784 | { 785 | 'Documentation/git.html': 1, 786 | 'Documentation/ppc/ppc.html': 0, 787 | 'tools/perf/Documentation/perf.html': 0 788 | } 789 | ], 790 | 791 | [ 792 | 'A leading slash matches the beginning of the pathname', 793 | [ 794 | '/*.c' 795 | ], 796 | { 797 | 'cat-file.c': 1, 798 | 'mozilla-sha1/sha1.c': 0 799 | } 800 | ], 801 | 802 | [ 803 | 'A leading "**" followed by a slash means match in all directories', 804 | [ 805 | '**/foo' 806 | ], 807 | { 808 | 'foo': 1, 809 | 'a/foo': 1, 810 | 'foo/a': 1, 811 | 'a/foo/a': 1, 812 | 'a/b/c/foo/a': 1 813 | } 814 | ], 815 | 816 | [ 817 | '"**/foo/bar" matches file or directory "bar" anywhere that is directly under directory "foo"', 818 | [ 819 | '**/foo/bar' 820 | ], 821 | { 822 | 'foo/bar': 1, 823 | 'abc/foo/bar': 1, 824 | 'abc/foo/bar/': 1 825 | } 826 | ], 827 | 828 | [ 829 | 'A trailing "/**" matches everything inside', 830 | [ 831 | 'abc/**' 832 | ], 833 | { 834 | 'abc/a/': 1, 835 | 'abc/b': 1, 836 | 'abc/d/e/f/g': 1, 837 | 'bcd/abc/a': 0, 838 | 'abc': 0 839 | } 840 | ], 841 | 842 | [ 843 | 'A slash followed by two consecutive asterisks then a slash matches zero or more directories', 844 | [ 845 | 'a/**/b' 846 | ], 847 | { 848 | 'a/b': 1, 849 | 'a/x/b': 1, 850 | 'a/x/y/b': 1, 851 | 'b/a/b': 0 852 | } 853 | ], 854 | 855 | [ 856 | 'add a file content', 857 | readPatterns('.aignore'), 858 | { 859 | 'abc/a.js': 1, 860 | 'abc/b/b.js': 1, 861 | '#e': 0, 862 | '#f': 1 863 | } 864 | ], 865 | 866 | // old test cases 867 | [ 868 | 'should excape metacharacters of regular expressions', [ 869 | '*.js', 870 | '!\\*.js', 871 | '!a#b.js', 872 | '!?.js', 873 | 874 | // comments 875 | '#abc', 876 | 877 | '\\#abc' 878 | ], { 879 | '*.js': 0, 880 | 'abc.js': 1, 881 | 'a#b.js': 0, 882 | 'abc': 0, 883 | '#abc': 1, 884 | '?.js': 0 885 | } 886 | ], 887 | 888 | [ 889 | 'issue #2: question mark should not break all things', 890 | readPatterns('.ignore-issue-2'), { 891 | '.project': 1, 892 | // remain 893 | 'abc/.project': 0, 894 | '.a.sw': 0, 895 | '.a.sw?': 1, 896 | 'thumbs.db': 1 897 | } 898 | ], 899 | [ 900 | 'dir ended with "*"', [ 901 | 'abc/*' 902 | ], { 903 | 'abc': 0 904 | } 905 | ], 906 | [ 907 | 'file ended with "*"', [ 908 | 'abc.js*' 909 | ], { 910 | 'abc.js/': 1, 911 | 'abc.js/abc': 1, 912 | 'abc.jsa/': 1, 913 | 'abc.jsa/abc': 1 914 | } 915 | ], 916 | [ 917 | 'wildcard as filename', [ 918 | '*.b' 919 | ], { 920 | 'b/a.b': 1, 921 | 'b/.b': 1, 922 | 'b/.ba': 0, 923 | 'b/c/a.b': 1 924 | } 925 | ], 926 | [ 927 | 'slash at the beginning and come with a wildcard', [ 928 | '/*.c' 929 | ], { 930 | '.c': 1, 931 | 'c.c': 1, 932 | 'c/c.c': 0, 933 | 'c/d': 0 934 | } 935 | ], 936 | [ 937 | 'dot file', [ 938 | '.d' 939 | ], { 940 | '.d': 1, 941 | '.dd': 0, 942 | 'd.d': 0, 943 | 'd/.d': 1, 944 | 'd/d.d': 0, 945 | 'd/e': 0 946 | } 947 | ], 948 | [ 949 | 'dot dir', [ 950 | '.e' 951 | ], { 952 | '.e/': 1, 953 | '.ee/': 0, 954 | 'e.e/': 0, 955 | '.e/e': 1, 956 | 'e/.e': 1, 957 | 'e/e.e': 0, 958 | 'e/f': 0 959 | } 960 | ], 961 | [ 962 | 'node modules: once', [ 963 | 'node_modules/' 964 | ], { 965 | 'node_modules/gulp/node_modules/abc.md': 1, 966 | 'node_modules/gulp/node_modules/abc.json': 1 967 | } 968 | ], 969 | [ 970 | 'node modules: sub directories', 971 | [ 972 | 'node_modules' 973 | ], { 974 | 'a/b/node_modules/abc.md': 1 975 | } 976 | ], 977 | [ 978 | 'node modules: twice', [ 979 | 'node_modules/', 980 | 'node_modules/' 981 | ], { 982 | 'node_modules/gulp/node_modules/abc.md': 1, 983 | 'node_modules/gulp/node_modules/abc.json': 1 984 | } 985 | ], 986 | [ 987 | 'unicode characters in windows paths', 988 | [ 989 | 'test' 990 | ], 991 | { 992 | 'some/path/to/test/ignored.js': 1, 993 | 'some/special/path/to/目录/test/ignored.js': 1 994 | }, 995 | false, 996 | true // git-check-ignore fails as git converts special chars to escaped unicode before printing 997 | ] 998 | ] 999 | 1000 | if (!SHOULD_TEST_WINDOWS) { 1001 | cases.push( 1002 | [ 1003 | '#68: ignore test for files named ...', 1004 | [ 1005 | '/...', 1006 | '/.....' 1007 | ], 1008 | { 1009 | '...': 1, 1010 | '....': 0, 1011 | '.....': 1, 1012 | '......': 0 1013 | } 1014 | ], 1015 | [ 1016 | '#68: file named ...', 1017 | [], 1018 | { 1019 | '...': 0, 1020 | '....': 0, 1021 | '.....': 0 1022 | } 1023 | ], 1024 | [ 1025 | '#130: consequent escaped backslashes with whitespaces', 1026 | [ 1027 | 'a\\\\ ', 1028 | 'a\\\\ b', 1029 | 'a\\\\\\ b' 1030 | ], 1031 | { 1032 | 'a\\': 1, 1033 | 'a\\ b': 1, 1034 | 'a\\\\ b': 0, 1035 | 'a\\\\\\ b': 0 1036 | } 1037 | ], 1038 | [ 1039 | '#81: invalid trailing backslash at the end should not throw, test non-windows env only', 1040 | [ 1041 | 'test\\', 1042 | 'testa\\\\', 1043 | '\\', 1044 | 'foo/*', 1045 | // test negative pattern 1046 | '!foo/test\\' 1047 | ], 1048 | { 1049 | 'test': 0, 1050 | 'test\\': 0, 1051 | 'testa\\': 1, 1052 | '\\': 0, 1053 | 'foo/test\\': 1 1054 | } 1055 | ], 1056 | [ 1057 | 'linux: back slashes on paths', 1058 | [ 1059 | 'a', 1060 | 'b\\\\c' 1061 | ], 1062 | { 1063 | 'b\\c/a.md': 1, 1064 | 'a\\b/a.js': 0, 1065 | 'a\\b/a': 1, 1066 | 'a/a.js': 1 1067 | } 1068 | ], 1069 | [ 1070 | '#59: test cases for linux only', 1071 | [ 1072 | 'src/\\[foo\\]', // 1 -> 0 1073 | 'src/\\[foo2\\\\]', // 2 -> 1 1074 | 'src/\\[foo3\\\\\\]', // 3 -> 1 1075 | 'src/\\[foo4\\\\\\\\]', // 4 -> 2 1076 | 'src/\\[foo5\\\\\\\\\\]', // 5 -> 2 1077 | 'src/\\[foo6\\\\\\\\\\\\]', // 6 -> 3 1078 | 1079 | 'src/\\[bar]', 1080 | 1081 | 'src/[e\\\\]', 1082 | 's/[f\\\\\\\\]', 1083 | 1084 | 's/[a-z0-9]' 1085 | ], 1086 | { 1087 | 'src/[foo]': 1, 1088 | 'src/[foo2\\]': 1, 1089 | 1090 | // Seems the followings are side-effects, 1091 | // however, we will implement these 1092 | 'src/[foo3\\]': 1, 1093 | 'src/[foo4\\\\]': 1, 1094 | 'src/[foo5\\\\]': 1, 1095 | 'src/[foo6\\\\\\]': 1, 1096 | 1097 | 'src/[bar]': 1, 1098 | 1099 | 'src/e': 1, 1100 | 'src/\\': 1, 1101 | 's/f': 1, 1102 | 's/\\': 1, 1103 | 1104 | 's/a': 1, 1105 | 1106 | 's/0': 1 1107 | } 1108 | ] 1109 | ) 1110 | } 1111 | 1112 | const cases_to_test_only = cases.filter(c => c[3]) 1113 | 1114 | const real_cases = cases_to_test_only.length 1115 | ? cases_to_test_only 1116 | : cases 1117 | 1118 | exports.cases = iteratee => { 1119 | real_cases.forEach(single => { 1120 | const [ 1121 | description, 1122 | patterns, 1123 | paths_object, 1124 | test_only, 1125 | skip_test_fixture, 1126 | scopes = false 1127 | ] = single 1128 | 1129 | // All paths to test 1130 | const paths = Object.keys(paths_object) 1131 | 1132 | // paths that NOT ignored 1133 | let expected = paths 1134 | .filter(p => !paths_object[p]) 1135 | .sort() 1136 | 1137 | function expect_result (t, result, mapper) { 1138 | if (mapper) { 1139 | expected = expected.map(mapper) 1140 | } 1141 | 1142 | t.same(result.sort(), expected.sort()) 1143 | } 1144 | 1145 | iteratee({ 1146 | description, 1147 | patterns, 1148 | paths_object, 1149 | test_only, 1150 | skip_test_fixture, 1151 | paths, 1152 | scopes, 1153 | expected, 1154 | expect_result 1155 | }) 1156 | }) 1157 | } 1158 | 1159 | // For local testing purpose 1160 | const ENV_KEYS = [ 1161 | 'IGNORE_ONLY_FILTER', 1162 | 'IGNORE_ONLY_CREATE_FILTER', 1163 | 'IGNORE_ONLY_IGNORES', 1164 | 'IGNORE_ONLY_CHECK_IGNORE', 1165 | 'IGNORE_ONLY_WIN32', 1166 | 'IGNORE_ONLY_FIXTURES', 1167 | 'IGNORE_ONLY_OTHERS' 1168 | ] 1169 | 1170 | const envs = {} 1171 | let hasOnly = false 1172 | ENV_KEYS.forEach(key => { 1173 | const value = !!process.env[key] 1174 | envs[key] = value 1175 | 1176 | if (value) { 1177 | hasOnly = true 1178 | } 1179 | }) 1180 | 1181 | exports.checkEnv = key => hasOnly 1182 | ? !!envs[key] 1183 | : true 1184 | 1185 | exports.IS_WINDOWS = IS_WINDOWS 1186 | exports.SHOULD_TEST_WINDOWS = SHOULD_TEST_WINDOWS 1187 | 1188 | exports.debug = debug 1189 | -------------------------------------------------------------------------------- /test/git-check-ignore.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const tap = require('tap') 5 | const spawn = require('spawn-sync') 6 | const tmp = require('tmp').dirSync 7 | const mkdirp = require('mkdirp').mkdirp.sync 8 | const rm = require('rimraf').sync 9 | const {removeEnding} = require('pre-suf') 10 | const { 11 | debug, 12 | cases, 13 | checkEnv, 14 | IS_WINDOWS 15 | } = require('./fixtures/cases') 16 | 17 | const {test} = tap 18 | 19 | // This test file is related to dealing with file systems which takes time 20 | tap.setTimeout(600000) 21 | 22 | const touch = (root, file, content) => { 23 | const dirs = file.split('/') 24 | const basename = dirs.pop() 25 | 26 | const dir = dirs.join('/') 27 | 28 | if (dir) { 29 | mkdirp(path.join(root, dir)) 30 | } 31 | 32 | // abc/ -> should not create file, but only dir 33 | if (basename) { 34 | fs.writeFileSync(path.join(root, file), content || '') 35 | } 36 | } 37 | 38 | const containsInOthers = (_path, index, paths) => { 39 | _path = removeEnding(_path, '/') 40 | 41 | return paths.some((p, i) => { 42 | if (index === i) { 43 | return false 44 | } 45 | 46 | return p === _path 47 | || p.indexOf(_path) === 0 && p[_path.length] === '/' 48 | }) 49 | } 50 | 51 | let tmpCount = 0 52 | const tmpRoot = tmp().name 53 | 54 | const createUniqueTmp = () => { 55 | const dir = path.join(tmpRoot, String(tmpCount ++)) 56 | // Make sure the dir not exists, 57 | // clean up dirty things 58 | rm(dir) 59 | mkdirp(dir) 60 | return dir 61 | } 62 | 63 | const debugSpawn = (...args) => { 64 | const out = spawn(...args) 65 | debug(out.output.toString()) 66 | } 67 | 68 | const mapObjectRule = rule => 69 | typeof rule === 'string' 70 | ? rule 71 | : rule.pattern 72 | 73 | const getNativeGitIgnoreResults = (rules, paths) => { 74 | const dir = createUniqueTmp() 75 | 76 | const gitignore = typeof rules === 'string' 77 | ? rules 78 | : rules.map(mapObjectRule).join('\n') 79 | 80 | touch(dir, '.gitignore', gitignore) 81 | 82 | paths.forEach((p, i) => { 83 | if (p === '.gitignore') { 84 | return 85 | } 86 | 87 | // We do not know if a path is NOT a file, 88 | // if we: 89 | // `touch a` 90 | // and then `touch a/b`, then boooom! 91 | if (containsInOthers(p, i, paths)) { 92 | return 93 | } 94 | 95 | touch(dir, p) 96 | }) 97 | 98 | spawn('git', ['init'], { 99 | cwd: dir 100 | }) 101 | 102 | spawn('git', ['add', '-A'], { 103 | cwd: dir 104 | }) 105 | 106 | debugSpawn('ls', ['-alF'], { 107 | cwd: dir 108 | }) 109 | 110 | debugSpawn('cat', ['.gitignore'], { 111 | cwd: dir 112 | }) 113 | 114 | return paths 115 | .filter(p => { 116 | let out = spawn('git', [ 117 | 'check-ignore', 118 | '--no-index', 119 | // `spawn` will escape the special cases for us 120 | p 121 | ], { 122 | cwd: dir 123 | }) 124 | .stdout 125 | .toString() 126 | // If a path has back slashes and is ignored by .gitignore, 127 | // the output of `git check-ignore` will contain 128 | // double quote pairs and CRLF 129 | // output: "b\\c/a.md" 130 | // -> string: 'b\\c.md' 131 | .replace(/\\\\/g, '\\') 132 | .replace(/^"?(.+?)"?(?:\r|\n)*$/g, (m, p1) => p1) 133 | 134 | out = removeEnding(out, '\n') 135 | 136 | debug('git check-ignore %s: %s -> ignored: %s', p, out, out === p) 137 | 138 | const ignored = out === p 139 | return !ignored 140 | }) 141 | } 142 | 143 | const notGitBuiltin = filename => filename.indexOf('.git/') !== 0 144 | 145 | checkEnv('IGNORE_ONLY_FIXTURES') && cases(({ 146 | description, 147 | patterns, 148 | skip_test_fixture, 149 | paths, 150 | expected, 151 | expect_result 152 | }) => { 153 | if ( 154 | // In some platform, the behavior of git command about trailing spaces 155 | // is not implemented as documented, so skip testing 156 | skip_test_fixture 157 | // Tired to handle test cases for test cases for windows 158 | || IS_WINDOWS 159 | // `git check-ignore` could only handles non-empty filenames 160 | || !paths.some(Boolean) 161 | // `git check-ignore` will by default ignore .git/ directory 162 | // which `node-ignore` should not do as well 163 | || !expected.every(notGitBuiltin) 164 | ) { 165 | return 166 | } 167 | 168 | test(`test for test: ${description}`, t => { 169 | const result = getNativeGitIgnoreResults(patterns, paths).sort() 170 | 171 | expect_result(t, result) 172 | t.end() 173 | }) 174 | }) 175 | 176 | test(`dummy test for windows`, t => { 177 | t.pass() 178 | t.end() 179 | }) 180 | -------------------------------------------------------------------------------- /test/ignore.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | test, 3 | only 4 | } = require('tap') 5 | 6 | const ignore = require('..') 7 | const { 8 | cases, 9 | checkEnv, 10 | SHOULD_TEST_WINDOWS 11 | } = require('./fixtures/cases') 12 | 13 | const make_win32 = path => path.replace(/\//g, '\\') 14 | 15 | cases(({ 16 | description, 17 | scopes, 18 | patterns, 19 | paths_object, 20 | test_only, 21 | paths, 22 | expect_result 23 | }) => { 24 | const tt = test_only 25 | ? only 26 | : test 27 | 28 | const check = (env, scope) => { 29 | if (!checkEnv(env)) { 30 | return false 31 | } 32 | 33 | if (!scope || scopes === false) { 34 | return true 35 | } 36 | 37 | return scopes.includes(scope) 38 | } 39 | 40 | check('IGNORE_ONLY_FILTER', 'filter') 41 | && tt(`.filter(): ${description}`, t => { 42 | const ig = ignore() 43 | const result = ig 44 | .addPattern(patterns) 45 | .filter(paths) 46 | 47 | expect_result(t, result) 48 | t.end() 49 | }) 50 | 51 | check('IGNORE_ONLY_CREATE_FILTER', 'createFilter') 52 | && tt(`.createFilter(): ${description}`, t => { 53 | const result = paths.filter( 54 | ignore() 55 | .addPattern(patterns) 56 | .createFilter(), 57 | // thisArg should be binded 58 | null 59 | ) 60 | 61 | expect_result(t, result) 62 | t.end() 63 | }) 64 | 65 | check('IGNORE_ONLY_IGNORES', 'ignores') 66 | && tt(`.ignores(path): ${description}`, t => { 67 | const ig = ignore().addPattern(patterns) 68 | 69 | Object.keys(paths_object).forEach(path => { 70 | const should_ignore = !!paths_object[path] 71 | const not = should_ignore ? '' : 'not ' 72 | 73 | t.equal( 74 | ig.ignores(path), 75 | should_ignore, 76 | `path: "${path}" should ${not}be ignored` 77 | ) 78 | }) 79 | t.end() 80 | }) 81 | 82 | check('IGNORE_ONLY_CHECK_IGNORE', 'checkIgnore') 83 | && tt(`.checkIgnore(path): ${description}`, t => { 84 | const ig = ignore().addPattern(patterns) 85 | 86 | Object.keys(paths_object).forEach(path => { 87 | const should_ignore = !!paths_object[path] 88 | const not = should_ignore ? '' : 'not ' 89 | const {ignored} = ig.checkIgnore(path) 90 | 91 | t.equal( 92 | ignored, 93 | should_ignore, 94 | `path: "${path}" should ${not}be ignored` 95 | ) 96 | }) 97 | t.end() 98 | }) 99 | 100 | if (!SHOULD_TEST_WINDOWS) { 101 | return 102 | } 103 | 104 | check('IGNORE_ONLY_WIN32', 'filter') 105 | && tt(`win32: .filter(): ${description}`, t => { 106 | const win_paths = paths.map(make_win32) 107 | 108 | const ig = ignore() 109 | const result = ig 110 | .addPattern(patterns) 111 | .filter(win_paths) 112 | 113 | expect_result(t, result, make_win32) 114 | t.end() 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /test/import/simple.cjs: -------------------------------------------------------------------------------- 1 | const ignore = require('../..') // eslint-disable-line @typescript-eslint/no-require-imports 2 | const {isPathValid} = require('../..') // eslint-disable-line @typescript-eslint/no-require-imports 3 | 4 | const equal = (actual, expect, message) => { 5 | if (actual !== expect) { 6 | throw new Error(`${message}, expect: ${expect}, actual: ${actual}`) 7 | } 8 | } 9 | 10 | const paths = ['a', 'a/b', 'foo/bar'] 11 | 12 | let ig = ignore() 13 | 14 | ig = ig.add('*') 15 | ig = ig.add(['!*/', '!foo/bar']) 16 | 17 | const filter = ig.createFilter() 18 | paths.filter(filter) 19 | const passed = filter('a') 20 | equal(passed, false, 'filters a out') 21 | 22 | const filtered_paths = ig.filter(paths) 23 | const ignores = ig.ignores('a') 24 | equal(ignores, true, 'ignores a') 25 | 26 | let ig2 = ignore() 27 | 28 | ig2 = ig2.add('# test ig.add(Ignore)') 29 | ig2 = ig2.add(ig) 30 | 31 | let ig3 = ignore() 32 | ig3 = ig3.add('*.js') 33 | 34 | let ig4 = ignore() 35 | ig4 = ig4.add('*.png') 36 | 37 | ig2 = ig2.add([ig3, ig4]) 38 | 39 | const ig5 = ignore({ 40 | ignorecase: false 41 | }) 42 | 43 | const isValid = isPathValid('./foo') 44 | equal(isValid, false, './foo is not valid') 45 | 46 | const { 47 | ignored, 48 | unignored 49 | } = ig4.test('foo') 50 | 51 | equal(ignored, false, 'not ignored') 52 | equal(unignored, false, 'not unignored') 53 | 54 | // Filter an Readyonly array 55 | const readonlyPaths = ['a', 'a/b', 'foo/bar'] 56 | ig.filter(readonlyPaths) 57 | 58 | // Add an Readonly array of rules 59 | const ig6 = ignore() 60 | ig6.add([ig3, ig4]) 61 | 62 | // options.ignoreCase and options.allowRelativePaths 63 | ignore({ 64 | ignoreCase: false, 65 | allowRelativePaths: true 66 | }) 67 | 68 | const ig7 = ignore() 69 | 70 | ig7.add({pattern: 'foo/*', mark: '10'}) 71 | const { 72 | ignored: ignored7, 73 | rule: ignoreRule7 74 | } = ig7.checkIgnore('foo/') 75 | 76 | equal(ignored7, true, 'should ignore') 77 | equal(ignoreRule7.mark, '10', 'mark is 10') 78 | equal(ignoreRule7.pattern, 'foo/*', 'ignored by foo/*') 79 | -------------------------------------------------------------------------------- /test/import/simple.mjs: -------------------------------------------------------------------------------- 1 | import ignore, {isPathValid} from '../../index.js' // eslint-disable-line import/extensions 2 | 3 | const equal = (actual, expect, message) => { 4 | if (actual !== expect) { 5 | throw new Error(`${message}, expect: ${expect}, actual: ${actual}`) 6 | } 7 | } 8 | 9 | const paths = ['a', 'a/b', 'foo/bar'] 10 | 11 | let ig = ignore() 12 | 13 | ig = ig.add('*') 14 | ig = ig.add(['!*/', '!foo/bar']) 15 | 16 | const filter = ig.createFilter() 17 | paths.filter(filter) 18 | const passed = filter('a') 19 | equal(passed, false, 'filters a out') 20 | 21 | const filtered_paths = ig.filter(paths) 22 | const ignores = ig.ignores('a') 23 | equal(ignores, true, 'ignores a') 24 | 25 | let ig2 = ignore() 26 | 27 | ig2 = ig2.add('# test ig.add(Ignore)') 28 | ig2 = ig2.add(ig) 29 | 30 | let ig3 = ignore() 31 | ig3 = ig3.add('*.js') 32 | 33 | let ig4 = ignore() 34 | ig4 = ig4.add('*.png') 35 | 36 | ig2 = ig2.add([ig3, ig4]) 37 | 38 | const ig5 = ignore({ 39 | ignorecase: false 40 | }) 41 | 42 | const isValid = isPathValid('./foo') 43 | equal(isValid, false, './foo is not valid') 44 | 45 | const { 46 | ignored, 47 | unignored 48 | } = ig4.test('foo') 49 | 50 | equal(ignored, false, 'not ignored') 51 | equal(unignored, false, 'not unignored') 52 | 53 | // Filter an Readyonly array 54 | const readonlyPaths = ['a', 'a/b', 'foo/bar'] 55 | ig.filter(readonlyPaths) 56 | 57 | // Add an Readonly array of rules 58 | const ig6 = ignore() 59 | ig6.add([ig3, ig4]) 60 | 61 | // options.ignoreCase and options.allowRelativePaths 62 | ignore({ 63 | ignoreCase: false, 64 | allowRelativePaths: true 65 | }) 66 | 67 | const ig7 = ignore() 68 | 69 | ig7.add({pattern: 'foo/*', mark: '10'}) 70 | const { 71 | ignored: ignored7, 72 | rule: ignoreRule7 73 | } = ig7.checkIgnore('foo/') 74 | 75 | equal(ignored7, true, 'should ignore') 76 | equal(ignoreRule7.mark, '10', 'mark is 10') 77 | equal(ignoreRule7.pattern, 'foo/*', 'ignored by foo/*') 78 | -------------------------------------------------------------------------------- /test/import/simple.ts: -------------------------------------------------------------------------------- 1 | import ignore, {isPathValid} from '../..' 2 | import type {Ignore} from '../..' 3 | 4 | const equal = (actual: unknown, expect: unknown, message: string) => { 5 | if (actual !== expect) { 6 | throw new Error(`${message}, expect: ${expect}, actual: ${actual}`) 7 | } 8 | } 9 | 10 | const paths = ['a', 'a/b', 'foo/bar'] 11 | 12 | let ig: Ignore = ignore() 13 | 14 | ig = ig.add('*') 15 | ig = ig.add(['!*/', '!foo/bar']) 16 | 17 | const filter = ig.createFilter() 18 | paths.filter(filter) 19 | const passed: boolean = filter('a') 20 | equal(passed, false, 'filters a out') 21 | 22 | const filtered_paths: Array = ig.filter(paths) 23 | const ignores: boolean = ig.ignores('a') 24 | equal(ignores, true, 'ignores a') 25 | 26 | let ig2 = ignore() 27 | 28 | ig2 = ig2.add('# test ig.add(Ignore)') 29 | ig2 = ig2.add(ig) 30 | 31 | let ig3 = ignore() 32 | ig3 = ig3.add('*.js') 33 | 34 | let ig4 = ignore() 35 | ig4 = ig4.add('*.png') 36 | 37 | ig2 = ig2.add([ig3, ig4]) 38 | 39 | const ig5 = ignore({ 40 | ignorecase: false 41 | }) 42 | 43 | const isValid: boolean = isPathValid('./foo') 44 | equal(isValid, false, './foo is not valid') 45 | 46 | const { 47 | ignored, 48 | unignored 49 | }: { 50 | ignored: boolean, 51 | unignored: boolean 52 | } = ig4.test('foo') 53 | 54 | equal(ignored, false, 'not ignored') 55 | equal(unignored, false, 'not unignored') 56 | 57 | // Filter an Readyonly array 58 | const readonlyPaths = ['a', 'a/b', 'foo/bar'] as const 59 | ig.filter(readonlyPaths) 60 | 61 | // Add an Readonly array of rules 62 | const ig6 = ignore() 63 | ig6.add([ig3, ig4] as const) 64 | 65 | // options.ignoreCase and options.allowRelativePaths 66 | ignore({ 67 | ignoreCase: false, 68 | allowRelativePaths: true 69 | }) 70 | 71 | const ig7 = ignore() 72 | 73 | ig7.add({pattern: 'foo/*', mark: '10'}) 74 | const { 75 | ignored: ignored7, 76 | rule: ignoreRule7 77 | } = ig7.checkIgnore('foo/') 78 | 79 | equal(ignored7, true, 'should ignore') 80 | equal(ignoreRule7?.mark, '10', 'mark is 10') 81 | equal(ignoreRule7?.pattern, 'foo/*', 'ignored by foo/*') 82 | -------------------------------------------------------------------------------- /test/others.test.js: -------------------------------------------------------------------------------- 1 | // - issues 2 | // - options 3 | // - static methods 4 | // - .test() 5 | 6 | const {test} = require('tap') 7 | const ignore = require('..') 8 | const { 9 | checkEnv, 10 | SHOULD_TEST_WINDOWS 11 | } = require('./fixtures/cases') 12 | 13 | const {isPathValid} = ignore 14 | 15 | const _test = checkEnv('IGNORE_ONLY_OTHERS') 16 | ? test 17 | : () => {} 18 | 19 | _test('.add()', t => { 20 | const a = ignore().add(['.abc/*', '!.abc/d/']) 21 | const b = ignore().add(a).add('!.abc/e/') 22 | 23 | const paths = [ 24 | '.abc/a.js', // filtered out 25 | '.abc/d/e.js', // included 26 | '.abc/e/e.js' // included by b, filtered out by a 27 | ] 28 | 29 | t.same(a.filter(paths), ['.abc/d/e.js']) 30 | t.same(b.filter(paths), ['.abc/d/e.js', '.abc/e/e.js']) 31 | t.end() 32 | }) 33 | 34 | _test('fixes babel class', t => { 35 | const {constructor} = ignore() 36 | 37 | try { 38 | constructor() 39 | } catch (e) { 40 | t.end() 41 | return 42 | } 43 | 44 | t.equal('there should be an error', 'no error found') 45 | t.end() 46 | }) 47 | 48 | _test('#32', t => { 49 | const KEY_IGNORE = typeof Symbol !== 'undefined' 50 | ? Symbol.for('node-ignore') 51 | : 'node-ignore' 52 | 53 | const a = ignore().add(['.abc/*', '!.abc/d/']) 54 | 55 | // aa is actually not an IgnoreBase instance 56 | const aa = {} 57 | 58 | /* eslint no-underscore-dangle: ["off"] */ 59 | aa._rules = { 60 | _rules: a._rules._rules.slice() 61 | } 62 | aa[KEY_IGNORE] = true 63 | 64 | const b = ignore().add(aa).add('!.abc/e/') 65 | 66 | const paths = [ 67 | '.abc/a.js', // filtered out 68 | '.abc/d/e.js', // included 69 | '.abc/e/e.js' // included by b, filtered out by a 70 | ] 71 | 72 | t.same(a.filter(paths), ['.abc/d/e.js']) 73 | t.same(b.filter(paths), ['.abc/d/e.js', '.abc/e/e.js']) 74 | t.end() 75 | }) 76 | 77 | _test('options.ignorecase', t => { 78 | const ig = ignore({ 79 | ignorecase: false 80 | }) 81 | 82 | ig.add('*.[jJ][pP]g') 83 | 84 | t.equal(ig.ignores('a.jpg'), true) 85 | t.equal(ig.ignores('a.JPg'), true) 86 | t.equal(ig.ignores('a.JPG'), false) 87 | t.end() 88 | }) 89 | 90 | _test('special case: internal cache respects ignorecase', t => { 91 | const rule = '*.[jJ][pP]g' 92 | 93 | const ig = ignore({ 94 | ignorecase: false 95 | }) 96 | 97 | ig.add(rule) 98 | 99 | t.equal(ig.ignores('a.JPG'), false) 100 | 101 | const ig2 = ignore({ 102 | ignorecase: true 103 | }) 104 | 105 | ig2.add(rule) 106 | 107 | t.equal(ig2.ignores('a.JPG'), true) 108 | 109 | t.end() 110 | }) 111 | 112 | _test('special case: invalid paths, throw', t => { 113 | const ig = ignore() 114 | 115 | const emptyMessage = 'path must be a string, but got ""' 116 | 117 | t.throws(() => ig.ignores(''), emptyMessage) 118 | 119 | t.throws( 120 | () => ig.ignores(false), 121 | 'path must be a string, but got `false`' 122 | ) 123 | 124 | t.throws( 125 | () => ig.ignores('/a'), 126 | 'path must be `path.relative()`d, but got "/a"' 127 | ) 128 | 129 | if (SHOULD_TEST_WINDOWS) { 130 | t.throws( 131 | () => ig.ignores('c:\\a'), 132 | 'path must be `path.relative()`d, but got "c:\\a"' 133 | ) 134 | 135 | t.throws( 136 | () => ig.ignores('C:\\a'), 137 | 'path must be `path.relative()`d, but got "C:\\a"' 138 | ) 139 | } 140 | 141 | t.throws(() => ig.filter(['']), emptyMessage) 142 | 143 | t.throws(() => [''].filter(ig.createFilter()), emptyMessage) 144 | 145 | t.end() 146 | }) 147 | 148 | _test('isPathValid', t => { 149 | const paths = [ 150 | '.', 151 | './foo', 152 | '../foo', 153 | '/foo', 154 | false, 155 | 'foo' 156 | ] 157 | 158 | if (SHOULD_TEST_WINDOWS) { 159 | paths.push( 160 | '..\\foo', 161 | '.\\foo', 162 | '\\foo', 163 | '\\\\foo', 164 | 'C:\\foo', 165 | 'd:\\foo' 166 | ) 167 | } 168 | 169 | t.same( 170 | paths.filter(isPathValid), 171 | [ 172 | 'foo' 173 | ] 174 | ) 175 | 176 | t.end() 177 | }) 178 | 179 | const IGNORE_TEST_CASES = [ 180 | [ 181 | // Description 182 | 'test: no rule', 183 | // patterns 184 | null, 185 | // path 186 | 'foo', 187 | // ignored, unignored 188 | [false, false] 189 | ], 190 | [ 191 | 'test: has rule, no match', 192 | 'bar', 193 | 'foo', 194 | [false, false] 195 | ], 196 | [ 197 | 'test: only negative', 198 | '!foo', 199 | 'foo', 200 | [false, true] 201 | ], 202 | [ 203 | 'test: ignored then unignored', 204 | ['foo', '!foo'], 205 | 'foo', 206 | [false, true] 207 | ], 208 | [ 209 | 'test: dir ignored then unignored -> not matched', 210 | ['foo', '!foo'], 211 | 'foo/bar', 212 | [false, false] 213 | ], 214 | [ 215 | 'test: ignored by wildcard, then unignored', 216 | ['*.js', '!a/a.js'], 217 | 'a/a.js', 218 | [false, true] 219 | ] 220 | ] 221 | 222 | if (!SHOULD_TEST_WINDOWS) { 223 | IGNORE_TEST_CASES.push([ 224 | `test: file which named '...'`, 225 | null, 226 | '...', 227 | [false, false] 228 | ]) 229 | } 230 | 231 | IGNORE_TEST_CASES.forEach(([d, patterns, path, [ignored, unignored]]) => { 232 | _test(d, t => { 233 | const ig = ignore() 234 | if (patterns) { 235 | ig.add(patterns) 236 | } 237 | 238 | t.same(ig.test(path), { 239 | ignored, unignored 240 | }) 241 | 242 | t.end() 243 | }) 244 | }) 245 | 246 | _test('options.allowRelativePaths = true', t => { 247 | const ig = ignore({ 248 | allowRelativePaths: true 249 | }) 250 | 251 | ig.add('foo') 252 | 253 | t.equal(ig.ignores('../foo/bar.js'), true) 254 | 255 | t.throws(() => ignore().ignores('../foo/bar.js')) 256 | 257 | t.end() 258 | }) 259 | 260 | _test('options.allowRelativePaths = false (default value)', t => { 261 | const ig = ignore() 262 | 263 | ig.add('foo') 264 | 265 | t.throws(() => ig.ignores('../foo/bar.js'), 'path.relative') 266 | t.throws(() => ig.ignores('/foo/bar.js'), 'path.relative') 267 | 268 | t.end() 269 | }) 270 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true 4 | } 5 | } 6 | --------------------------------------------------------------------------------