├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── benchmark.yml │ ├── codeql.yml │ └── main.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── __snapshots__ ├── absolute.e2e.js ├── base-name-match.e2e.js ├── case-sensitive-match.e2e.js ├── deep.e2e.js ├── dot.e2e.js ├── errors.e2e.js ├── ignore.e2e.js ├── mark-directories.e2e.js ├── only-directories.e2e.js ├── only-files.e2e.js ├── regular.e2e.js ├── root.e2e.js ├── static.e2e.js └── unique.e2e.js ├── eslint.config.mjs ├── fixtures ├── .directory │ └── file.md ├── .file ├── file.md ├── first │ ├── file.md │ └── nested │ │ ├── directory │ │ ├── file.json │ │ └── file.md │ │ └── file.md ├── second │ ├── file.md │ └── nested │ │ ├── directory │ │ └── file.md │ │ └── file.md └── third │ └── library │ ├── a │ └── book.md │ └── b │ └── book.md ├── herebyfile.mjs ├── package.json ├── src ├── benchmark │ ├── suites │ │ ├── overhead │ │ │ ├── async.ts │ │ │ ├── stream.ts │ │ │ └── sync.ts │ │ ├── product │ │ │ ├── async.ts │ │ │ ├── stream.ts │ │ │ └── sync.ts │ │ └── regression │ │ │ ├── async.ts │ │ │ ├── stream.ts │ │ │ └── sync.ts │ └── utils.ts ├── index.spec.ts ├── index.ts ├── managers │ ├── tasks.spec.ts │ └── tasks.ts ├── providers │ ├── async.spec.ts │ ├── async.ts │ ├── filters │ │ ├── deep.spec.ts │ │ ├── deep.ts │ │ ├── entry.spec.ts │ │ ├── entry.ts │ │ ├── error.spec.ts │ │ └── error.ts │ ├── index.ts │ ├── matchers │ │ ├── matcher.spec.ts │ │ ├── matcher.ts │ │ ├── partial.spec.ts │ │ └── partial.ts │ ├── provider.spec.ts │ ├── provider.ts │ ├── stream.spec.ts │ ├── stream.ts │ ├── sync.spec.ts │ ├── sync.ts │ └── transformers │ │ ├── entry.spec.ts │ │ └── entry.ts ├── readers │ ├── async.spec.ts │ ├── async.ts │ ├── index.ts │ ├── reader.spec.ts │ ├── reader.ts │ ├── stream.spec.ts │ ├── stream.ts │ ├── sync.spec.ts │ └── sync.ts ├── settings.spec.ts ├── settings.ts ├── tests │ ├── e2e │ │ ├── errors.e2e.ts │ │ ├── options │ │ │ ├── absolute.e2e.ts │ │ │ ├── base-name-match.e2e.ts │ │ │ ├── case-sensitive-match.e2e.ts │ │ │ ├── deep.e2e.ts │ │ │ ├── dot.e2e.ts │ │ │ ├── ignore.e2e.ts │ │ │ ├── mark-directories.e2e.ts │ │ │ ├── only-directories.e2e.ts │ │ │ ├── only-files.e2e.ts │ │ │ └── unique.e2e.ts │ │ ├── patterns │ │ │ ├── regular.e2e.ts │ │ │ ├── root.e2e.ts │ │ │ └── static.e2e.ts │ │ └── runner.ts │ ├── index.ts │ └── utils │ │ ├── entry.ts │ │ ├── errno.ts │ │ ├── fs.ts │ │ ├── pattern.ts │ │ ├── platform.ts │ │ └── task.ts ├── types │ └── index.ts └── utils │ ├── array.spec.ts │ ├── array.ts │ ├── errno.spec.ts │ ├── errno.ts │ ├── fs.spec.ts │ ├── fs.ts │ ├── index.ts │ ├── path.spec.ts │ ├── path.ts │ ├── pattern.spec.ts │ ├── pattern.ts │ ├── stream.spec.ts │ ├── stream.ts │ ├── string.spec.ts │ └── string.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [{.travis.yml,circle.yml,appveyor.yml,.github/**}] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [{npm-shrinkwrap.json,package-lock.json,package.json}] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "mrmlnc", 3 | "rules": { 4 | "no-magic-numbers": "off", 5 | "@typescript-eslint/no-magic-numbers": "off" 6 | }, 7 | "overrides": [ 8 | { 9 | "files": [ 10 | "**/*.e2e.ts" 11 | ], 12 | "rules": { 13 | "unicorn/prevent-abbreviations": [ 14 | "error", 15 | { 16 | "checkFilenames": false 17 | } 18 | ] 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at dmalinochkin@outlook.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to my package 2 | 3 | Welcome, and thank you for your interest in contributing to **fast-glob**! 4 | 5 | Please note that this project is released with a [Contributor Code of Conduct](CODE-OF-CONDUCT.md). By participating in this project you agree to abide by its terms. 6 | 7 | ## Contribution Guidelines 8 | 9 | There are a couple of ways you can contribute to this repository: 10 | 11 | * **Ideas, feature requests and bugs**: We are open to all ideas and we want to get rid of bugs! Use the [Issues section](https://github.com/mrmlnc/fast-glob/issues) to either report a new issue, provide your ideas or contribute to existing threads. 12 | * **Documentation**: Found a typo or strangely worded sentences? Submit a PR! 13 | * **Code**: Contribute bug fixes, features or design changes. 14 | 15 | ### Creating an Issue 16 | 17 | Before you create a new Issue: 18 | 19 | * Check the [Issues](https://github.com/mrmlnc/fast-glob/issues) on GitHub to ensure one doesn't already exist. 20 | * Clearly describe the issue, including the steps to reproduce the issue. 21 | 22 | ### Making Changes 23 | 24 | #### Getting Started 25 | 26 | * Install [Node.js](https://nodejs.org/en/). 27 | * Fork the project and clone the fork repository. ([how to create fork?](https://help.github.com/articles/fork-a-repo/#fork-an-example-repository)). 28 | * Create a topic branch from the master branch. 29 | * Run `npm install` to install the application dependencies. 30 | 31 | #### Setup 32 | 33 | > 📖 Only `npm` is supported for working with this repository. Problems with other package managers will be ignored. 34 | 35 | ```bash 36 | # Clone repository 37 | git clone https://github.com/mrmlnc/fast-glob 38 | cd fast-glob 39 | 40 | # Install dependencies 41 | npm install 42 | 43 | # Build package 44 | npm run build 45 | 46 | # Run tests 47 | npm t 48 | npm run test:e2e 49 | 50 | # Watch changes 51 | npm run watch 52 | 53 | # Run benchmark 54 | npm run bench:async 55 | npm run bench:sync 56 | npm run bench:stream 57 | ``` 58 | 59 | #### Commit 60 | 61 | Keep git commit messages clear and appropriate. You can use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Environment 2 | 3 | * OS Version: … 4 | * Node.js Version: … 5 | 6 | ### Actual behavior 7 | … 8 | 9 | ### Expected behavior 10 | … 11 | 12 | ### Steps to reproduce 13 | 14 | 1. … 15 | 16 | ### Code sample 17 | 18 | ```js 19 | // Paste your code here. 20 | ``` 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What is the purpose of this pull request? 2 | … 3 | 4 | ### What changes did you make? (Give an overview) 5 | … 6 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Benchmark 2 | 3 | on: 4 | push: 5 | branches: ['master', 'releases/*'] 6 | pull_request: 7 | branches: ['*'] 8 | 9 | jobs: 10 | product: 11 | name: Product benchmark 12 | concurrency: 13 | group: ${{ github.workflow }}-benchmark-product-${{ github.ref }} 14 | cancel-in-progress: true 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Setup repository 18 | uses: actions/checkout@v4 19 | - name: Setup environment 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 22 23 | - name: Install dependencies 24 | run: npm install 25 | - name: Build 26 | run: node --run build 27 | - name: Benchmark (async) 28 | run: node --run bench:product:async 29 | - name: Benchmark (stream) 30 | run: node --run bench:product:stream 31 | - name: Benchmark (sync) 32 | run: node --run bench:product:sync 33 | 34 | regress: 35 | name: Regress benchmark with options (${{ matrix.benchmark_options }}) 36 | concurrency: 37 | group: ${{ github.workflow }}-benchmark-regress-${{ matrix.benchmark_options }}-${{ github.ref }} 38 | cancel-in-progress: true 39 | runs-on: ubuntu-latest 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | benchmark_options: 44 | - '{}' 45 | - '{ "objectMode": true }' 46 | - '{ "absolute": true }' 47 | env: 48 | BENCHMARK_OPTIONS: ${{ matrix.benchmark_options }} 49 | steps: 50 | - name: Setup repository 51 | uses: actions/checkout@v4 52 | - name: Setup environment 53 | uses: actions/setup-node@v4 54 | with: 55 | node-version: 22 56 | - name: Install dependencies 57 | run: npm install 58 | - name: Build 59 | run: node --run build 60 | - name: Benchmark (async) 61 | run: node --run bench:regression:async 62 | - name: Benchmark (stream) 63 | run: node --run bench:regression:stream 64 | - name: Benchmark (sync) 65 | run: node --run bench:regression:sync 66 | 67 | overhead: 68 | name: Overhead benchmark 69 | concurrency: 70 | group: ${{ github.workflow }}-benchmark-overhead-${{ github.ref }} 71 | cancel-in-progress: true 72 | runs-on: ubuntu-latest 73 | steps: 74 | - name: Setup repository 75 | uses: actions/checkout@v4 76 | - name: Setup environment 77 | uses: actions/setup-node@v4 78 | with: 79 | node-version: 22 80 | - name: Install dependencies 81 | run: npm install 82 | - name: Build 83 | run: node --run build 84 | - name: Benchmark (async) 85 | run: node --run bench:overhead:async 86 | - name: Benchmark (stream) 87 | run: node --run bench:overhead:stream 88 | - name: Benchmark (sync) 89 | run: node --run bench:overhead:sync 90 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: ['master'] 6 | pull_request: 7 | branches: ['master'] 8 | schedule: 9 | - cron: "0 0 * * 0" 10 | workflow_dispatch: 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | concurrency: 16 | group: ${{ github.workflow }}-codeql-${{ github.ref }} 17 | cancel-in-progress: true 18 | runs-on: ubuntu-latest 19 | permissions: 20 | actions: read 21 | contents: read 22 | security-events: write 23 | 24 | steps: 25 | - name: Clone repository 26 | uses: actions/checkout@v4 27 | with: 28 | persist-credentials: false 29 | 30 | - name: Initialize CodeQL 31 | uses: github/codeql-action/init@v3 32 | with: 33 | languages: "javascript" 34 | queries: +security-and-quality 35 | 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v3 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v3 41 | with: 42 | category: "/language:javascript" 43 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ['master', 'releases/*'] 6 | pull_request: 7 | branches: ['*'] 8 | 9 | env: 10 | SNAPSHOT_SKIP_PRUNING: 1 11 | 12 | jobs: 13 | test: 14 | name: Node.js ${{ matrix.node_version }} on ${{ matrix.os }} 15 | concurrency: 16 | group: ${{ github.workflow }}-build-${{ matrix.os }}-${{ matrix.node_version }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | node_version: [18.18.0, 18, 20, 22] 23 | os: 24 | - ubuntu-latest 25 | - macos-latest 26 | - windows-latest 27 | steps: 28 | - name: Setup repository 29 | uses: actions/checkout@v4 30 | - name: Setup environment 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ matrix.node_version }} 34 | - name: Install dependencies 35 | run: npm install 36 | - name: Run Hygiene Checks 37 | run: npm run lint 38 | - name: Compile 39 | run: npm run compile 40 | - name: Run unit tests 41 | run: npm run test 42 | - name: Run e2e tests (sync) 43 | run: npm run test:e2e:sync 44 | - name: Run e2e tests (async) 45 | run: npm run test:e2e:async 46 | - name: Run e2e tests (stream) 47 | run: npm run test:e2e:stream 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs/ 3 | *.log 4 | npm-debug.log* 5 | 6 | # IDE & editors 7 | .idea 8 | .vscode 9 | 10 | # Dependency directory 11 | node_modules/ 12 | 13 | # Compiled and temporary files 14 | .eslintcache 15 | .tmp/ 16 | .benchmark/ 17 | out/ 18 | build/ 19 | 20 | # Other files 21 | package-lock.json 22 | yarn.lock 23 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Denis Malinochkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /__snapshots__/base-name-match.e2e.js: -------------------------------------------------------------------------------- 1 | exports['Options MatchBase {"pattern":"file.md","options":{"cwd":"fixtures","baseNameMatch":true}} (sync) 1'] = [ 2 | "file.md", 3 | "first/file.md", 4 | "first/nested/directory/file.md", 5 | "first/nested/file.md", 6 | "second/file.md", 7 | "second/nested/directory/file.md", 8 | "second/nested/file.md" 9 | ] 10 | 11 | exports['Options MatchBase {"pattern":"file.md","options":{"cwd":"fixtures","baseNameMatch":true}} (async) 1'] = [ 12 | "file.md", 13 | "first/file.md", 14 | "first/nested/directory/file.md", 15 | "first/nested/file.md", 16 | "second/file.md", 17 | "second/nested/directory/file.md", 18 | "second/nested/file.md" 19 | ] 20 | 21 | exports['Options MatchBase {"pattern":"file.md","options":{"cwd":"fixtures","baseNameMatch":true}} (stream) 1'] = [ 22 | "file.md", 23 | "first/file.md", 24 | "first/nested/directory/file.md", 25 | "first/nested/file.md", 26 | "second/file.md", 27 | "second/nested/directory/file.md", 28 | "second/nested/file.md" 29 | ] 30 | 31 | exports['Options MatchBase {"pattern":"first/*/file.md","options":{"cwd":"fixtures","baseNameMatch":true}} (sync) 1'] = [ 32 | "first/nested/file.md" 33 | ] 34 | 35 | exports['Options MatchBase {"pattern":"first/*/file.md","options":{"cwd":"fixtures","baseNameMatch":true}} (async) 1'] = [ 36 | "first/nested/file.md" 37 | ] 38 | 39 | exports['Options MatchBase {"pattern":"first/*/file.md","options":{"cwd":"fixtures","baseNameMatch":true}} (stream) 1'] = [ 40 | "first/nested/file.md" 41 | ] 42 | 43 | exports['Options MatchBase {"pattern":"first/**/file.md","options":{"cwd":"fixtures","baseNameMatch":true}} (sync) 1'] = [ 44 | "first/file.md", 45 | "first/nested/directory/file.md", 46 | "first/nested/file.md" 47 | ] 48 | 49 | exports['Options MatchBase {"pattern":"first/**/file.md","options":{"cwd":"fixtures","baseNameMatch":true}} (async) 1'] = [ 50 | "first/file.md", 51 | "first/nested/directory/file.md", 52 | "first/nested/file.md" 53 | ] 54 | 55 | exports['Options MatchBase {"pattern":"first/**/file.md","options":{"cwd":"fixtures","baseNameMatch":true}} (stream) 1'] = [ 56 | "first/file.md", 57 | "first/nested/directory/file.md", 58 | "first/nested/file.md" 59 | ] 60 | -------------------------------------------------------------------------------- /__snapshots__/case-sensitive-match.e2e.js: -------------------------------------------------------------------------------- 1 | exports['Options CaseSensitiveMatch {"pattern":"fixtures/File.md","options":{"caseSensitiveMatch":false}} (sync) 1'] = [ 2 | "fixtures/file.md" 3 | ] 4 | 5 | exports['Options CaseSensitiveMatch {"pattern":"fixtures/File.md","options":{"caseSensitiveMatch":false}} (async) 1'] = [ 6 | "fixtures/file.md" 7 | ] 8 | 9 | exports['Options CaseSensitiveMatch {"pattern":"fixtures/File.md","options":{"caseSensitiveMatch":false}} (stream) 1'] = [ 10 | "fixtures/file.md" 11 | ] 12 | -------------------------------------------------------------------------------- /__snapshots__/deep.e2e.js: -------------------------------------------------------------------------------- 1 | exports['Options Deep {"pattern":"fixtures/**","options":{"deep":0}} (sync) 1'] = [ 2 | "fixtures/file.md" 3 | ] 4 | 5 | exports['Options Deep {"pattern":"fixtures/**","options":{"deep":0}} (async) 1'] = [ 6 | "fixtures/file.md" 7 | ] 8 | 9 | exports['Options Deep {"pattern":"fixtures/**","options":{"deep":0}} (stream) 1'] = [ 10 | "fixtures/file.md" 11 | ] 12 | 13 | exports['Options Deep {"pattern":"fixtures/**","options":{"deep":2}} (sync) 1'] = [ 14 | "fixtures/file.md", 15 | "fixtures/first/file.md", 16 | "fixtures/second/file.md" 17 | ] 18 | 19 | exports['Options Deep {"pattern":"fixtures/**","options":{"deep":2}} (async) 1'] = [ 20 | "fixtures/file.md", 21 | "fixtures/first/file.md", 22 | "fixtures/second/file.md" 23 | ] 24 | 25 | exports['Options Deep {"pattern":"fixtures/**","options":{"deep":2}} (stream) 1'] = [ 26 | "fixtures/file.md", 27 | "fixtures/first/file.md", 28 | "fixtures/second/file.md" 29 | ] 30 | 31 | exports['Options Deep (cwd) {"pattern":"**","options":{"cwd":"fixtures","deep":0}} (sync) 1'] = [ 32 | "file.md" 33 | ] 34 | 35 | exports['Options Deep (cwd) {"pattern":"**","options":{"cwd":"fixtures","deep":0}} (async) 1'] = [ 36 | "file.md" 37 | ] 38 | 39 | exports['Options Deep (cwd) {"pattern":"**","options":{"cwd":"fixtures","deep":0}} (stream) 1'] = [ 40 | "file.md" 41 | ] 42 | 43 | exports['Options Deep (cwd) {"pattern":"**","options":{"cwd":"fixtures","deep":2}} (sync) 1'] = [ 44 | "file.md", 45 | "first/file.md", 46 | "second/file.md" 47 | ] 48 | 49 | exports['Options Deep (cwd) {"pattern":"**","options":{"cwd":"fixtures","deep":2}} (async) 1'] = [ 50 | "file.md", 51 | "first/file.md", 52 | "second/file.md" 53 | ] 54 | 55 | exports['Options Deep (cwd) {"pattern":"**","options":{"cwd":"fixtures","deep":2}} (stream) 1'] = [ 56 | "file.md", 57 | "first/file.md", 58 | "second/file.md" 59 | ] 60 | -------------------------------------------------------------------------------- /__snapshots__/errors.e2e.js: -------------------------------------------------------------------------------- 1 | exports['Errors {"pattern":"non-exist-directory/**","options":{}} (sync) 1'] = [] 2 | 3 | exports['Errors {"pattern":"non-exist-directory/**","options":{}} (async) 1'] = [] 4 | 5 | exports['Errors {"pattern":"non-exist-directory/**","options":{}} (stream) 1'] = [] 6 | 7 | exports['Errors {"pattern":"non-exist-file.txt","options":{}} (sync) 1'] = [] 8 | 9 | exports['Errors {"pattern":"non-exist-file.txt","options":{}} (async) 1'] = [] 10 | 11 | exports['Errors {"pattern":"non-exist-file.txt","options":{}} (stream) 1'] = [] 12 | 13 | exports['Errors (cwd) {"pattern":"**","options":{"cwd":"non-exist-directory"}} (sync) 1'] = [] 14 | 15 | exports['Errors (cwd) {"pattern":"**","options":{"cwd":"non-exist-directory"}} (async) 1'] = [] 16 | 17 | exports['Errors (cwd) {"pattern":"**","options":{"cwd":"non-exist-directory"}} (stream) 1'] = [] 18 | -------------------------------------------------------------------------------- /__snapshots__/ignore.e2e.js: -------------------------------------------------------------------------------- 1 | exports['Options Ignore {"pattern":"fixtures/**/*","options":{"ignore":["**/*.md"]}} (sync) 1'] = [ 2 | "fixtures/first/nested/directory/file.json" 3 | ] 4 | 5 | exports['Options Ignore {"pattern":"fixtures/**/*","options":{"ignore":["**/*.md"]}} (async) 1'] = [ 6 | "fixtures/first/nested/directory/file.json" 7 | ] 8 | 9 | exports['Options Ignore {"pattern":"fixtures/**/*","options":{"ignore":["**/*.md"]}} (stream) 1'] = [ 10 | "fixtures/first/nested/directory/file.json" 11 | ] 12 | 13 | exports['Options Ignore {"pattern":"fixtures/**/*","options":{"ignore":["!**/*.md"]}} (sync) 1'] = [ 14 | "fixtures/first/nested/directory/file.json" 15 | ] 16 | 17 | exports['Options Ignore {"pattern":"fixtures/**/*","options":{"ignore":["!**/*.md"]}} (async) 1'] = [ 18 | "fixtures/first/nested/directory/file.json" 19 | ] 20 | 21 | exports['Options Ignore {"pattern":"fixtures/**/*","options":{"ignore":["!**/*.md"]}} (stream) 1'] = [ 22 | "fixtures/first/nested/directory/file.json" 23 | ] 24 | -------------------------------------------------------------------------------- /__snapshots__/mark-directories.e2e.js: -------------------------------------------------------------------------------- 1 | exports['Options MarkDirectories {"pattern":"fixtures/**/*","options":{"markDirectories":true}} (sync) 1'] = [ 2 | "fixtures/file.md", 3 | "fixtures/first/file.md", 4 | "fixtures/first/nested/directory/file.json", 5 | "fixtures/first/nested/directory/file.md", 6 | "fixtures/first/nested/file.md", 7 | "fixtures/second/file.md", 8 | "fixtures/second/nested/directory/file.md", 9 | "fixtures/second/nested/file.md", 10 | "fixtures/third/library/a/book.md", 11 | "fixtures/third/library/b/book.md" 12 | ] 13 | 14 | exports['Options MarkDirectories {"pattern":"fixtures/**/*","options":{"markDirectories":true}} (async) 1'] = [ 15 | "fixtures/file.md", 16 | "fixtures/first/file.md", 17 | "fixtures/first/nested/directory/file.json", 18 | "fixtures/first/nested/directory/file.md", 19 | "fixtures/first/nested/file.md", 20 | "fixtures/second/file.md", 21 | "fixtures/second/nested/directory/file.md", 22 | "fixtures/second/nested/file.md", 23 | "fixtures/third/library/a/book.md", 24 | "fixtures/third/library/b/book.md" 25 | ] 26 | 27 | exports['Options MarkDirectories {"pattern":"fixtures/**/*","options":{"markDirectories":true}} (stream) 1'] = [ 28 | "fixtures/file.md", 29 | "fixtures/first/file.md", 30 | "fixtures/first/nested/directory/file.json", 31 | "fixtures/first/nested/directory/file.md", 32 | "fixtures/first/nested/file.md", 33 | "fixtures/second/file.md", 34 | "fixtures/second/nested/directory/file.md", 35 | "fixtures/second/nested/file.md", 36 | "fixtures/third/library/a/book.md", 37 | "fixtures/third/library/b/book.md" 38 | ] 39 | -------------------------------------------------------------------------------- /__snapshots__/only-files.e2e.js: -------------------------------------------------------------------------------- 1 | exports['Options OnlyFiles {"pattern":"fixtures/*","options":{"onlyFiles":true}} (sync) 1'] = [ 2 | "fixtures/file.md" 3 | ] 4 | 5 | exports['Options OnlyFiles {"pattern":"fixtures/*","options":{"onlyFiles":true}} (async) 1'] = [ 6 | "fixtures/file.md" 7 | ] 8 | 9 | exports['Options OnlyFiles {"pattern":"fixtures/*","options":{"onlyFiles":true}} (stream) 1'] = [ 10 | "fixtures/file.md" 11 | ] 12 | 13 | exports['Options OnlyFiles {"pattern":"fixtures/**","options":{"onlyFiles":true}} (sync) 1'] = [ 14 | "fixtures/file.md", 15 | "fixtures/first/file.md", 16 | "fixtures/first/nested/directory/file.json", 17 | "fixtures/first/nested/directory/file.md", 18 | "fixtures/first/nested/file.md", 19 | "fixtures/second/file.md", 20 | "fixtures/second/nested/directory/file.md", 21 | "fixtures/second/nested/file.md", 22 | "fixtures/third/library/a/book.md", 23 | "fixtures/third/library/b/book.md" 24 | ] 25 | 26 | exports['Options OnlyFiles {"pattern":"fixtures/**","options":{"onlyFiles":true}} (async) 1'] = [ 27 | "fixtures/file.md", 28 | "fixtures/first/file.md", 29 | "fixtures/first/nested/directory/file.json", 30 | "fixtures/first/nested/directory/file.md", 31 | "fixtures/first/nested/file.md", 32 | "fixtures/second/file.md", 33 | "fixtures/second/nested/directory/file.md", 34 | "fixtures/second/nested/file.md", 35 | "fixtures/third/library/a/book.md", 36 | "fixtures/third/library/b/book.md" 37 | ] 38 | 39 | exports['Options OnlyFiles {"pattern":"fixtures/**","options":{"onlyFiles":true}} (stream) 1'] = [ 40 | "fixtures/file.md", 41 | "fixtures/first/file.md", 42 | "fixtures/first/nested/directory/file.json", 43 | "fixtures/first/nested/directory/file.md", 44 | "fixtures/first/nested/file.md", 45 | "fixtures/second/file.md", 46 | "fixtures/second/nested/directory/file.md", 47 | "fixtures/second/nested/file.md", 48 | "fixtures/third/library/a/book.md", 49 | "fixtures/third/library/b/book.md" 50 | ] 51 | 52 | exports['Options OnlyFiles {"pattern":"fixtures/**/*","options":{"onlyFiles":true}} (sync) 1'] = [ 53 | "fixtures/file.md", 54 | "fixtures/first/file.md", 55 | "fixtures/first/nested/directory/file.json", 56 | "fixtures/first/nested/directory/file.md", 57 | "fixtures/first/nested/file.md", 58 | "fixtures/second/file.md", 59 | "fixtures/second/nested/directory/file.md", 60 | "fixtures/second/nested/file.md", 61 | "fixtures/third/library/a/book.md", 62 | "fixtures/third/library/b/book.md" 63 | ] 64 | 65 | exports['Options OnlyFiles {"pattern":"fixtures/**/*","options":{"onlyFiles":true}} (async) 1'] = [ 66 | "fixtures/file.md", 67 | "fixtures/first/file.md", 68 | "fixtures/first/nested/directory/file.json", 69 | "fixtures/first/nested/directory/file.md", 70 | "fixtures/first/nested/file.md", 71 | "fixtures/second/file.md", 72 | "fixtures/second/nested/directory/file.md", 73 | "fixtures/second/nested/file.md", 74 | "fixtures/third/library/a/book.md", 75 | "fixtures/third/library/b/book.md" 76 | ] 77 | 78 | exports['Options OnlyFiles {"pattern":"fixtures/**/*","options":{"onlyFiles":true}} (stream) 1'] = [ 79 | "fixtures/file.md", 80 | "fixtures/first/file.md", 81 | "fixtures/first/nested/directory/file.json", 82 | "fixtures/first/nested/directory/file.md", 83 | "fixtures/first/nested/file.md", 84 | "fixtures/second/file.md", 85 | "fixtures/second/nested/directory/file.md", 86 | "fixtures/second/nested/file.md", 87 | "fixtures/third/library/a/book.md", 88 | "fixtures/third/library/b/book.md" 89 | ] 90 | 91 | exports['Options Files (cwd) {"pattern":"*","options":{"cwd":"fixtures","onlyFiles":true}} (sync) 1'] = [ 92 | "file.md" 93 | ] 94 | 95 | exports['Options Files (cwd) {"pattern":"*","options":{"cwd":"fixtures","onlyFiles":true}} (async) 1'] = [ 96 | "file.md" 97 | ] 98 | 99 | exports['Options Files (cwd) {"pattern":"*","options":{"cwd":"fixtures","onlyFiles":true}} (stream) 1'] = [ 100 | "file.md" 101 | ] 102 | 103 | exports['Options Files (cwd) {"pattern":"**","options":{"cwd":"fixtures","onlyFiles":true}} (sync) 1'] = [ 104 | "file.md", 105 | "first/file.md", 106 | "first/nested/directory/file.json", 107 | "first/nested/directory/file.md", 108 | "first/nested/file.md", 109 | "second/file.md", 110 | "second/nested/directory/file.md", 111 | "second/nested/file.md", 112 | "third/library/a/book.md", 113 | "third/library/b/book.md" 114 | ] 115 | 116 | exports['Options Files (cwd) {"pattern":"**","options":{"cwd":"fixtures","onlyFiles":true}} (async) 1'] = [ 117 | "file.md", 118 | "first/file.md", 119 | "first/nested/directory/file.json", 120 | "first/nested/directory/file.md", 121 | "first/nested/file.md", 122 | "second/file.md", 123 | "second/nested/directory/file.md", 124 | "second/nested/file.md", 125 | "third/library/a/book.md", 126 | "third/library/b/book.md" 127 | ] 128 | 129 | exports['Options Files (cwd) {"pattern":"**","options":{"cwd":"fixtures","onlyFiles":true}} (stream) 1'] = [ 130 | "file.md", 131 | "first/file.md", 132 | "first/nested/directory/file.json", 133 | "first/nested/directory/file.md", 134 | "first/nested/file.md", 135 | "second/file.md", 136 | "second/nested/directory/file.md", 137 | "second/nested/file.md", 138 | "third/library/a/book.md", 139 | "third/library/b/book.md" 140 | ] 141 | 142 | exports['Options Files (cwd) {"pattern":"**/*","options":{"cwd":"fixtures","onlyFiles":true}} (sync) 1'] = [ 143 | "file.md", 144 | "first/file.md", 145 | "first/nested/directory/file.json", 146 | "first/nested/directory/file.md", 147 | "first/nested/file.md", 148 | "second/file.md", 149 | "second/nested/directory/file.md", 150 | "second/nested/file.md", 151 | "third/library/a/book.md", 152 | "third/library/b/book.md" 153 | ] 154 | 155 | exports['Options Files (cwd) {"pattern":"**/*","options":{"cwd":"fixtures","onlyFiles":true}} (async) 1'] = [ 156 | "file.md", 157 | "first/file.md", 158 | "first/nested/directory/file.json", 159 | "first/nested/directory/file.md", 160 | "first/nested/file.md", 161 | "second/file.md", 162 | "second/nested/directory/file.md", 163 | "second/nested/file.md", 164 | "third/library/a/book.md", 165 | "third/library/b/book.md" 166 | ] 167 | 168 | exports['Options Files (cwd) {"pattern":"**/*","options":{"cwd":"fixtures","onlyFiles":true}} (stream) 1'] = [ 169 | "file.md", 170 | "first/file.md", 171 | "first/nested/directory/file.json", 172 | "first/nested/directory/file.md", 173 | "first/nested/file.md", 174 | "second/file.md", 175 | "second/nested/directory/file.md", 176 | "second/nested/file.md", 177 | "third/library/a/book.md", 178 | "third/library/b/book.md" 179 | ] 180 | -------------------------------------------------------------------------------- /__snapshots__/root.e2e.js: -------------------------------------------------------------------------------- 1 | exports['Patterns Root {"pattern":"/*","options":{}} (sync) 1'] = [] 2 | 3 | exports['Patterns Root {"pattern":"/*","options":{}} (async) 1'] = [] 4 | 5 | exports['Patterns Root {"pattern":"/*","options":{}} (stream) 1'] = [] 6 | 7 | exports['Patterns Root {"pattern":"/tmp/*","options":{}} (sync) 1'] = [] 8 | 9 | exports['Patterns Root {"pattern":"/tmp/*","options":{}} (async) 1'] = [] 10 | 11 | exports['Patterns Root {"pattern":"/tmp/*","options":{}} (stream) 1'] = [] 12 | 13 | exports['Patterns Root (cwd) {"pattern":"*","options":{"cwd":"/"}} (sync) 1'] = [] 14 | 15 | exports['Patterns Root (cwd) {"pattern":"*","options":{"cwd":"/"}} (async) 1'] = [] 16 | 17 | exports['Patterns Root (cwd) {"pattern":"*","options":{"cwd":"/"}} (stream) 1'] = [] 18 | -------------------------------------------------------------------------------- /__snapshots__/unique.e2e.js: -------------------------------------------------------------------------------- 1 | exports['Options Unique {"pattern":["./file.md","file.md","*"],"options":{"cwd":"fixtures","unique":false}} (sync) 1'] = [ 2 | "./file.md", 3 | "file.md", 4 | "file.md" 5 | ] 6 | 7 | exports['Options Unique {"pattern":["./file.md","file.md","*"],"options":{"cwd":"fixtures","unique":false}} (async) 1'] = [ 8 | "./file.md", 9 | "file.md", 10 | "file.md" 11 | ] 12 | 13 | exports['Options Unique {"pattern":["./file.md","file.md","*"],"options":{"cwd":"fixtures","unique":false}} (stream) 1'] = [ 14 | "./file.md", 15 | "file.md", 16 | "file.md" 17 | ] 18 | 19 | exports['Options Unique {"pattern":["./file.md","file.md","*"],"options":{"cwd":"fixtures","unique":true}} (sync) 1'] = [ 20 | "file.md" 21 | ] 22 | 23 | exports['Options Unique {"pattern":["./file.md","file.md","*"],"options":{"cwd":"fixtures","unique":true}} (async) 1'] = [ 24 | "file.md" 25 | ] 26 | 27 | exports['Options Unique {"pattern":["./file.md","file.md","*"],"options":{"cwd":"fixtures","unique":true}} (stream) 1'] = [ 28 | "file.md" 29 | ] 30 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import cfg from 'eslint-config-mrmlnc'; 2 | 3 | /** @type {import('eslint').Linter.Config[]} */ 4 | const overrides = [ 5 | ...cfg.build({}), 6 | ]; 7 | 8 | export default overrides; 9 | -------------------------------------------------------------------------------- /fixtures/.directory/file.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmlnc/fast-glob/096a5b620f4224eb692bd8ff2c8de0e634e50d8e/fixtures/.directory/file.md -------------------------------------------------------------------------------- /fixtures/.file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmlnc/fast-glob/096a5b620f4224eb692bd8ff2c8de0e634e50d8e/fixtures/.file -------------------------------------------------------------------------------- /fixtures/file.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmlnc/fast-glob/096a5b620f4224eb692bd8ff2c8de0e634e50d8e/fixtures/file.md -------------------------------------------------------------------------------- /fixtures/first/file.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmlnc/fast-glob/096a5b620f4224eb692bd8ff2c8de0e634e50d8e/fixtures/first/file.md -------------------------------------------------------------------------------- /fixtures/first/nested/directory/file.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmlnc/fast-glob/096a5b620f4224eb692bd8ff2c8de0e634e50d8e/fixtures/first/nested/directory/file.json -------------------------------------------------------------------------------- /fixtures/first/nested/directory/file.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmlnc/fast-glob/096a5b620f4224eb692bd8ff2c8de0e634e50d8e/fixtures/first/nested/directory/file.md -------------------------------------------------------------------------------- /fixtures/first/nested/file.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmlnc/fast-glob/096a5b620f4224eb692bd8ff2c8de0e634e50d8e/fixtures/first/nested/file.md -------------------------------------------------------------------------------- /fixtures/second/file.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmlnc/fast-glob/096a5b620f4224eb692bd8ff2c8de0e634e50d8e/fixtures/second/file.md -------------------------------------------------------------------------------- /fixtures/second/nested/directory/file.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmlnc/fast-glob/096a5b620f4224eb692bd8ff2c8de0e634e50d8e/fixtures/second/nested/directory/file.md -------------------------------------------------------------------------------- /fixtures/second/nested/file.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmlnc/fast-glob/096a5b620f4224eb692bd8ff2c8de0e634e50d8e/fixtures/second/nested/file.md -------------------------------------------------------------------------------- /fixtures/third/library/a/book.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmlnc/fast-glob/096a5b620f4224eb692bd8ff2c8de0e634e50d8e/fixtures/third/library/a/book.md -------------------------------------------------------------------------------- /fixtures/third/library/b/book.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmlnc/fast-glob/096a5b620f4224eb692bd8ff2c8de0e634e50d8e/fixtures/third/library/b/book.md -------------------------------------------------------------------------------- /herebyfile.mjs: -------------------------------------------------------------------------------- 1 | import { execa } from 'execa'; 2 | import { task } from 'hereby'; 3 | 4 | const CONCURRENCY = process.env.CONCURRENCY === '1'; 5 | const REPORTER = process.env.REPORTER ?? 'compact'; 6 | const WARMUP_COUNT = process.env.WARMUP_COUNT ?? 50; 7 | const RUNS_COUNT = process.env.RUNS_COUNT ?? 150; 8 | 9 | const PRODUCT_ASYNC_SUITE = './out/benchmark/suites/product/async.js'; 10 | const PRODUCT_SYNC_SUITE = './out/benchmark/suites/product/sync.js'; 11 | const PRODUCT_STREAM_SUITE = './out/benchmark/suites/product/stream.js'; 12 | 13 | const REGRESSION_ASYNC_SUITE = './out/benchmark/suites/regression/async.js'; 14 | const REGRESSION_SYNC_SUITE = './out/benchmark/suites/regression/sync.js'; 15 | const REGRESSION_STREAM_SUITE = './out/benchmark/suites/regression/stream.js'; 16 | 17 | const OVERHEAD_ASYNC_SUITE = './out/benchmark/suites/overhead/async.js'; 18 | const OVERHEAD_SYNC_SUITE = './out/benchmark/suites/overhead/sync.js'; 19 | const OVERHEAD_STREAM_SUITE = './out/benchmark/suites/overhead/stream.js'; 20 | 21 | const FLATTEN_PATTERN = '*'; 22 | const DEEP_PATTERN = '**'; 23 | const PARTIAL_FLATTEN_PATTERN = '{fixtures,out}/{first,second}/*'; 24 | const PARTIAL_DEEP_PATTERN = '{fixtures,out}/**'; 25 | 26 | async function benchTask(suite, label, pattern, implementations = []) { 27 | await execa('bencho', [ 28 | `'node ${suite} . "${pattern}" {impl}'`, 29 | `-n "${label} {impl} ${pattern}"`, 30 | `-w ${WARMUP_COUNT}`, 31 | `-r ${RUNS_COUNT}`, 32 | `-l impl=${implementations.join(',')}`, 33 | `--reporter=${REPORTER}`, 34 | ], { 35 | shell: true, 36 | stdout: 'inherit', 37 | }); 38 | } 39 | 40 | function makeBenchSuiteTask(type, label, suite, implementations = [], includePartialTasks = true) { 41 | const asyncFlattenTask = task({ 42 | name: `bench:${type}:${label}:flatten`, 43 | run: () => benchTask(suite, label, FLATTEN_PATTERN, implementations), 44 | }); 45 | 46 | const asyncDeepTask = task({ 47 | name: `bench:${type}:${label}:deep`, 48 | dependencies: CONCURRENCY ? [] : [asyncFlattenTask], 49 | run: () => benchTask(suite, label, DEEP_PATTERN, implementations), 50 | }); 51 | 52 | const asyncPartialFlattenTask = includePartialTasks && task({ 53 | name: `bench:${type}:${label}:partial_flatten`, 54 | dependencies: CONCURRENCY ? [] : [asyncDeepTask], 55 | run: () => benchTask(suite, label, PARTIAL_FLATTEN_PATTERN, implementations), 56 | }); 57 | 58 | const asyncPartialDeepTask = includePartialTasks && task({ 59 | name: `bench:${type}:${label}:partial_deep`, 60 | dependencies: CONCURRENCY ? [] : [asyncPartialFlattenTask], 61 | run: () => benchTask(suite, label, PARTIAL_DEEP_PATTERN, implementations), 62 | }); 63 | 64 | return task({ 65 | name: `bench:${type}:${label}`, 66 | dependencies: CONCURRENCY ? [] : [includePartialTasks ? asyncPartialDeepTask : asyncDeepTask], 67 | run: () => {}, 68 | }); 69 | } 70 | 71 | export const { 72 | productAsyncTask, 73 | productStreamTask, 74 | productSyncTask, 75 | } = { 76 | productAsyncTask: makeBenchSuiteTask('product', 'async', PRODUCT_ASYNC_SUITE, ['fast-glob', 'node-glob', 'tinyglobby']), 77 | productStreamTask: makeBenchSuiteTask('product', 'stream', PRODUCT_STREAM_SUITE, ['fast-glob', 'node-glob']), 78 | productSyncTask: makeBenchSuiteTask('product', 'sync', PRODUCT_SYNC_SUITE, ['fast-glob', 'node-glob', 'tinyglobby']), 79 | }; 80 | 81 | export const { 82 | regressionAsyncTask, 83 | regressionStreamTask, 84 | regressionSyncTask, 85 | } = { 86 | regressionAsyncTask: makeBenchSuiteTask('regression', 'async', REGRESSION_ASYNC_SUITE, ['current', 'previous']), 87 | regressionStreamTask: makeBenchSuiteTask('regression', 'stream', REGRESSION_STREAM_SUITE, ['current', 'previous']), 88 | regressionSyncTask: makeBenchSuiteTask('regression', 'sync', REGRESSION_SYNC_SUITE, ['current', 'previous']), 89 | }; 90 | 91 | export const { 92 | overheadAsyncTask, 93 | overheadSyncTask, 94 | overStreamTask, 95 | } = { 96 | overheadAsyncTask: makeBenchSuiteTask('overhead', 'async', OVERHEAD_ASYNC_SUITE, ['fast-glob', 'fs-walk'], false), 97 | overheadSyncTask: makeBenchSuiteTask('overhead', 'sync', OVERHEAD_SYNC_SUITE, ['fast-glob', 'fs-walk'], false), 98 | overStreamTask: makeBenchSuiteTask('overhead', 'stream', OVERHEAD_STREAM_SUITE, ['fast-glob', 'fs-walk'], false), 99 | }; 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-glob", 3 | "version": "4.0.0", 4 | "description": "It's a very fast and efficient glob library for Node.js", 5 | "license": "MIT", 6 | "repository": "mrmlnc/fast-glob", 7 | "author": { 8 | "name": "Denis Malinochkin", 9 | "url": "https://mrmlnc.com" 10 | }, 11 | "engines": { 12 | "node": ">=18.18.0" 13 | }, 14 | "main": "out/index.js", 15 | "typings": "out/index.d.ts", 16 | "files": [ 17 | "out", 18 | "!out/{benchmark,tests}", 19 | "!out/**/*.map", 20 | "!out/**/*.spec.*" 21 | ], 22 | "keywords": [ 23 | "glob", 24 | "patterns", 25 | "fast", 26 | "implementation" 27 | ], 28 | "devDependencies": { 29 | "@nodelib/fs.macchiato": "^3.0.0", 30 | "@types/glob-parent": "^5.1.3", 31 | "@types/merge2": "^1.4.4", 32 | "@types/micromatch": "^4.0.9", 33 | "@types/mocha": "^10.0.10", 34 | "@types/node": "^18.19.67", 35 | "@types/picomatch": "^3.0.1", 36 | "@types/sinon": "^17.0.3", 37 | "bencho": "^0.1.1", 38 | "eslint": "9.14.0", 39 | "eslint-config-mrmlnc": "^5.0.0", 40 | "execa": "^7.2.0", 41 | "fast-glob": "^3.3.2", 42 | "glob": "^11.0.0", 43 | "hereby": "^1.10.0", 44 | "mocha": "^10.8.2", 45 | "rimraf": "^6.0.1", 46 | "sinon": "^19.0.2", 47 | "snap-shot-it": "^7.9.10", 48 | "tinyglobby": "^0.2.10", 49 | "typescript": "^5.7.2" 50 | }, 51 | "dependencies": { 52 | "@nodelib/fs.stat": "^4.0.0", 53 | "@nodelib/fs.walk": "^3.0.0", 54 | "glob-parent": "^6.0.2", 55 | "merge2": "^1.4.1", 56 | "micromatch": "^4.0.8" 57 | }, 58 | "scripts": { 59 | "clean": "rimraf out", 60 | "lint": "eslint \"src/**/*.ts\" --cache", 61 | "compile": "tsc", 62 | "test": "mocha \"out/**/*.spec.js\" -s 0", 63 | "test:e2e": "mocha \"out/**/*.e2e.js\" -s 0", 64 | "test:e2e:sync": "mocha \"out/**/*.e2e.js\" -s 0 --grep \"\\(sync\\)\"", 65 | "test:e2e:async": "mocha \"out/**/*.e2e.js\" -s 0 --grep \"\\(async\\)\"", 66 | "test:e2e:stream": "mocha \"out/**/*.e2e.js\" -s 0 --grep \"\\(stream\\)\"", 67 | "_build:compile": "npm run clean && npm run compile", 68 | "build": "npm run _build:compile && npm run lint && npm test", 69 | "watch": "npm run _build:compile -- -- --sourceMap --watch", 70 | "bench:async": "npm run bench:product:async && npm run bench:regression:async", 71 | "bench:stream": "npm run bench:product:stream && npm run bench:regression:stream", 72 | "bench:sync": "npm run bench:product:sync && npm run bench:regression:sync", 73 | "bench:product": "npm run bench:product:async && npm run bench:product:sync && npm run bench:product:stream", 74 | "bench:product:async": "hereby bench:product:async", 75 | "bench:product:sync": "hereby bench:product:sync", 76 | "bench:product:stream": "hereby bench:product:stream", 77 | "bench:regression": "npm run bench:regression:async && npm run bench:regression:sync && npm run bench:regression:stream", 78 | "bench:regression:async": "hereby bench:regression:async", 79 | "bench:regression:sync": "hereby bench:regression:sync", 80 | "bench:regression:stream": "hereby bench:regression:stream", 81 | "bench:overhead": "npm run bench:overhead:async && npm run bench:overhead:sync && npm run bench:overhead:stream", 82 | "bench:overhead:async": "hereby bench:overhead:async", 83 | "bench:overhead:sync": "hereby bench:overhead:sync", 84 | "bench:overhead:stream": "hereby bench:overhead:stream", 85 | "prepublishOnly": "npm run build" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/benchmark/suites/overhead/async.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import * as util from 'node:util'; 3 | 4 | import * as bencho from 'bencho'; 5 | 6 | import * as utils from '../../utils'; 7 | 8 | type MeasurableImplementation = 'fast-glob' | 'fs-walk'; 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | type ImplementationFunction = (...args: any[]) => Promise; 11 | 12 | class Glob { 13 | readonly #cwd: string; 14 | readonly #pattern: string; 15 | 16 | constructor(cwd: string, pattern: string) { 17 | this.#cwd = cwd; 18 | this.#pattern = pattern; 19 | } 20 | 21 | public async measureFastGlob(): Promise { 22 | const glob = await utils.importAndMeasure(utils.importCurrentFastGlob); 23 | 24 | await this.#measure(() => glob.glob(this.#pattern, { 25 | cwd: this.#cwd, 26 | unique: false, 27 | onlyFiles: false, 28 | followSymbolicLinks: false, 29 | })); 30 | } 31 | 32 | public async measureFsWalk(): Promise { 33 | const fsWalk = await utils.importAndMeasure(() => import('@nodelib/fs.walk')); 34 | 35 | const walk = util.promisify(fsWalk.walk); 36 | 37 | const settings = new fsWalk.Settings({ 38 | deepFilter: (entry) => this.#pattern !== '*' && !entry.name.startsWith('.'), 39 | entryFilter: (entry) => !entry.name.startsWith('.'), 40 | }); 41 | 42 | await this.#measure(() => walk(this.#cwd, settings)); 43 | } 44 | 45 | async #measure(function_: ImplementationFunction): Promise { 46 | const timeStart = utils.timeStart(); 47 | 48 | const matches = await function_(); 49 | 50 | const count = matches.length; 51 | const memory = utils.getMemory(); 52 | const time = utils.timeEnd(timeStart); 53 | 54 | bencho.time('time', time); 55 | bencho.memory('memory', memory); 56 | bencho.value('entries', count); 57 | } 58 | } 59 | 60 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 61 | (async () => { 62 | const args = process.argv.slice(2); 63 | 64 | const cwd = path.join(process.cwd(), args[0]); 65 | const pattern = args[1]; 66 | const impl = args[2] as MeasurableImplementation; 67 | 68 | if (!['*', '**'].includes(pattern)) { 69 | throw new TypeError('Unknown pattern.'); 70 | } 71 | 72 | const glob = new Glob(cwd, pattern); 73 | 74 | switch (impl) { 75 | case 'fast-glob': { 76 | await glob.measureFastGlob(); 77 | break; 78 | } 79 | 80 | case 'fs-walk': { 81 | await glob.measureFsWalk(); 82 | break; 83 | } 84 | 85 | default: { 86 | throw new TypeError('Unknown implementation.'); 87 | } 88 | } 89 | })(); 90 | -------------------------------------------------------------------------------- /src/benchmark/suites/overhead/stream.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | import * as bencho from 'bencho'; 4 | 5 | import * as utils from '../../utils'; 6 | 7 | import type { Entry } from '@nodelib/fs.walk'; 8 | 9 | type MeasurableImplementation = 'fast-glob' | 'fs-walk'; 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | type ImplementationFunction = (...args: any[]) => Promise; 12 | 13 | class Glob { 14 | readonly #cwd: string; 15 | readonly #pattern: string; 16 | 17 | constructor(cwd: string, pattern: string) { 18 | this.#cwd = cwd; 19 | this.#pattern = pattern; 20 | } 21 | 22 | public async measureFastGlob(): Promise { 23 | const glob = await utils.importAndMeasure(utils.importCurrentFastGlob); 24 | 25 | const entries: string[] = []; 26 | 27 | const stream = glob.globStream(this.#pattern, { 28 | cwd: this.#cwd, 29 | unique: false, 30 | onlyFiles: false, 31 | followSymbolicLinks: false, 32 | }); 33 | 34 | const action = new Promise((resolve, reject) => { 35 | stream.once('error', (error: Error) => { 36 | reject(error); 37 | }); 38 | stream.on('data', (entry: string) => entries.push(entry)); 39 | stream.once('end', () => { 40 | resolve(entries); 41 | }); 42 | }); 43 | 44 | await this.#measure(() => action); 45 | } 46 | 47 | public async measureFsWalk(): Promise { 48 | const fsWalk = await utils.importAndMeasure(() => import('@nodelib/fs.walk')); 49 | 50 | const settings = new fsWalk.Settings({ 51 | deepFilter: (entry) => this.#pattern !== '*' && !entry.name.startsWith('.'), 52 | entryFilter: (entry) => !entry.name.startsWith('.'), 53 | }); 54 | 55 | const entries: Entry[] = []; 56 | 57 | const stream = fsWalk.walkStream(this.#cwd, settings); 58 | 59 | const action = new Promise((resolve, reject) => { 60 | stream.once('error', (error) => { 61 | reject(error); 62 | }); 63 | stream.on('data', (entry: Entry) => entries.push(entry)); 64 | stream.once('end', () => { 65 | resolve(entries); 66 | }); 67 | }); 68 | 69 | await this.#measure(() => action); 70 | } 71 | 72 | async #measure(function_: ImplementationFunction): Promise { 73 | const timeStart = utils.timeStart(); 74 | 75 | const matches = await function_(); 76 | 77 | const count = matches.length; 78 | const memory = utils.getMemory(); 79 | const time = utils.timeEnd(timeStart); 80 | 81 | bencho.time('time', time); 82 | bencho.memory('memory', memory); 83 | bencho.value('entries', count); 84 | } 85 | } 86 | 87 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 88 | (async () => { 89 | const args = process.argv.slice(2); 90 | 91 | const cwd = path.join(process.cwd(), args[0]); 92 | const pattern = args[1]; 93 | const impl = args[2] as MeasurableImplementation; 94 | 95 | if (!['*', '**'].includes(pattern)) { 96 | throw new TypeError('Unknown pattern.'); 97 | } 98 | 99 | const glob = new Glob(cwd, pattern); 100 | 101 | switch (impl) { 102 | case 'fast-glob': { 103 | await glob.measureFastGlob(); 104 | break; 105 | } 106 | 107 | case 'fs-walk': { 108 | await glob.measureFsWalk(); 109 | break; 110 | } 111 | 112 | default: { 113 | throw new TypeError('Unknown implementation.'); 114 | } 115 | } 116 | })(); 117 | -------------------------------------------------------------------------------- /src/benchmark/suites/overhead/sync.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | import * as bencho from 'bencho'; 4 | 5 | import * as utils from '../../utils'; 6 | 7 | type MeasurableImplementation = 'fast-glob' | 'fs-walk'; 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | type ImplementationFunction = (...args: any[]) => unknown[]; 10 | 11 | class Glob { 12 | readonly #cwd: string; 13 | readonly #pattern: string; 14 | 15 | constructor(cwd: string, pattern: string) { 16 | this.#cwd = cwd; 17 | this.#pattern = pattern; 18 | } 19 | 20 | public async measureFastGlob(): Promise { 21 | const glob = await utils.importAndMeasure(utils.importCurrentFastGlob); 22 | 23 | this.#measure(() => glob.globSync(this.#pattern, { 24 | cwd: this.#cwd, 25 | unique: false, 26 | onlyFiles: false, 27 | followSymbolicLinks: false, 28 | })); 29 | } 30 | 31 | public async measureFsWalk(): Promise { 32 | const fsWalk = await utils.importAndMeasure(() => import('@nodelib/fs.walk')); 33 | 34 | const settings = new fsWalk.Settings({ 35 | deepFilter: (entry) => this.#pattern !== '*' && !entry.name.startsWith('.'), 36 | entryFilter: (entry) => !entry.name.startsWith('.'), 37 | }); 38 | 39 | this.#measure(() => fsWalk.walkSync(this.#cwd, settings)); 40 | } 41 | 42 | #measure(function_: ImplementationFunction): void { 43 | const timeStart = utils.timeStart(); 44 | 45 | const matches = function_(); 46 | 47 | const count = matches.length; 48 | const memory = utils.getMemory(); 49 | const time = utils.timeEnd(timeStart); 50 | 51 | bencho.time('time', time); 52 | bencho.memory('memory', memory); 53 | bencho.value('entries', count); 54 | } 55 | } 56 | 57 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 58 | (async () => { 59 | const args = process.argv.slice(2); 60 | 61 | const cwd = path.join(process.cwd(), args[0]); 62 | const pattern = args[1]; 63 | const impl = args[2] as MeasurableImplementation; 64 | 65 | if (!['*', '**'].includes(pattern)) { 66 | throw new TypeError('Unknown pattern.'); 67 | } 68 | 69 | const glob = new Glob(cwd, pattern); 70 | 71 | switch (impl) { 72 | case 'fast-glob': { 73 | await glob.measureFastGlob(); 74 | break; 75 | } 76 | 77 | case 'fs-walk': { 78 | await glob.measureFsWalk(); 79 | break; 80 | } 81 | 82 | default: { 83 | throw new TypeError('Unknown implementation.'); 84 | } 85 | } 86 | })(); 87 | -------------------------------------------------------------------------------- /src/benchmark/suites/product/async.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | import * as bencho from 'bencho'; 4 | 5 | import * as utils from '../../utils'; 6 | 7 | type GlobImplementation = 'fast-glob' | 'node-glob' | 'tinyglobby'; 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | type GlobImplFunction = (...args: any[]) => Promise; 10 | 11 | class Glob { 12 | readonly #cwd: string; 13 | readonly #pattern: string; 14 | 15 | constructor(cwd: string, pattern: string) { 16 | this.#cwd = cwd; 17 | this.#pattern = pattern; 18 | } 19 | 20 | public async measureNodeGlob(): Promise { 21 | const glob = await utils.importAndMeasure(utils.importNodeGlob); 22 | 23 | await this.#measure(() => glob.glob(this.#pattern, { 24 | cwd: this.#cwd, 25 | nodir: true, 26 | })); 27 | } 28 | 29 | public async measureFastGlob(): Promise { 30 | const glob = await utils.importAndMeasure(utils.importCurrentFastGlob); 31 | 32 | await this.#measure(() => glob.glob(this.#pattern, { 33 | cwd: this.#cwd, 34 | unique: false, 35 | followSymbolicLinks: false, 36 | })); 37 | } 38 | 39 | public async measureTinyGlobby(): Promise { 40 | const tinyglobby = await utils.importAndMeasure(utils.importTinyGlobby); 41 | 42 | await this.#measure(() => tinyglobby.glob(this.#pattern, { 43 | cwd: this.#cwd, 44 | followSymbolicLinks: false, 45 | })); 46 | } 47 | 48 | async #measure(function_: GlobImplFunction): Promise { 49 | const timeStart = utils.timeStart(); 50 | 51 | const matches = await function_(); 52 | 53 | const count = matches.length; 54 | const memory = utils.getMemory(); 55 | const time = utils.timeEnd(timeStart); 56 | 57 | bencho.time('time', time); 58 | bencho.memory('memory', memory); 59 | bencho.value('entries', count); 60 | } 61 | } 62 | 63 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 64 | (async () => { 65 | const args = process.argv.slice(2); 66 | 67 | const cwd = path.join(process.cwd(), args[0]); 68 | const pattern = args[1]; 69 | const impl = args[2] as GlobImplementation; 70 | 71 | const glob = new Glob(cwd, pattern); 72 | 73 | switch (impl) { 74 | case 'node-glob': { 75 | await glob.measureNodeGlob(); 76 | break; 77 | } 78 | 79 | case 'fast-glob': { 80 | await glob.measureFastGlob(); 81 | break; 82 | } 83 | 84 | case 'tinyglobby': { 85 | await glob.measureTinyGlobby(); 86 | break; 87 | } 88 | 89 | default: { 90 | throw new TypeError('Unknown glob implementation.'); 91 | } 92 | } 93 | })(); 94 | -------------------------------------------------------------------------------- /src/benchmark/suites/product/stream.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | import * as bencho from 'bencho'; 4 | 5 | import * as utils from '../../utils'; 6 | 7 | type GlobImplementation = 'fast-glob' | 'node-glob'; 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | type GlobImplFunction = (...args: any[]) => Promise; 10 | 11 | class Glob { 12 | readonly #cwd: string; 13 | readonly #pattern: string; 14 | 15 | constructor(cwd: string, pattern: string) { 16 | this.#cwd = cwd; 17 | this.#pattern = pattern; 18 | } 19 | 20 | public async measureNodeGlob(): Promise { 21 | const glob = await utils.importAndMeasure(utils.importNodeGlob); 22 | 23 | const entries: string[] = []; 24 | 25 | const stream = glob.globStream(this.#pattern, { 26 | cwd: this.#cwd, 27 | nodir: true, 28 | }); 29 | 30 | const action = new Promise((resolve, reject) => { 31 | stream.on('error', (error) => { 32 | reject(error as Error); 33 | }); 34 | stream.on('data', (entry: string) => entries.push(entry)); 35 | stream.on('end', () => { 36 | resolve(entries); 37 | }); 38 | }); 39 | 40 | await this.#measure(() => action); 41 | } 42 | 43 | public async measureFastGlob(): Promise { 44 | const glob = await utils.importAndMeasure(utils.importCurrentFastGlob); 45 | 46 | const entries: string[] = []; 47 | 48 | const stream = glob.globStream(this.#pattern, { 49 | cwd: this.#cwd, 50 | unique: false, 51 | followSymbolicLinks: false, 52 | }); 53 | 54 | const action = new Promise((resolve, reject) => { 55 | stream.once('error', (error: Error) => { 56 | reject(error); 57 | }); 58 | stream.on('data', (entry: string) => entries.push(entry)); 59 | stream.once('end', () => { 60 | resolve(entries); 61 | }); 62 | }); 63 | 64 | await this.#measure(() => action); 65 | } 66 | 67 | async #measure(function_: GlobImplFunction): Promise { 68 | const timeStart = utils.timeStart(); 69 | 70 | const matches = await function_(); 71 | 72 | const count = matches.length; 73 | const memory = utils.getMemory(); 74 | const time = utils.timeEnd(timeStart); 75 | 76 | bencho.time('time', time); 77 | bencho.memory('memory', memory); 78 | bencho.value('entries', count); 79 | } 80 | } 81 | 82 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 83 | (async () => { 84 | const args = process.argv.slice(2); 85 | 86 | const cwd = path.join(process.cwd(), args[0]); 87 | const pattern = args[1]; 88 | const impl = args[2] as GlobImplementation; 89 | 90 | const glob = new Glob(cwd, pattern); 91 | 92 | switch (impl) { 93 | case 'node-glob': { 94 | await glob.measureNodeGlob(); 95 | break; 96 | } 97 | 98 | case 'fast-glob': { 99 | await glob.measureFastGlob(); 100 | break; 101 | } 102 | 103 | default: { 104 | throw new TypeError('Unknown glob implementation.'); 105 | } 106 | } 107 | })(); 108 | -------------------------------------------------------------------------------- /src/benchmark/suites/product/sync.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | import * as bencho from 'bencho'; 4 | 5 | import * as utils from '../../utils'; 6 | 7 | type GlobImplementation = 'fast-glob' | 'node-glob' | 'tinyglobby'; 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | type GlobImplFunction = (...args: any[]) => unknown[]; 10 | 11 | class Glob { 12 | readonly #cwd: string; 13 | readonly #pattern: string; 14 | 15 | constructor(cwd: string, pattern: string) { 16 | this.#cwd = cwd; 17 | this.#pattern = pattern; 18 | } 19 | 20 | public async measureNodeGlob(): Promise { 21 | const glob = await utils.importAndMeasure(utils.importNodeGlob); 22 | 23 | this.#measure(() => glob.globSync(this.#pattern, { 24 | cwd: this.#cwd, 25 | nodir: true, 26 | })); 27 | } 28 | 29 | public async measureFastGlob(): Promise { 30 | const glob = await utils.importAndMeasure(utils.importCurrentFastGlob); 31 | 32 | this.#measure(() => glob.globSync(this.#pattern, { 33 | cwd: this.#cwd, 34 | unique: false, 35 | followSymbolicLinks: false, 36 | })); 37 | } 38 | 39 | public async measureTinyGlobby(): Promise { 40 | const tinyglobby = await utils.importAndMeasure(utils.importTinyGlobby); 41 | 42 | this.#measure(() => tinyglobby.globSync(this.#pattern, { 43 | cwd: this.#cwd, 44 | followSymbolicLinks: false, 45 | })); 46 | } 47 | 48 | #measure(function_: GlobImplFunction): void { 49 | const timeStart = utils.timeStart(); 50 | 51 | const matches = function_(); 52 | 53 | const count = matches.length; 54 | const memory = utils.getMemory(); 55 | const time = utils.timeEnd(timeStart); 56 | 57 | bencho.time('time', time); 58 | bencho.memory('memory', memory); 59 | bencho.value('entries', count); 60 | } 61 | } 62 | 63 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 64 | (async () => { 65 | const args = process.argv.slice(2); 66 | 67 | const cwd = path.join(process.cwd(), args[0]); 68 | const pattern = args[1]; 69 | const impl = args[2] as GlobImplementation; 70 | 71 | const glob = new Glob(cwd, pattern); 72 | 73 | switch (impl) { 74 | case 'node-glob': { 75 | await glob.measureNodeGlob(); 76 | break; 77 | } 78 | 79 | case 'fast-glob': { 80 | await glob.measureFastGlob(); 81 | break; 82 | } 83 | 84 | case 'tinyglobby': { 85 | await glob.measureTinyGlobby(); 86 | break; 87 | } 88 | 89 | default: { 90 | throw new TypeError('Unknown glob implementation.'); 91 | } 92 | } 93 | })(); 94 | -------------------------------------------------------------------------------- /src/benchmark/suites/regression/async.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | import * as bencho from 'bencho'; 4 | 5 | import * as utils from '../../utils'; 6 | 7 | import type * as fastGlobCurrent from '../../..'; 8 | 9 | type GlobImplementation = 'current' | 'previous'; 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | type GlobImplFunction = (...args: any[]) => Promise; 12 | type GlobOptions = fastGlobCurrent.Options; 13 | 14 | class Glob { 15 | readonly #pattern: string; 16 | readonly #options: GlobOptions; 17 | 18 | constructor(pattern: string, options: GlobOptions) { 19 | this.#pattern = pattern; 20 | this.#options = { 21 | unique: false, 22 | followSymbolicLinks: false, 23 | ...options, 24 | }; 25 | } 26 | 27 | public async measurePreviousVersion(): Promise { 28 | const glob = await utils.importAndMeasure(utils.importPreviousFastGlob); 29 | 30 | // @ts-expect-error remove this line after the next major release. 31 | await this.#measure(() => glob.glob(this.#pattern, this.#options)); 32 | } 33 | 34 | public async measureCurrentVersion(): Promise { 35 | const glob = await utils.importAndMeasure(utils.importCurrentFastGlob); 36 | 37 | await this.#measure(() => glob.glob(this.#pattern, this.#options)); 38 | } 39 | 40 | async #measure(function_: GlobImplFunction): Promise { 41 | const timeStart = utils.timeStart(); 42 | 43 | const matches = await function_(); 44 | 45 | const count = matches.length; 46 | const memory = utils.getMemory(); 47 | const time = utils.timeEnd(timeStart); 48 | 49 | bencho.time('time', time); 50 | bencho.memory('memory', memory); 51 | bencho.value('entries', count); 52 | } 53 | } 54 | 55 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 56 | (async () => { 57 | const args = process.argv.slice(2); 58 | 59 | const cwd = path.join(process.cwd(), args[0]); 60 | const pattern = args[1]; 61 | const impl = args[2] as GlobImplementation; 62 | const options = JSON.parse(process.env['BENCHMARK_OPTIONS'] ?? '{}') as GlobOptions; 63 | 64 | const glob = new Glob(pattern, { 65 | cwd, 66 | ...options, 67 | }); 68 | 69 | switch (impl) { 70 | case 'current': { 71 | await glob.measureCurrentVersion(); 72 | break; 73 | } 74 | 75 | case 'previous': { 76 | await glob.measurePreviousVersion(); 77 | break; 78 | } 79 | 80 | default: { 81 | throw new TypeError('Unknown glob implementation.'); 82 | } 83 | } 84 | })(); 85 | -------------------------------------------------------------------------------- /src/benchmark/suites/regression/stream.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | import * as bencho from 'bencho'; 4 | 5 | import * as utils from '../../utils'; 6 | 7 | import type * as fastGlobCurrent from '../../..'; 8 | 9 | type GlobImplementation = 'current' | 'previous'; 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | type GlobImplFunction = (...args: any[]) => ReturnType; 12 | type GlobOptions = fastGlobCurrent.Options; 13 | 14 | class Glob { 15 | readonly #pattern: string; 16 | readonly #options: GlobOptions; 17 | 18 | constructor(pattern: string, options: GlobOptions) { 19 | this.#pattern = pattern; 20 | this.#options = { 21 | unique: false, 22 | followSymbolicLinks: false, 23 | ...options, 24 | }; 25 | } 26 | 27 | public async measurePreviousVersion(): Promise { 28 | const glob = await utils.importAndMeasure(utils.importPreviousFastGlob); 29 | 30 | // @ts-expect-error remove this line after the next major release. 31 | await this.#measure(() => glob.globStream(this.#pattern, this.#options)); 32 | } 33 | 34 | public async measureCurrentVersion(): Promise { 35 | const glob = await utils.importAndMeasure(utils.importCurrentFastGlob); 36 | 37 | await this.#measure(() => glob.globStream(this.#pattern, this.#options)); 38 | } 39 | 40 | async #measure(function_: GlobImplFunction): Promise { 41 | const entries: string[] = []; 42 | 43 | const timeStart = utils.timeStart(); 44 | 45 | await new Promise((resolve, reject) => { 46 | const stream = function_(); 47 | 48 | stream.once('error', (error: Error) => { 49 | reject(error); 50 | }); 51 | stream.on('data', (entry: string) => entries.push(entry)); 52 | stream.once('end', () => { 53 | resolve(); 54 | }); 55 | }); 56 | 57 | const count = entries.length; 58 | const memory = utils.getMemory(); 59 | const time = utils.timeEnd(timeStart); 60 | 61 | bencho.time('time', time); 62 | bencho.memory('memory', memory); 63 | bencho.value('entries', count); 64 | } 65 | } 66 | 67 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 68 | (async () => { 69 | const args = process.argv.slice(2); 70 | 71 | const cwd = path.join(process.cwd(), args[0]); 72 | const pattern = args[1]; 73 | const impl = args[2] as GlobImplementation; 74 | const options = JSON.parse(process.env['BENCHMARK_OPTIONS'] ?? '{}') as GlobOptions; 75 | 76 | const glob = new Glob(pattern, { 77 | cwd, 78 | ...options, 79 | }); 80 | 81 | switch (impl) { 82 | case 'current': { 83 | await glob.measureCurrentVersion(); 84 | break; 85 | } 86 | 87 | case 'previous': { 88 | await glob.measurePreviousVersion(); 89 | break; 90 | } 91 | 92 | default: { 93 | throw new TypeError('Unknown glob implementation.'); 94 | } 95 | } 96 | })(); 97 | -------------------------------------------------------------------------------- /src/benchmark/suites/regression/sync.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | import * as bencho from 'bencho'; 4 | 5 | import * as utils from '../../utils'; 6 | 7 | import type * as fastGlobCurrent from '../../..'; 8 | 9 | type GlobImplementation = 'current' | 'previous'; 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | type GlobImplFunction = (...args: any[]) => unknown[]; 12 | type GlobOptions = fastGlobCurrent.Options; 13 | 14 | class Glob { 15 | readonly #pattern: string; 16 | readonly #options: GlobOptions; 17 | 18 | constructor(pattern: string, options: GlobOptions) { 19 | this.#pattern = pattern; 20 | this.#options = { 21 | unique: false, 22 | followSymbolicLinks: false, 23 | ...options, 24 | }; 25 | } 26 | 27 | public async measurePreviousVersion(): Promise { 28 | const glob = await utils.importAndMeasure(utils.importPreviousFastGlob); 29 | 30 | // @ts-expect-error remove this line after the next major release. 31 | this.#measure(() => glob.globSync(this.#pattern, this.#options)); 32 | } 33 | 34 | public async measureCurrentVersion(): Promise { 35 | const glob = await utils.importAndMeasure(utils.importCurrentFastGlob); 36 | 37 | this.#measure(() => glob.globSync(this.#pattern, this.#options)); 38 | } 39 | 40 | #measure(function_: GlobImplFunction): void { 41 | const timeStart = utils.timeStart(); 42 | 43 | const matches = function_(); 44 | 45 | const count = matches.length; 46 | const memory = utils.getMemory(); 47 | const time = utils.timeEnd(timeStart); 48 | 49 | bencho.time('time', time); 50 | bencho.memory('memory', memory); 51 | bencho.value('entries', count); 52 | } 53 | } 54 | 55 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 56 | (async () => { 57 | const args = process.argv.slice(2); 58 | 59 | const cwd = path.join(process.cwd(), args[0]); 60 | const pattern = args[1]; 61 | const impl = args[2] as GlobImplementation; 62 | const options = JSON.parse(process.env['BENCHMARK_OPTIONS'] ?? '{}') as GlobOptions; 63 | 64 | const glob = new Glob(pattern, { 65 | cwd, 66 | ...options, 67 | }); 68 | 69 | switch (impl) { 70 | case 'current': { 71 | await glob.measureCurrentVersion(); 72 | break; 73 | } 74 | 75 | case 'previous': { 76 | await glob.measurePreviousVersion(); 77 | break; 78 | } 79 | 80 | default: { 81 | throw new TypeError('Unknown glob implementation.'); 82 | } 83 | } 84 | })(); 85 | -------------------------------------------------------------------------------- /src/benchmark/utils.ts: -------------------------------------------------------------------------------- 1 | import { performance } from 'node:perf_hooks'; 2 | 3 | import * as bencho from 'bencho'; 4 | 5 | import type * as currentVersion from '..'; 6 | import type * as previousVersion from 'fast-glob'; 7 | import type * as glob from 'glob'; 8 | import type * as tg from 'tinyglobby'; 9 | 10 | export function timeStart(): number { 11 | return performance.now(); 12 | } 13 | 14 | export function timeEnd(start: number): number { 15 | return performance.now() - start; 16 | } 17 | 18 | export function getMemory(): number { 19 | return process.memoryUsage().heapUsed; 20 | } 21 | 22 | export function importCurrentFastGlob(): Promise { 23 | return import('..'); 24 | } 25 | 26 | export function importPreviousFastGlob(): Promise { 27 | return import('fast-glob'); 28 | } 29 | 30 | export function importNodeGlob(): Promise { 31 | return import('glob'); 32 | } 33 | 34 | export function importTinyGlobby(): Promise { 35 | return import('tinyglobby'); 36 | } 37 | 38 | export async function importAndMeasure(function_: () => Promise): Promise { 39 | const start = timeStart(); 40 | 41 | const result = await function_(); 42 | 43 | const time = timeEnd(start); 44 | 45 | bencho.time('import.time', time); 46 | 47 | return result; 48 | } 49 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as taskManager from './managers/tasks'; 2 | import Settings from './settings'; 3 | import * as utils from './utils'; 4 | import { ProviderAsync, ProviderStream, ProviderSync } from './providers'; 5 | import { ReaderAsync, ReaderStream, ReaderSync } from './readers'; 6 | 7 | import type { Options as OptionsInternal } from './settings'; 8 | import type { Entry as EntryInternal, EntryItem, FileSystemAdapter as FileSystemAdapterInternal, Pattern as PatternInternal } from './types'; 9 | 10 | type InputPattern = PatternInternal | readonly PatternInternal[]; 11 | 12 | type EntryObjectModePredicate = { [TKey in keyof Pick]-?: true }; 13 | type EntryStatsPredicate = { [TKey in keyof Pick]-?: true }; 14 | type EntryObjectPredicate = EntryObjectModePredicate | EntryStatsPredicate; 15 | 16 | export type Options = OptionsInternal; 17 | export type Entry = EntryInternal; 18 | export type Task = taskManager.Task; 19 | export type Pattern = PatternInternal; 20 | export type FileSystemAdapter = FileSystemAdapterInternal; 21 | 22 | export function glob(source: InputPattern, options: EntryObjectPredicate & OptionsInternal): Promise; 23 | export function glob(source: InputPattern, options?: OptionsInternal): Promise; 24 | export async function glob(source: InputPattern, options?: OptionsInternal): Promise { 25 | assertPatternsInput(source); 26 | 27 | const settings = new Settings(options); 28 | const reader = new ReaderAsync(settings); 29 | const provider = new ProviderAsync(reader, settings); 30 | 31 | const tasks = getTasks(source, settings); 32 | const promises = tasks.map((task) => provider.read(task)); 33 | 34 | const result = await Promise.all(promises); 35 | 36 | return utils.array.flatFirstLevel(result); 37 | } 38 | 39 | /** 40 | * @deprecated 41 | * This method will be removed in v5, use the `.glob` method instead. 42 | */ 43 | export const async = glob; 44 | 45 | export function globSync(source: InputPattern, options: EntryObjectPredicate & OptionsInternal): EntryInternal[]; 46 | export function globSync(source: InputPattern, options?: OptionsInternal): string[]; 47 | export function globSync(source: InputPattern, options?: OptionsInternal): EntryItem[] { 48 | assertPatternsInput(source); 49 | 50 | const settings = new Settings(options); 51 | const reader = new ReaderSync(settings); 52 | const provider = new ProviderSync(reader, settings); 53 | 54 | const tasks = getTasks(source, settings); 55 | const entries = tasks.map((task) => provider.read(task)); 56 | 57 | return utils.array.flatFirstLevel(entries); 58 | } 59 | 60 | /** 61 | * @deprecated 62 | * This method will be removed in v5, use the `.globSync` method instead. 63 | */ 64 | export const sync = globSync; 65 | 66 | export function globStream(source: InputPattern, options?: OptionsInternal): NodeJS.ReadableStream { 67 | assertPatternsInput(source); 68 | 69 | const settings = new Settings(options); 70 | const reader = new ReaderStream(settings); 71 | const provider = new ProviderStream(reader, settings); 72 | 73 | const tasks = getTasks(source, settings); 74 | const streams = tasks.map((task) => provider.read(task)); 75 | 76 | /** 77 | * The stream returned by the provider cannot work with an asynchronous iterator. 78 | * To support asynchronous iterators, regardless of the number of tasks, we always multiplex streams. 79 | * This affects performance (+25%). I don't see best solution right now. 80 | */ 81 | return utils.stream.merge(streams); 82 | } 83 | 84 | /** 85 | * @deprecated 86 | * This method will be removed in v5, use the `.globStream` method instead. 87 | */ 88 | export const stream = globStream; 89 | 90 | export function generateTasks(source: InputPattern, options?: OptionsInternal): Task[] { 91 | assertPatternsInput(source); 92 | 93 | const patterns = ([] as PatternInternal[]).concat(source); 94 | const settings = new Settings(options); 95 | 96 | return taskManager.generate(patterns, settings); 97 | } 98 | 99 | export function isDynamicPattern(source: PatternInternal, options?: OptionsInternal): boolean { 100 | assertPatternsInput(source); 101 | 102 | const settings = new Settings(options); 103 | 104 | return utils.pattern.isDynamicPattern(source, settings); 105 | } 106 | 107 | export const escapePath = withPatternsInputAssert(utils.path.escape); 108 | export const convertPathToPattern = withPatternsInputAssert(utils.path.convertPathToPattern); 109 | 110 | export const posix = { 111 | escapePath: withPatternsInputAssert(utils.path.escapePosixPath), 112 | convertPathToPattern: withPatternsInputAssert(utils.path.convertPosixPathToPattern), 113 | }; 114 | 115 | export const win32 = { 116 | escapePath: withPatternsInputAssert(utils.path.escapeWindowsPath), 117 | convertPathToPattern: withPatternsInputAssert(utils.path.convertWindowsPathToPattern), 118 | }; 119 | 120 | function getTasks(source: InputPattern, settings: Settings): taskManager.Task[] { 121 | const patterns = ([] as PatternInternal[]).concat(source); 122 | 123 | return taskManager.generate(patterns, settings); 124 | } 125 | 126 | function assertPatternsInput(input: unknown): never | void { 127 | const source = ([] as unknown[]).concat(input); 128 | const isValidSource = source.every((item) => utils.string.isString(item) && !utils.string.isEmpty(item)); 129 | 130 | if (!isValidSource) { 131 | throw new TypeError('Patterns must be a string (non empty) or an array of strings'); 132 | } 133 | } 134 | 135 | function withPatternsInputAssert(method: (source: string) => string) { 136 | return (source: string): string => { 137 | assertPatternsInput(source); 138 | 139 | return method(source); 140 | }; 141 | } 142 | -------------------------------------------------------------------------------- /src/managers/tasks.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | 3 | import { describe, it } from 'mocha'; 4 | 5 | import Settings from '../settings'; 6 | import * as tests from '../tests'; 7 | import * as manager from './tasks'; 8 | 9 | import type { PatternsGroup } from '../types'; 10 | 11 | describe('Managers → Task', () => { 12 | describe('.generate', () => { 13 | it('should return task with negative patterns from «ignore» option', () => { 14 | const settings = new Settings({ ignore: ['*.txt'] }); 15 | 16 | const expected = [ 17 | tests.task.builder().base('a').positive('a/*').negative('*.md').negative('*.txt').build(), 18 | ]; 19 | 20 | const actual = manager.generate(['a/*', '!*.md'], settings); 21 | 22 | assert.deepStrictEqual(actual, expected); 23 | }); 24 | 25 | it('should return static and dynamic tasks', () => { 26 | const settings = new Settings({ ignore: ['*.txt'] }); 27 | 28 | const expected = [ 29 | tests.task.builder().base('a').static().positive('a/file.json').negative('b/*.md').negative('*.txt').build(), 30 | tests.task.builder().base('b').positive('b/*').negative('b/*.md').negative('*.txt').build(), 31 | ]; 32 | 33 | const actual = manager.generate(['a/file.json', 'b/*', '!b/*.md'], settings); 34 | 35 | assert.deepStrictEqual(actual, expected); 36 | }); 37 | 38 | it('should return only dynamic tasks when the `caseSensitiveMatch` option is enabled', () => { 39 | const settings = new Settings({ caseSensitiveMatch: false }); 40 | 41 | const expected = [ 42 | tests.task.builder().base('a').positive('a/file.json').negative('b/*.md').build(), 43 | tests.task.builder().base('b').positive('b/*').negative('b/*.md').build(), 44 | ]; 45 | 46 | const actual = manager.generate(['a/file.json', 'b/*', '!b/*.md'], settings); 47 | 48 | assert.deepStrictEqual(actual, expected); 49 | }); 50 | 51 | it('should expand patterns with brace expansion', () => { 52 | const settings = new Settings(); 53 | 54 | const expected = [ 55 | tests.task.builder().base('a').positive('a/*').build(), 56 | tests.task.builder().base('a/b').positive('a/b/*').build(), 57 | ]; 58 | 59 | const actual = manager.generate(['a/{b,}/*'], settings); 60 | 61 | assert.deepStrictEqual(actual, expected); 62 | }); 63 | 64 | it('should do not expand patterns with brace expansion when the `braceExpansion` option is disabled', () => { 65 | const settings = new Settings({ braceExpansion: false }); 66 | 67 | const expected = [ 68 | tests.task.builder().base('a').positive('a/{b,}/*').build(), 69 | ]; 70 | 71 | const actual = manager.generate(['a/{b,}/*'], settings); 72 | 73 | assert.deepStrictEqual(actual, expected); 74 | }); 75 | 76 | it('should do not process patterns when the `baseNameMatch` option is enabled and the pattern has a slash', () => { 77 | const settings = new Settings({ baseNameMatch: true }); 78 | 79 | const expected = [ 80 | tests.task.builder().base('root').positive('root/*/file.txt').build(), 81 | ]; 82 | 83 | const actual = manager.generate(['root/*/file.txt'], settings); 84 | 85 | assert.deepStrictEqual(actual, expected); 86 | }); 87 | 88 | it('should add glob star to patterns when the `baseNameMatch` option is enabled and the pattern does not have a slash', () => { 89 | const settings = new Settings({ baseNameMatch: true }); 90 | 91 | const expected = [ 92 | tests.task.builder().base('.').positive('**/file.txt').build(), 93 | ]; 94 | 95 | const actual = manager.generate(['file.txt'], settings); 96 | 97 | assert.deepStrictEqual(actual, expected); 98 | }); 99 | }); 100 | 101 | describe('.convertPatternsToTasks', () => { 102 | it('should return one task when positive patterns have a global pattern', () => { 103 | const expected = [ 104 | tests.task.builder().base('.').positive('*').negative('*.md').build(), 105 | ]; 106 | 107 | const actual = manager.convertPatternsToTasks(['*'], ['*.md'], /* dynamic */ true); 108 | 109 | assert.deepStrictEqual(actual, expected); 110 | }); 111 | 112 | it('should return two tasks when one of patterns contains reference to the parent directory', () => { 113 | const expected = [ 114 | tests.task.builder().base('..').positive('../*.md').negative('*.md').build(), 115 | tests.task.builder().base('.').positive('*').positive('a/*').negative('*.md').build(), 116 | ]; 117 | 118 | const actual = manager.convertPatternsToTasks(['*', 'a/*', '../*.md'], ['*.md'], /* dynamic */ true); 119 | 120 | assert.deepStrictEqual(actual, expected); 121 | }); 122 | 123 | it('should return two tasks when all patterns refers to the different base directories', () => { 124 | const expected = [ 125 | tests.task.builder().base('a').positive('a/*').negative('b/*.md').build(), 126 | tests.task.builder().base('b').positive('b/*').negative('b/*.md').build(), 127 | ]; 128 | 129 | const actual = manager.convertPatternsToTasks(['a/*', 'b/*'], ['b/*.md'], /* dynamic */ true); 130 | 131 | assert.deepStrictEqual(actual, expected); 132 | }); 133 | }); 134 | 135 | describe('.getPositivePatterns', () => { 136 | it('should return only positive patterns', () => { 137 | const expected = ['*']; 138 | 139 | const actual = manager.getPositivePatterns(['*', '!*.md']); 140 | 141 | assert.deepStrictEqual(actual, expected); 142 | }); 143 | }); 144 | 145 | describe('.getNegativePatternsAsPositive', () => { 146 | it('should return negative patterns as positive', () => { 147 | const expected = ['*.md']; 148 | 149 | const actual = manager.getNegativePatternsAsPositive(['*', '!*.md'], []); 150 | 151 | assert.deepStrictEqual(actual, expected); 152 | }); 153 | 154 | it('should return negative patterns as positive with patterns from ignore option', () => { 155 | const expected = ['*.md', '*.txt', '*.json']; 156 | 157 | const actual = manager.getNegativePatternsAsPositive(['*', '!*.md'], ['*.txt', '!*.json']); 158 | 159 | assert.deepStrictEqual(actual, expected); 160 | }); 161 | }); 162 | 163 | describe('.groupPatternsByBaseDirectory', () => { 164 | it('should return empty object', () => { 165 | const expected: PatternsGroup = {}; 166 | 167 | const actual = manager.groupPatternsByBaseDirectory([]); 168 | 169 | assert.deepStrictEqual(actual, expected); 170 | }); 171 | 172 | it('should return grouped patterns', () => { 173 | const expected: PatternsGroup = { 174 | '.': ['*'], 175 | a: ['a/*'], 176 | }; 177 | 178 | const actual = manager.groupPatternsByBaseDirectory(['*', 'a/*']); 179 | 180 | assert.deepStrictEqual(actual, expected); 181 | }); 182 | 183 | it('should remove backslashes from the base directory', () => { 184 | const expected: PatternsGroup = { 185 | "a'b": [String.raw`a\'b/*`], 186 | }; 187 | 188 | const actual = manager.groupPatternsByBaseDirectory([String.raw`a\'b/*`]); 189 | 190 | assert.deepStrictEqual(actual, expected); 191 | }); 192 | }); 193 | 194 | describe('.convertPatternGroupsToTasks', () => { 195 | it('should return two tasks', () => { 196 | const expected = [ 197 | tests.task.builder().base('a').positive('a/*').negative('b/*.md').build(), 198 | tests.task.builder().base('b').positive('b/*').negative('b/*.md').build(), 199 | ]; 200 | 201 | const actual = manager.convertPatternGroupsToTasks({ a: ['a/*'], b: ['b/*'] }, ['b/*.md'], /* dynamic */ true); 202 | 203 | assert.deepStrictEqual(actual, expected); 204 | }); 205 | }); 206 | 207 | describe('.convertPatternGroupToTask', () => { 208 | it('should return created dynamic task', () => { 209 | const expected = tests.task.builder().base('.').positive('*').negative('*.md').build(); 210 | 211 | const actual = manager.convertPatternGroupToTask('.', ['*'], ['*.md'], /* dynamic */ true); 212 | 213 | assert.deepStrictEqual(actual, expected); 214 | }); 215 | 216 | it('should return created static task', () => { 217 | const expected = tests.task.builder().base('.').static().positive('.gitignore').negative('.git*').build(); 218 | 219 | const actual = manager.convertPatternGroupToTask('.', ['.gitignore'], ['.git*'], /* dynamic */ false); 220 | 221 | assert.deepStrictEqual(actual, expected); 222 | }); 223 | 224 | it('should normalize the base path', () => { 225 | const expected = tests.task.builder().base('root/directory').build(); 226 | 227 | const actual = manager.convertPatternGroupToTask('root/directory', [], [], /* dynamic */ true); 228 | 229 | assert.deepStrictEqual(actual, expected); 230 | }); 231 | }); 232 | }); 233 | -------------------------------------------------------------------------------- /src/managers/tasks.ts: -------------------------------------------------------------------------------- 1 | import * as utils from '../utils'; 2 | 3 | import type Settings from '../settings'; 4 | import type { Pattern, PatternsGroup } from '../types'; 5 | 6 | export interface Task { 7 | base: string; 8 | dynamic: boolean; 9 | patterns: Pattern[]; 10 | positive: Pattern[]; 11 | negative: Pattern[]; 12 | } 13 | 14 | export function generate(input: readonly Pattern[], settings: Settings): Task[] { 15 | const patterns = processPatterns([...input], settings); 16 | const ignore = processPatterns([...settings.ignore], settings); 17 | 18 | const positivePatterns = getPositivePatterns(patterns); 19 | const negativePatterns = getNegativePatternsAsPositive(patterns, ignore); 20 | 21 | const staticPatterns = positivePatterns.filter((pattern) => utils.pattern.isStaticPattern(pattern, settings)); 22 | const dynamicPatterns = positivePatterns.filter((pattern) => utils.pattern.isDynamicPattern(pattern, settings)); 23 | 24 | const staticTasks = convertPatternsToTasks(staticPatterns, negativePatterns, /* dynamic */ false); 25 | const dynamicTasks = convertPatternsToTasks(dynamicPatterns, negativePatterns, /* dynamic */ true); 26 | 27 | return staticTasks.concat(dynamicTasks); 28 | } 29 | 30 | function processPatterns(input: Pattern[], settings: Settings): Pattern[] { 31 | let patterns: Pattern[] = input; 32 | 33 | /** 34 | * The original pattern like `{,*,**,a/*}` can lead to problems checking the depth when matching entry 35 | * and some problems with the micromatch package (see fast-glob issues: #365, #394). 36 | * 37 | * To solve this problem, we expand all patterns containing brace expansion. This can lead to a slight slowdown 38 | * in matching in the case of a large set of patterns after expansion. 39 | */ 40 | if (settings.braceExpansion) { 41 | patterns = utils.pattern.expandPatternsWithBraceExpansion(patterns); 42 | } 43 | 44 | /** 45 | * If the `baseNameMatch` option is enabled, we must add globstar to patterns, so that they can be used 46 | * at any nesting level. 47 | * 48 | * We do this here, because otherwise we have to complicate the filtering logic. For example, we need to change 49 | * the pattern in the filter before creating a regular expression. There is no need to change the patterns 50 | * in the application. Only on the input. 51 | */ 52 | if (settings.baseNameMatch) { 53 | patterns = patterns.map((pattern) => pattern.includes('/') ? pattern : `**/${pattern}`); 54 | } 55 | 56 | /** 57 | * This method also removes duplicate slashes that may have been in the pattern or formed as a result of expansion. 58 | */ 59 | return patterns.map((pattern) => utils.pattern.removeDuplicateSlashes(pattern)); 60 | } 61 | 62 | /** 63 | * Returns tasks grouped by basic pattern directories. 64 | * 65 | * Patterns that can be found inside (`./`) and outside (`../`) the current directory are handled separately. 66 | * This is necessary because directory traversal starts at the base directory and goes deeper. 67 | */ 68 | export function convertPatternsToTasks(positive: Pattern[], negative: Pattern[], dynamic: boolean): Task[] { 69 | const tasks: Task[] = []; 70 | 71 | const patternsOutsideCurrentDirectory = utils.pattern.getPatternsOutsideCurrentDirectory(positive); 72 | const patternsInsideCurrentDirectory = utils.pattern.getPatternsInsideCurrentDirectory(positive); 73 | 74 | const outsideCurrentDirectoryGroup = groupPatternsByBaseDirectory(patternsOutsideCurrentDirectory); 75 | const insideCurrentDirectoryGroup = groupPatternsByBaseDirectory(patternsInsideCurrentDirectory); 76 | 77 | tasks.push(...convertPatternGroupsToTasks(outsideCurrentDirectoryGroup, negative, dynamic)); 78 | 79 | /* 80 | * For the sake of reducing future accesses to the file system, we merge all tasks within the current directory 81 | * into a global task, if at least one pattern refers to the root (`.`). In this case, the global task covers the rest. 82 | */ 83 | if ('.' in insideCurrentDirectoryGroup) { 84 | tasks.push(convertPatternGroupToTask('.', patternsInsideCurrentDirectory, negative, dynamic)); 85 | } else { 86 | tasks.push(...convertPatternGroupsToTasks(insideCurrentDirectoryGroup, negative, dynamic)); 87 | } 88 | 89 | return tasks; 90 | } 91 | 92 | export function getPositivePatterns(patterns: Pattern[]): Pattern[] { 93 | return utils.pattern.getPositivePatterns(patterns); 94 | } 95 | 96 | export function getNegativePatternsAsPositive(patterns: Pattern[], ignore: Pattern[]): Pattern[] { 97 | const negative = utils.pattern.getNegativePatterns(patterns).concat(ignore); 98 | const positive = negative.map((pattern) => utils.pattern.convertToPositivePattern(pattern)); 99 | 100 | return positive; 101 | } 102 | 103 | export function groupPatternsByBaseDirectory(patterns: Pattern[]): PatternsGroup { 104 | const group: PatternsGroup = {}; 105 | 106 | return patterns.reduce((collection, pattern) => { 107 | let base = utils.pattern.getBaseDirectory(pattern); 108 | 109 | /** 110 | * After extracting the basic static part of the pattern, it becomes a path, 111 | * so escaping leads to referencing non-existent paths. 112 | */ 113 | base = utils.path.removeBackslashes(base); 114 | 115 | if (base in collection) { 116 | collection[base].push(pattern); 117 | } else { 118 | collection[base] = [pattern]; 119 | } 120 | 121 | return collection; 122 | }, group); 123 | } 124 | 125 | export function convertPatternGroupsToTasks(positive: PatternsGroup, negative: Pattern[], dynamic: boolean): Task[] { 126 | return Object.keys(positive).map((base) => { 127 | return convertPatternGroupToTask(base, positive[base], negative, dynamic); 128 | }); 129 | } 130 | 131 | export function convertPatternGroupToTask(base: string, positive: Pattern[], negative: Pattern[], dynamic: boolean): Task { 132 | return { 133 | dynamic, 134 | positive, 135 | negative, 136 | base, 137 | patterns: ([] as Pattern[]).concat(positive, negative.map((pattern) => utils.pattern.convertToNegativePattern(pattern))), 138 | }; 139 | } 140 | -------------------------------------------------------------------------------- /src/providers/async.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | 3 | import * as sinon from 'sinon'; 4 | import { describe, it } from 'mocha'; 5 | 6 | import Settings from '../settings'; 7 | import * as tests from '../tests'; 8 | import { ReaderAsync } from '../readers'; 9 | import { ProviderAsync } from './async'; 10 | 11 | import type { IReaderAsync } from '../readers'; 12 | import type { Options } from '../settings'; 13 | import type { Entry, EntryItem, ErrnoException } from '../types'; 14 | import type { Task } from '../managers/tasks'; 15 | 16 | type StubbedReaderAsync = sinon.SinonStubbedInstance; 17 | 18 | class TestProvider extends ProviderAsync { 19 | public readonly reader: StubbedReaderAsync; 20 | 21 | constructor( 22 | options?: Options, 23 | reader: StubbedReaderAsync = sinon.createStubInstance(ReaderAsync), 24 | ) { 25 | super(reader, new Settings(options)); 26 | 27 | this.reader = reader; 28 | } 29 | } 30 | 31 | function getProvider(options?: Options): TestProvider { 32 | return new TestProvider(options); 33 | } 34 | 35 | function getEntries(provider: TestProvider, task: Task, entry: Entry): Promise { 36 | provider.reader.dynamic.resolves([entry]); 37 | provider.reader.static.resolves([entry]); 38 | 39 | return provider.read(task); 40 | } 41 | 42 | describe('Providers → ProviderAsync', () => { 43 | describe('Constructor', () => { 44 | it('should create instance of class', () => { 45 | const provider = getProvider(); 46 | 47 | assert.ok(provider instanceof ProviderAsync); 48 | }); 49 | }); 50 | 51 | describe('.read', () => { 52 | it('should return entries for dynamic task', async () => { 53 | const provider = getProvider(); 54 | const task = tests.task.builder().base('.').positive('*').build(); 55 | const entry = tests.entry.builder().path('root/file.txt').build(); 56 | 57 | const expected = ['root/file.txt']; 58 | 59 | const actual = await getEntries(provider, task, entry); 60 | 61 | assert.strictEqual(provider.reader.dynamic.callCount, 1); 62 | assert.deepStrictEqual(actual, expected); 63 | }); 64 | 65 | it('should return entries for static task', async () => { 66 | const provider = getProvider(); 67 | const task = tests.task.builder().base('.').static().positive('*').build(); 68 | const entry = tests.entry.builder().path('root/file.txt').build(); 69 | 70 | const expected = ['root/file.txt']; 71 | 72 | const actual = await getEntries(provider, task, entry); 73 | 74 | assert.strictEqual(provider.reader.static.callCount, 1); 75 | assert.deepStrictEqual(actual, expected); 76 | }); 77 | 78 | it('should throw error', async () => { 79 | const provider = getProvider(); 80 | const task = tests.task.builder().base('.').positive('*').build(); 81 | 82 | provider.reader.dynamic.rejects(tests.errno.getEnoent()); 83 | 84 | try { 85 | await provider.read(task); 86 | 87 | throw new Error('Wow'); 88 | } catch (error) { 89 | assert.strictEqual((error as ErrnoException).code, 'ENOENT'); 90 | } 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/providers/async.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from './provider'; 2 | 3 | import type { IReaderAsync } from '../readers'; 4 | import type Settings from '../settings'; 5 | import type { Task } from '../managers/tasks'; 6 | import type { Entry, EntryItem, ReaderOptions } from '../types'; 7 | 8 | export class ProviderAsync extends Provider> { 9 | readonly #reader: IReaderAsync; 10 | 11 | constructor(reader: IReaderAsync, settings: Settings) { 12 | super(settings); 13 | 14 | this.#reader = reader; 15 | } 16 | 17 | public async read(task: Task): Promise { 18 | const root = this._getRootDirectory(task); 19 | const options = this._getReaderOptions(task); 20 | 21 | const entries = await this.api(root, task, options); 22 | 23 | return entries.map((entry) => options.transform(entry)); 24 | } 25 | 26 | public api(root: string, task: Task, options: ReaderOptions): Promise { 27 | if (task.dynamic) { 28 | return this.#reader.dynamic(root, options); 29 | } 30 | 31 | return this.#reader.static(task.patterns, options); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/providers/filters/deep.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | 3 | import { describe, it } from 'mocha'; 4 | 5 | import Settings from '../../settings'; 6 | import * as tests from '../../tests'; 7 | import DeepFilter from './deep'; 8 | 9 | import type { EntryFilterFunction, Pattern, Entry } from '../../types'; 10 | import type { Options } from '../../settings'; 11 | 12 | interface FilterOptions { 13 | base?: string; 14 | positive: Pattern[]; 15 | negative?: Pattern[]; 16 | options?: Options; 17 | } 18 | 19 | const DIRECTORY_ENTRY_LEVEL_1 = tests.entry.builder().path('root').directory().build(); 20 | const DIRECTORY_ENTRY_LEVEL_2 = tests.entry.builder().path('root/directory').directory().build(); 21 | const DIRECTORY_ENTRY_LEVEL_3 = tests.entry.builder().path('root/nested/directory').directory().build(); 22 | 23 | function getDeepFilterInstance(options?: Options): DeepFilter { 24 | const settings = new Settings(options); 25 | 26 | return new DeepFilter(settings, { 27 | dot: settings.dot, 28 | }); 29 | } 30 | 31 | function getFilter(options: FilterOptions): EntryFilterFunction { 32 | const base = options.base ?? '.'; 33 | const negative = options.negative ?? []; 34 | 35 | return getDeepFilterInstance(options.options).getFilter(base, options.positive, negative); 36 | } 37 | 38 | function getResult(entry: Entry, options: FilterOptions): boolean { 39 | const filter = getFilter(options); 40 | 41 | return filter(entry); 42 | } 43 | 44 | function accept(entry: Entry, options: FilterOptions): void { 45 | assert.strictEqual(getResult(entry, options), true); 46 | } 47 | 48 | function reject(entry: Entry, options: FilterOptions): void { 49 | assert.strictEqual(getResult(entry, options), false); 50 | } 51 | 52 | describe('Providers → Filters → Deep', () => { 53 | describe('Constructor', () => { 54 | it('should create instance of class', () => { 55 | const filter = getDeepFilterInstance(); 56 | 57 | assert.ok(filter instanceof DeepFilter); 58 | }); 59 | }); 60 | 61 | describe('.getFilter', () => { 62 | describe('options.deep', () => { 63 | it('should reject when an option has "0" as value', () => { 64 | reject(DIRECTORY_ENTRY_LEVEL_1, { 65 | positive: ['**/*'], 66 | options: { deep: 0 }, 67 | }); 68 | }); 69 | 70 | it('should reject when the depth of entry is greater than an allowable value (without base)', () => { 71 | reject(DIRECTORY_ENTRY_LEVEL_3, { 72 | positive: ['**/*'], 73 | options: { deep: 1 }, 74 | }); 75 | }); 76 | 77 | it('should reject when the depth of entry is greater than an allowable value (with base as current level)', () => { 78 | reject(DIRECTORY_ENTRY_LEVEL_3, { 79 | positive: ['**/*'], 80 | options: { deep: 1 }, 81 | }); 82 | }); 83 | 84 | it('should reject when the depth of entry is greater than an allowable value (with nested base)', () => { 85 | reject(DIRECTORY_ENTRY_LEVEL_3, { 86 | base: 'root/a', 87 | positive: ['root/a/*'], 88 | options: { deep: 1 }, 89 | }); 90 | }); 91 | 92 | it('should accept when an option has "Infinity" as value', () => { 93 | accept(DIRECTORY_ENTRY_LEVEL_1, { 94 | positive: ['**/*'], 95 | options: { deep: Number.POSITIVE_INFINITY }, 96 | }); 97 | }); 98 | }); 99 | 100 | describe('options.followSymbolicLinks', () => { 101 | it('should reject when an entry is symbolic link and option is disabled', () => { 102 | const entry = tests.entry.builder().path('root').directory().symlink().build(); 103 | 104 | reject(entry, { 105 | positive: ['**/*'], 106 | options: { followSymbolicLinks: false }, 107 | }); 108 | }); 109 | 110 | it('should accept when an entry is symbolic link and option is enabled', () => { 111 | const entry = tests.entry.builder().path('root').directory().symlink().build(); 112 | 113 | accept(entry, { 114 | positive: ['**/*'], 115 | options: { followSymbolicLinks: true }, 116 | }); 117 | }); 118 | }); 119 | 120 | describe('Positive pattern', () => { 121 | it('should reject when an entry does not match to the positive pattern', () => { 122 | reject(DIRECTORY_ENTRY_LEVEL_1, { 123 | positive: ['non-root/*'], 124 | }); 125 | }); 126 | 127 | it('should reject when an entry starts with leading dot and does not match to the positive pattern', () => { 128 | const entry = tests.entry.builder().path('./root').directory().build(); 129 | 130 | reject(entry, { 131 | positive: ['non-root/*'], 132 | }); 133 | }); 134 | 135 | it('should accept when an entry match to the positive pattern with leading dot', () => { 136 | const entry = tests.entry.builder().path('./root').directory().build(); 137 | 138 | accept(entry, { 139 | positive: ['./root/*'], 140 | }); 141 | }); 142 | 143 | it('should accept when the positive pattern does not match by level, but the "baseNameMatch" is enabled', () => { 144 | accept(DIRECTORY_ENTRY_LEVEL_2, { 145 | positive: ['*'], 146 | options: { baseNameMatch: true }, 147 | }); 148 | }); 149 | 150 | it('should accept when the positive pattern has a globstar', () => { 151 | accept(DIRECTORY_ENTRY_LEVEL_3, { 152 | positive: ['**/*'], 153 | }); 154 | }); 155 | }); 156 | 157 | describe('Negative pattern', () => { 158 | it('should reject when an entry match to the negative pattern', () => { 159 | reject(DIRECTORY_ENTRY_LEVEL_2, { 160 | positive: ['**/*'], 161 | negative: ['root/**'], 162 | }); 163 | }); 164 | 165 | it('should accept when the negative pattern has no effect to depth reading', () => { 166 | accept(DIRECTORY_ENTRY_LEVEL_3, { 167 | positive: ['**/*'], 168 | negative: ['**/*'], 169 | }); 170 | }); 171 | 172 | it('should accept when an entry does not match to the negative pattern', () => { 173 | accept(DIRECTORY_ENTRY_LEVEL_3, { 174 | positive: ['**/*'], 175 | negative: ['non-root/**/*'], 176 | }); 177 | }); 178 | }); 179 | }); 180 | 181 | describe('Immutability', () => { 182 | it('should return the data without changes', () => { 183 | const filter = getFilter({ 184 | positive: ['**/*'], 185 | }); 186 | 187 | const reference = tests.entry.builder().path('root/directory').directory().build(); 188 | const entry = tests.entry.builder().path('root/directory').directory().build(); 189 | 190 | filter(entry); 191 | 192 | assert.deepStrictEqual(entry, reference); 193 | }); 194 | }); 195 | }); 196 | -------------------------------------------------------------------------------- /src/providers/filters/deep.ts: -------------------------------------------------------------------------------- 1 | import * as utils from '../../utils'; 2 | import PartialMatcher from '../matchers/partial'; 3 | 4 | import type { MicromatchOptions, Entry, EntryFilterFunction, Pattern, PatternRe } from '../../types'; 5 | import type Settings from '../../settings'; 6 | 7 | export default class DeepFilter { 8 | readonly #settings: Settings; 9 | readonly #micromatchOptions: MicromatchOptions; 10 | 11 | constructor(settings: Settings, micromatchOptions: MicromatchOptions) { 12 | this.#settings = settings; 13 | this.#micromatchOptions = micromatchOptions; 14 | } 15 | 16 | public getFilter(basePath: string, positive: Pattern[], negative: Pattern[]): EntryFilterFunction { 17 | const matcher = this.#getMatcher(positive); 18 | const negativeRe = this.#getNegativePatternsRe(negative); 19 | 20 | return (entry) => this.#filter(basePath, entry, matcher, negativeRe); 21 | } 22 | 23 | #getMatcher(patterns: Pattern[]): PartialMatcher { 24 | return new PartialMatcher(patterns, this.#settings, this.#micromatchOptions); 25 | } 26 | 27 | #getNegativePatternsRe(patterns: Pattern[]): PatternRe[] { 28 | const affectDepthOfReadingPatterns = patterns.filter((pattern) => utils.pattern.isAffectDepthOfReadingPattern(pattern)); 29 | 30 | return utils.pattern.convertPatternsToRe(affectDepthOfReadingPatterns, this.#micromatchOptions); 31 | } 32 | 33 | #filter(basePath: string, entry: Entry, matcher: PartialMatcher, negativeRe: PatternRe[]): boolean { 34 | if (this.#isSkippedByDeep(basePath, entry.path)) { 35 | return false; 36 | } 37 | 38 | if (this.#isSkippedSymbolicLink(entry)) { 39 | return false; 40 | } 41 | 42 | const filepath = utils.path.removeLeadingDotSegment(entry.path); 43 | 44 | if (this.#isSkippedByPositivePatterns(filepath, matcher)) { 45 | return false; 46 | } 47 | 48 | return this.#isSkippedByNegativePatterns(filepath, negativeRe); 49 | } 50 | 51 | #isSkippedByDeep(basePath: string, entryPath: string): boolean { 52 | /** 53 | * Avoid unnecessary depth calculations when it doesn't matter. 54 | */ 55 | if (this.#settings.deep === Number.POSITIVE_INFINITY) { 56 | return false; 57 | } 58 | 59 | return this.#getEntryLevel(basePath, entryPath) >= this.#settings.deep; 60 | } 61 | 62 | #getEntryLevel(basePath: string, entryPath: string): number { 63 | const entryPathDepth = entryPath.split('/').length; 64 | 65 | if (basePath === '') { 66 | return entryPathDepth; 67 | } 68 | 69 | const basePathDepth = basePath.split('/').length; 70 | 71 | return entryPathDepth - basePathDepth; 72 | } 73 | 74 | #isSkippedSymbolicLink(entry: Entry): boolean { 75 | return !this.#settings.followSymbolicLinks && entry.dirent.isSymbolicLink(); 76 | } 77 | 78 | #isSkippedByPositivePatterns(entryPath: string, matcher: PartialMatcher): boolean { 79 | return !this.#settings.baseNameMatch && !matcher.match(entryPath); 80 | } 81 | 82 | #isSkippedByNegativePatterns(entryPath: string, patternsRe: PatternRe[]): boolean { 83 | return !utils.pattern.matchAny(entryPath, patternsRe); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/providers/filters/entry.ts: -------------------------------------------------------------------------------- 1 | import * as utils from '../../utils'; 2 | 3 | import type Settings from '../../settings'; 4 | import type { MicromatchOptions, Entry, EntryFilterFunction, Pattern, PatternRe } from '../../types'; 5 | 6 | interface PatternsRegexSet { 7 | positive: { 8 | all: PatternRe[]; 9 | }; 10 | negative: { 11 | absolute: PatternRe[]; 12 | relative: PatternRe[]; 13 | }; 14 | } 15 | 16 | export default class EntryFilter { 17 | public readonly index = new Map(); 18 | 19 | readonly #settings: Settings; 20 | readonly #micromatchOptions: MicromatchOptions; 21 | 22 | constructor(settings: Settings, micromatchOptions: MicromatchOptions) { 23 | this.#settings = settings; 24 | this.#micromatchOptions = micromatchOptions; 25 | } 26 | 27 | public getFilter(positive: Pattern[], negative: Pattern[]): EntryFilterFunction { 28 | const [absoluteNegative, relativeNegative] = utils.pattern.partitionAbsoluteAndRelative(negative); 29 | 30 | const patterns: PatternsRegexSet = { 31 | positive: { 32 | all: utils.pattern.convertPatternsToRe(positive, this.#micromatchOptions), 33 | }, 34 | negative: { 35 | absolute: utils.pattern.convertPatternsToRe(absoluteNegative, { ...this.#micromatchOptions, dot: true }), 36 | relative: utils.pattern.convertPatternsToRe(relativeNegative, { ...this.#micromatchOptions, dot: true }), 37 | }, 38 | }; 39 | 40 | return (entry) => this.#filter(entry, patterns); 41 | } 42 | 43 | #filter(entry: Entry, pattens: PatternsRegexSet): boolean { 44 | const filepath = utils.path.removeLeadingDotSegment(entry.path); 45 | 46 | if (this.#settings.unique && this.#isDuplicateEntry(filepath)) { 47 | return false; 48 | } 49 | 50 | const isDirectory = entry.dirent.isDirectory(); 51 | 52 | if (this.#onlyFileFilter(isDirectory) || this.#onlyDirectoryFilter(isDirectory)) { 53 | return false; 54 | } 55 | 56 | const isMatched = this.#isMatchToPatternsSet(filepath, pattens, isDirectory); 57 | 58 | if (this.#settings.unique && isMatched) { 59 | this.#createIndexRecord(filepath); 60 | } 61 | 62 | return isMatched; 63 | } 64 | 65 | #isDuplicateEntry(filepath: string): boolean { 66 | return this.index.has(filepath); 67 | } 68 | 69 | #createIndexRecord(filepath: string): void { 70 | this.index.set(filepath, undefined); 71 | } 72 | 73 | #onlyFileFilter(isDirectory: boolean): boolean { 74 | return this.#settings.onlyFiles && isDirectory; 75 | } 76 | 77 | #onlyDirectoryFilter(isDirectory: boolean): boolean { 78 | return this.#settings.onlyDirectories && !isDirectory; 79 | } 80 | 81 | #isMatchToPatternsSet(filepath: string, patterns: PatternsRegexSet, isDirectory: boolean): boolean { 82 | const isMatched = this.#isMatchToPatterns(filepath, patterns.positive.all, isDirectory); 83 | if (!isMatched) { 84 | return false; 85 | } 86 | 87 | const isMatchedByRelativeNegative = this.#isMatchToPatterns(filepath, patterns.negative.relative, isDirectory); 88 | if (isMatchedByRelativeNegative) { 89 | return false; 90 | } 91 | 92 | const isMatchedByAbsoluteNegative = this.#isMatchToAbsoluteNegative(filepath, patterns.negative.absolute, isDirectory); 93 | if (isMatchedByAbsoluteNegative) { 94 | return false; 95 | } 96 | 97 | return true; 98 | } 99 | 100 | #isMatchToAbsoluteNegative(filepath: string, patternsRe: PatternRe[], isDirectory: boolean): boolean { 101 | if (patternsRe.length === 0) { 102 | return false; 103 | } 104 | 105 | const fullpath = utils.path.makeAbsolute(this.#settings.cwd, filepath); 106 | 107 | return this.#isMatchToPatterns(fullpath, patternsRe, isDirectory); 108 | } 109 | 110 | #isMatchToPatterns(filepath: string, patternsRe: PatternRe[], isDirectory: boolean): boolean { 111 | if (patternsRe.length === 0) { 112 | return false; 113 | } 114 | 115 | // Trying to match files and directories by patterns. 116 | const isMatched = utils.pattern.matchAny(filepath, patternsRe); 117 | 118 | // A pattern with a trailling slash can be used for directory matching. 119 | // To apply such pattern, we need to add a tralling slash to the path. 120 | if (!isMatched && isDirectory) { 121 | return utils.pattern.matchAny(`${filepath}/`, patternsRe); 122 | } 123 | 124 | return isMatched; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/providers/filters/error.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | 3 | import { describe, it } from 'mocha'; 4 | 5 | import Settings from '../../settings'; 6 | import * as tests from '../../tests'; 7 | import ErrorFilter from './error'; 8 | 9 | import type { ErrorFilterFunction } from '../../types'; 10 | import type { Options } from '../../settings'; 11 | 12 | function getErrorFilterInstance(options?: Options): ErrorFilter { 13 | const settings = new Settings(options); 14 | 15 | return new ErrorFilter(settings); 16 | } 17 | 18 | function getFilter(options?: Options): ErrorFilterFunction { 19 | return getErrorFilterInstance(options).getFilter(); 20 | } 21 | 22 | describe('Providers → Filters → Error', () => { 23 | describe('Constructor', () => { 24 | it('should create instance of class', () => { 25 | const filter = getErrorFilterInstance(); 26 | 27 | assert.ok(filter instanceof ErrorFilter); 28 | }); 29 | }); 30 | 31 | describe('.getFilter', () => { 32 | it('should return true for ENOENT error', () => { 33 | const filter = getFilter(); 34 | 35 | const actual = filter(tests.errno.getEnoent()); 36 | 37 | assert.ok(actual); 38 | }); 39 | 40 | it('should return true for EPERM error when the `suppressErrors` options is enabled', () => { 41 | const filter = getFilter({ suppressErrors: true }); 42 | 43 | const actual = filter(tests.errno.getEperm()); 44 | 45 | assert.ok(actual); 46 | }); 47 | 48 | it('should return false for EPERM error', () => { 49 | const filter = getFilter(); 50 | 51 | const actual = filter(tests.errno.getEperm()); 52 | 53 | assert.ok(!actual); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/providers/filters/error.ts: -------------------------------------------------------------------------------- 1 | import * as utils from '../../utils'; 2 | 3 | import type Settings from '../../settings'; 4 | import type { ErrnoException, ErrorFilterFunction } from '../../types'; 5 | 6 | export default class ErrorFilter { 7 | readonly #settings: Settings; 8 | 9 | constructor(settings: Settings) { 10 | this.#settings = settings; 11 | } 12 | 13 | public getFilter(): ErrorFilterFunction { 14 | return (error) => this.#isNonFatalError(error); 15 | } 16 | 17 | #isNonFatalError(error: ErrnoException): boolean { 18 | return utils.errno.isEnoentCodeError(error) || this.#settings.suppressErrors; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './async'; 2 | export * from './stream'; 3 | export * from './sync'; 4 | -------------------------------------------------------------------------------- /src/providers/matchers/matcher.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | 3 | import { describe, it } from 'mocha'; 4 | 5 | import * as tests from '../../tests'; 6 | import Settings from '../../settings'; 7 | import Matcher from './matcher'; 8 | 9 | import type { Pattern, MicromatchOptions } from '../../types'; 10 | import type { PatternInfo } from './matcher'; 11 | 12 | class TestMatcher extends Matcher { 13 | public get storage(): PatternInfo[] { 14 | return this._storage; 15 | } 16 | } 17 | 18 | function getMatcher(patterns: Pattern[], options: MicromatchOptions = {}): TestMatcher { 19 | return new TestMatcher(patterns, new Settings(), options); 20 | } 21 | 22 | describe('Providers → Matchers → Matcher', () => { 23 | describe('.storage', () => { 24 | it('should return created storage', () => { 25 | const matcher = getMatcher(['a*', 'a/**/b']); 26 | 27 | const expected: PatternInfo[] = [ 28 | tests.pattern.info() 29 | .section(tests.pattern.segment().dynamic().pattern('a*').build()) 30 | .build(), 31 | tests.pattern.info() 32 | .section(tests.pattern.segment().pattern('a').build()) 33 | .section(tests.pattern.segment().pattern('b').build()) 34 | .build(), 35 | ]; 36 | 37 | const actual = matcher.storage; 38 | 39 | assert.deepStrictEqual(actual, expected); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/providers/matchers/matcher.ts: -------------------------------------------------------------------------------- 1 | import * as utils from '../../utils'; 2 | 3 | import type { MicromatchOptions, Pattern, PatternRe } from '../../types'; 4 | import type Settings from '../../settings'; 5 | 6 | export type PatternSegment = DynamicPatternSegment | StaticPatternSegment; 7 | 8 | interface StaticPatternSegment { 9 | dynamic: false; 10 | pattern: Pattern; 11 | } 12 | 13 | interface DynamicPatternSegment { 14 | dynamic: true; 15 | pattern: Pattern; 16 | patternRe: PatternRe; 17 | } 18 | 19 | export type PatternSection = PatternSegment[]; 20 | 21 | export interface PatternInfo { 22 | /** 23 | * Indicates that the pattern has a globstar (more than a single section). 24 | */ 25 | complete: boolean; 26 | pattern: Pattern; 27 | segments: PatternSegment[]; 28 | sections: PatternSection[]; 29 | } 30 | 31 | export default abstract class Matcher { 32 | protected readonly _storage: PatternInfo[] = []; 33 | 34 | readonly #patterns: string[]; 35 | readonly #settings: Settings; 36 | readonly #micromatchOptions: MicromatchOptions; 37 | 38 | constructor(patterns: Pattern[], settings: Settings, micromatchOptions: MicromatchOptions) { 39 | this.#patterns = patterns; 40 | this.#settings = settings; 41 | this.#micromatchOptions = micromatchOptions; 42 | 43 | this.#fillStorage(); 44 | } 45 | 46 | #fillStorage(): void { 47 | for (const pattern of this.#patterns) { 48 | const segments = this.#getPatternSegments(pattern); 49 | const sections = this.#splitSegmentsIntoSections(segments); 50 | 51 | this._storage.push({ 52 | complete: sections.length <= 1, 53 | pattern, 54 | segments, 55 | sections, 56 | }); 57 | } 58 | } 59 | 60 | #getPatternSegments(pattern: Pattern): PatternSegment[] { 61 | const parts = utils.pattern.getPatternParts(pattern, this.#micromatchOptions); 62 | 63 | return parts.map((part) => { 64 | const dynamic = utils.pattern.isDynamicPattern(part, this.#settings); 65 | 66 | if (!dynamic) { 67 | return { 68 | dynamic: false, 69 | pattern: part, 70 | }; 71 | } 72 | 73 | return { 74 | dynamic: true, 75 | pattern: part, 76 | patternRe: utils.pattern.makeRe(part, this.#micromatchOptions), 77 | }; 78 | }); 79 | } 80 | 81 | #splitSegmentsIntoSections(segments: PatternSegment[]): PatternSection[] { 82 | return utils.array.splitWhen(segments, (segment) => segment.dynamic && utils.pattern.hasGlobStar(segment.pattern)); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/providers/matchers/partial.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | 3 | import { describe, it } from 'mocha'; 4 | 5 | import Settings from '../../settings'; 6 | import Matcher from './partial'; 7 | 8 | import type { Pattern, MicromatchOptions } from '../../types'; 9 | 10 | function getMatcher(patterns: Pattern[], options: MicromatchOptions = {}): Matcher { 11 | return new Matcher(patterns, new Settings(), options); 12 | } 13 | 14 | function assertMatch(patterns: Pattern[], filepath: string): never | void { 15 | const matcher = getMatcher(patterns); 16 | 17 | assert.ok(matcher.match(filepath), `Path "${filepath}" should match: ${patterns.join(', ')}`); 18 | } 19 | 20 | function assertNotMatch(patterns: Pattern[], filepath: string): never | void { 21 | const matcher = getMatcher(patterns); 22 | 23 | assert.ok(!matcher.match(filepath), `Path "${filepath}" should do not match: ${patterns.join(', ')}`); 24 | } 25 | 26 | describe('Providers → Matchers → Partial', () => { 27 | describe('.match', () => { 28 | it('should handle patterns with globstar', () => { 29 | assertMatch(['**'], 'a'); 30 | assertMatch(['**'], './a'); 31 | assertMatch(['**/a'], 'a'); 32 | assertMatch(['**/a'], 'b/a'); 33 | assertMatch(['a/**'], 'a/b'); 34 | assertNotMatch(['a/**'], 'b'); 35 | }); 36 | 37 | it('should do not match the latest segment', () => { 38 | assertMatch(['b/*'], 'b'); 39 | assertNotMatch(['*'], 'a'); 40 | assertNotMatch(['a/*'], 'a/b'); 41 | }); 42 | 43 | it('should trying to match all patterns', () => { 44 | assertMatch(['a/*', 'b/*'], 'b'); 45 | assertMatch(['non-match/b/c', 'a/*/c'], 'a/b'); 46 | assertNotMatch(['non-match/d/c', 'a/b/c'], 'a/d'); 47 | }); 48 | 49 | it('should match a static segment', () => { 50 | assertMatch(['a/b'], 'a'); 51 | assertNotMatch(['b/b'], 'a'); 52 | }); 53 | 54 | it('should match a dynamic segment', () => { 55 | assertMatch(['*/b'], 'a'); 56 | assertMatch(['{a,b}/*'], 'a'); 57 | assertNotMatch(['{a,b}/*'], 'c'); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/providers/matchers/partial.ts: -------------------------------------------------------------------------------- 1 | import Matcher from './matcher'; 2 | 3 | export default class PartialMatcher extends Matcher { 4 | public match(filepath: string): boolean { 5 | const parts = filepath.split('/'); 6 | const levels = parts.length; 7 | 8 | const patterns = this._storage.filter((info) => !info.complete || info.segments.length > levels); 9 | 10 | for (const pattern of patterns) { 11 | const section = pattern.sections[0]; 12 | 13 | /** 14 | * In this case, the pattern has a globstar and we must read all directories unconditionally, 15 | * but only if the level has reached the end of the first group. 16 | * 17 | * fixtures/{a,b}/** 18 | * ^ true/false ^ always true 19 | */ 20 | if (!pattern.complete && levels > section.length) { 21 | return true; 22 | } 23 | 24 | const match = parts.every((part, index) => { 25 | const segment = pattern.segments[index]; 26 | 27 | if (segment.dynamic && segment.patternRe.test(part)) { 28 | return true; 29 | } 30 | 31 | if (!segment.dynamic && segment.pattern === part) { 32 | return true; 33 | } 34 | 35 | return false; 36 | }); 37 | 38 | if (match) { 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/providers/provider.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | import * as path from 'node:path'; 3 | 4 | import { describe, it } from 'mocha'; 5 | 6 | import Settings from '../settings'; 7 | import * as tests from '../tests'; 8 | import { Provider } from './provider'; 9 | 10 | import type { Task } from '../managers/tasks'; 11 | import type { Options } from '../settings'; 12 | import type { Dictionary, MicromatchOptions, ReaderOptions } from '../types'; 13 | 14 | class TestProvider extends Provider { 15 | public read(): Dictionary[] { 16 | return []; 17 | } 18 | 19 | public getRootDirectory(task: Task): string { 20 | return this._getRootDirectory(task); 21 | } 22 | 23 | public getReaderOptions(task: Task): ReaderOptions { 24 | return this._getReaderOptions(task); 25 | } 26 | 27 | public getMicromatchOptions(): MicromatchOptions { 28 | return this._getMicromatchOptions(); 29 | } 30 | } 31 | 32 | function getProvider(options?: Options): TestProvider { 33 | const settings = new Settings(options); 34 | 35 | return new TestProvider(settings); 36 | } 37 | 38 | describe('Providers → Provider', () => { 39 | describe('Constructor', () => { 40 | it('should create instance of class', () => { 41 | const provider = getProvider(); 42 | 43 | assert.ok(provider instanceof Provider); 44 | }); 45 | }); 46 | 47 | describe('.getRootDirectory', () => { 48 | it('should return root directory for reader with global base (.)', () => { 49 | const provider = getProvider(); 50 | const task = tests.task.builder().base('.').build(); 51 | 52 | const expected = process.cwd(); 53 | 54 | const actual = provider.getRootDirectory(task); 55 | 56 | assert.strictEqual(actual, expected); 57 | }); 58 | 59 | it('should return root directory for reader with non-global base (fixtures)', () => { 60 | const provider = getProvider(); 61 | const task = tests.task.builder().base('root').build(); 62 | 63 | const expected = path.join(process.cwd(), 'root'); 64 | 65 | const actual = provider.getRootDirectory(task); 66 | 67 | assert.strictEqual(actual, expected); 68 | }); 69 | }); 70 | 71 | describe('.getReaderOptions', () => { 72 | it('should return options for reader with global base (.)', () => { 73 | const settings = new Settings(); 74 | const provider = getProvider(settings); 75 | const task = tests.task.builder().base('.').positive('*').build(); 76 | 77 | const actual = provider.getReaderOptions(task); 78 | 79 | assert.strictEqual(actual.basePath, ''); 80 | assert.strictEqual(typeof actual.deepFilter, 'function'); 81 | assert.strictEqual(typeof actual.entryFilter, 'function'); 82 | assert.strictEqual(typeof actual.errorFilter, 'function'); 83 | assert.strictEqual(actual.followSymbolicLinks, true); 84 | assert.strictEqual(typeof actual.fs, 'object'); 85 | assert.ok(!actual.stats); 86 | assert.ok(actual.throwErrorOnBrokenSymbolicLink === false); 87 | assert.strictEqual(typeof actual.transform, 'function'); 88 | assert.strictEqual(actual.signal, undefined); 89 | }); 90 | 91 | it('should return options for reader with non-global base', () => { 92 | const provider = getProvider(); 93 | const task = tests.task.builder().base('root').positive('*').build(); 94 | 95 | const actual = provider.getReaderOptions(task); 96 | 97 | assert.strictEqual(actual.basePath, 'root'); 98 | }); 99 | }); 100 | 101 | describe('.getMicromatchOptions', () => { 102 | it('should return options for micromatch', () => { 103 | const provider = getProvider(); 104 | 105 | const expected: MicromatchOptions = { 106 | dot: false, 107 | matchBase: false, 108 | nobrace: false, 109 | nocase: false, 110 | noext: false, 111 | noglobstar: false, 112 | posix: true, 113 | strictSlashes: false, 114 | }; 115 | 116 | const actual = provider.getMicromatchOptions(); 117 | 118 | assert.deepStrictEqual(actual, expected); 119 | }); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /src/providers/provider.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | import DeepFilter from './filters/deep'; 4 | import EntryFilter from './filters/entry'; 5 | import ErrorFilter from './filters/error'; 6 | import EntryTransformer from './transformers/entry'; 7 | 8 | import type Settings from '../settings'; 9 | import type { MicromatchOptions, ReaderOptions } from '../types'; 10 | import type { Task } from '../managers/tasks'; 11 | 12 | export abstract class Provider { 13 | public readonly errorFilter: ErrorFilter; 14 | public readonly entryFilter: EntryFilter; 15 | public readonly deepFilter: DeepFilter; 16 | public readonly entryTransformer: EntryTransformer; 17 | 18 | readonly #settings: Settings; 19 | 20 | constructor(settings: Settings) { 21 | this.#settings = settings; 22 | 23 | const micromatchOptions = this._getMicromatchOptions(); 24 | 25 | this.errorFilter = new ErrorFilter(settings); 26 | this.entryFilter = new EntryFilter(settings, micromatchOptions); 27 | this.deepFilter = new DeepFilter(settings, micromatchOptions); 28 | this.entryTransformer = new EntryTransformer(settings); 29 | } 30 | 31 | public abstract read(_task: Task): T; 32 | 33 | protected _getRootDirectory(task: Task): string { 34 | return path.resolve(this.#settings.cwd, task.base); 35 | } 36 | 37 | protected _getReaderOptions(task: Task): ReaderOptions { 38 | const basePath = task.base === '.' ? '' : task.base; 39 | 40 | return { 41 | basePath, 42 | pathSegmentSeparator: '/', 43 | deepFilter: this.deepFilter.getFilter(basePath, task.positive, task.negative), 44 | entryFilter: this.entryFilter.getFilter(task.positive, task.negative), 45 | errorFilter: this.errorFilter.getFilter(), 46 | followSymbolicLinks: this.#settings.followSymbolicLinks, 47 | fs: this.#settings.fs, 48 | stats: this.#settings.stats, 49 | throwErrorOnBrokenSymbolicLink: this.#settings.throwErrorOnBrokenSymbolicLink, 50 | transform: this.entryTransformer.getTransformer(), 51 | signal: this.#settings.signal, 52 | }; 53 | } 54 | 55 | protected _getMicromatchOptions(): MicromatchOptions { 56 | return { 57 | dot: this.#settings.dot, 58 | matchBase: this.#settings.baseNameMatch, 59 | nobrace: !this.#settings.braceExpansion, 60 | nocase: !this.#settings.caseSensitiveMatch, 61 | noext: !this.#settings.extglob, 62 | noglobstar: !this.#settings.globstar, 63 | posix: true, 64 | strictSlashes: false, 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/providers/stream.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | import { PassThrough } from 'node:stream'; 3 | 4 | import * as sinon from 'sinon'; 5 | import { describe, it } from 'mocha'; 6 | 7 | import Settings from '../settings'; 8 | import * as tests from '../tests'; 9 | import { ProviderStream } from './stream'; 10 | import { ReaderStream } from '../readers'; 11 | 12 | import type { IReaderStream } from '../readers'; 13 | import type { Options } from '../settings'; 14 | import type { Entry, EntryItem, ErrnoException } from '../types'; 15 | import type { Task } from '../managers/tasks'; 16 | 17 | type StubbedReaderStream = sinon.SinonStubbedInstance; 18 | 19 | class TestProvider extends ProviderStream { 20 | public readonly reader: StubbedReaderStream; 21 | 22 | constructor( 23 | options?: Options, 24 | reader: StubbedReaderStream = sinon.createStubInstance(ReaderStream), 25 | ) { 26 | super(reader, new Settings(options)); 27 | 28 | this.reader = reader; 29 | } 30 | } 31 | 32 | function getProvider(options?: Options): TestProvider { 33 | return new TestProvider(options); 34 | } 35 | 36 | function getEntries(provider: TestProvider, task: Task, entry: Entry): Promise { 37 | const reader = new PassThrough({ objectMode: true }); 38 | 39 | provider.reader.dynamic.returns(reader); 40 | provider.reader.static.returns(reader); 41 | 42 | reader.push(entry); 43 | reader.push(null); 44 | 45 | return new Promise((resolve, reject) => { 46 | const items: EntryItem[] = []; 47 | 48 | const api = provider.read(task); 49 | 50 | api.on('data', (item: EntryItem) => items.push(item)); 51 | api.once('error', reject); 52 | api.once('end', () => { 53 | resolve(items); 54 | }); 55 | }); 56 | } 57 | 58 | describe('Providers → ProviderStream', () => { 59 | describe('Constructor', () => { 60 | it('should create instance of class', () => { 61 | const provider = getProvider(); 62 | 63 | assert.ok(provider instanceof ProviderStream); 64 | }); 65 | }); 66 | 67 | describe('.read', () => { 68 | it('should return entries for dynamic entries', async () => { 69 | const provider = getProvider(); 70 | const task = tests.task.builder().base('.').positive('*').build(); 71 | const entry = tests.entry.builder().path('root/file.txt').file().build(); 72 | 73 | const expected = ['root/file.txt']; 74 | 75 | const actual = await getEntries(provider, task, entry); 76 | 77 | assert.strictEqual(provider.reader.dynamic.callCount, 1); 78 | assert.deepStrictEqual(actual, expected); 79 | }); 80 | 81 | it('should return entries for static entries', async () => { 82 | const provider = getProvider(); 83 | const task = tests.task.builder().base('.').static().positive('root/file.txt').build(); 84 | const entry = tests.entry.builder().path('root/file.txt').file().build(); 85 | 86 | const expected = ['root/file.txt']; 87 | 88 | const actual = await getEntries(provider, task, entry); 89 | 90 | assert.strictEqual(provider.reader.static.callCount, 1); 91 | assert.deepStrictEqual(actual, expected); 92 | }); 93 | 94 | it('should emit error to the transform stream', (done) => { 95 | const provider = getProvider(); 96 | const task = tests.task.builder().base('.').positive('*').build(); 97 | const stream = new PassThrough({ 98 | read(): void { 99 | stream.emit('error', tests.errno.getEnoent()); 100 | }, 101 | }); 102 | 103 | provider.reader.dynamic.returns(stream); 104 | 105 | const actual = provider.read(task); 106 | 107 | actual.once('error', (error: ErrnoException) => { 108 | assert.strictEqual(error.code, 'ENOENT'); 109 | done(); 110 | }); 111 | }); 112 | 113 | it('should destroy source stream when the destination stream is closed', (done) => { 114 | const provider = getProvider(); 115 | const task = tests.task.builder().base('.').positive('*').build(); 116 | const stream = new PassThrough(); 117 | 118 | provider.reader.dynamic.returns(stream); 119 | 120 | const actual = provider.read(task); 121 | 122 | actual.once('close', () => { 123 | assert.ok(stream.destroyed); 124 | 125 | done(); 126 | }); 127 | 128 | actual.emit('close'); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /src/providers/stream.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'node:stream'; 2 | 3 | import { Provider } from './provider'; 4 | 5 | import type { IReaderStream } from '../readers'; 6 | import type Settings from '../settings'; 7 | import type { Task } from '../managers/tasks'; 8 | import type { Entry, ErrnoException, ReaderOptions } from '../types'; 9 | 10 | export class ProviderStream extends Provider { 11 | readonly #reader: IReaderStream; 12 | 13 | constructor(reader: IReaderStream, settings: Settings) { 14 | super(settings); 15 | 16 | this.#reader = reader; 17 | } 18 | 19 | public read(task: Task): Readable { 20 | const root = this._getRootDirectory(task); 21 | const options = this._getReaderOptions(task); 22 | 23 | const source = this.api(root, task, options); 24 | const destination = new Readable({ objectMode: true, read: () => { /* noop */ } }); 25 | 26 | source 27 | .once('error', (error: ErrnoException) => destination.emit('error', error)) 28 | .on('data', (entry: Entry) => destination.emit('data', options.transform(entry))) 29 | .once('end', () => destination.emit('end')); 30 | 31 | destination 32 | .once('close', () => source.destroy()); 33 | 34 | return destination; 35 | } 36 | 37 | public api(root: string, task: Task, options: ReaderOptions): Readable { 38 | if (task.dynamic) { 39 | return this.#reader.dynamic(root, options); 40 | } 41 | 42 | return this.#reader.static(task.patterns, options); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/providers/sync.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | 3 | import * as sinon from 'sinon'; 4 | import { describe, it } from 'mocha'; 5 | 6 | import { ReaderSync } from '../readers'; 7 | import Settings from '../settings'; 8 | import * as tests from '../tests'; 9 | import { ProviderSync } from './sync'; 10 | 11 | import type { IReaderSync } from '../readers'; 12 | import type { Options } from '../settings'; 13 | 14 | type StubbedReaderSync = sinon.SinonStubbedInstance; 15 | 16 | class TestProvider extends ProviderSync { 17 | public readonly reader: StubbedReaderSync; 18 | 19 | constructor( 20 | options?: Options, 21 | reader: StubbedReaderSync = sinon.createStubInstance(ReaderSync), 22 | ) { 23 | super(reader, new Settings(options)); 24 | 25 | this.reader = reader; 26 | } 27 | } 28 | 29 | function getProvider(options?: Options): TestProvider { 30 | return new TestProvider(options); 31 | } 32 | 33 | describe('Providers → ProviderSync', () => { 34 | describe('Constructor', () => { 35 | it('should create instance of class', () => { 36 | const provider = getProvider(); 37 | 38 | assert.ok(provider instanceof ProviderSync); 39 | }); 40 | }); 41 | 42 | describe('.read', () => { 43 | it('should return entries for dynamic task', () => { 44 | const provider = getProvider(); 45 | const task = tests.task.builder().base('.').positive('*').build(); 46 | const entry = tests.entry.builder().path('root/file.txt').file().build(); 47 | 48 | provider.reader.dynamic.returns([entry]); 49 | 50 | const expected = ['root/file.txt']; 51 | 52 | const actual = provider.read(task); 53 | 54 | assert.strictEqual(provider.reader.dynamic.callCount, 1); 55 | assert.deepStrictEqual(actual, expected); 56 | }); 57 | 58 | it('should return entries for static task', () => { 59 | const provider = getProvider(); 60 | const task = tests.task.builder().base('.').static().positive('root/file.txt').build(); 61 | const entry = tests.entry.builder().path('root/file.txt').file().build(); 62 | 63 | provider.reader.static.returns([entry]); 64 | 65 | const expected = ['root/file.txt']; 66 | 67 | const actual = provider.read(task); 68 | 69 | assert.strictEqual(provider.reader.static.callCount, 1); 70 | assert.deepStrictEqual(actual, expected); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/providers/sync.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from './provider'; 2 | 3 | import type { IReaderSync } from '../readers'; 4 | import type Settings from '../settings'; 5 | import type { Task } from '../managers/tasks'; 6 | import type { Entry, EntryItem, ReaderOptions } from '../types'; 7 | 8 | export class ProviderSync extends Provider { 9 | readonly #reader: IReaderSync; 10 | 11 | constructor(reader: IReaderSync, settings: Settings) { 12 | super(settings); 13 | 14 | this.#reader = reader; 15 | } 16 | 17 | public read(task: Task): EntryItem[] { 18 | const root = this._getRootDirectory(task); 19 | const options = this._getReaderOptions(task); 20 | 21 | const entries = this.api(root, task, options); 22 | 23 | return entries.map((entry) => options.transform(entry)); 24 | } 25 | 26 | public api(root: string, task: Task, options: ReaderOptions): Entry[] { 27 | if (task.dynamic) { 28 | return this.#reader.dynamic(root, options); 29 | } 30 | 31 | return this.#reader.static(task.patterns, options); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/providers/transformers/entry.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | import * as path from 'node:path'; 3 | 4 | import { describe, it } from 'mocha'; 5 | 6 | import Settings from '../../settings'; 7 | import * as tests from '../../tests'; 8 | import EntryTransformer from './entry'; 9 | 10 | import type { EntryTransformerFunction } from '../../types'; 11 | import type { Options } from '../../settings'; 12 | 13 | function getEntryTransformer(options?: Options): EntryTransformer { 14 | return new EntryTransformer(new Settings(options)); 15 | } 16 | 17 | function getTransformer(options?: Options): EntryTransformerFunction { 18 | return getEntryTransformer(options).getTransformer(); 19 | } 20 | 21 | describe('Providers → Transformers → Entry', () => { 22 | describe('Constructor', () => { 23 | it('should create instance of class', () => { 24 | const filter = getEntryTransformer(); 25 | 26 | assert.ok(filter instanceof EntryTransformer); 27 | }); 28 | }); 29 | 30 | describe('.getTransformer', () => { 31 | it('should return transformed entry as string when options is not provided', () => { 32 | const transformer = getTransformer(); 33 | const entry = tests.entry.builder().path('root/file.txt').file().build(); 34 | 35 | const expected = 'root/file.txt'; 36 | 37 | const actual = transformer(entry); 38 | 39 | assert.strictEqual(actual, expected); 40 | }); 41 | 42 | it('should return transformed entry as object when the `objectMode` option is enabled', () => { 43 | const transformer = getTransformer({ objectMode: true }); 44 | const entry = tests.entry.builder().path('root/file.txt').file().build(); 45 | 46 | const expected = entry; 47 | 48 | const actual = transformer(entry); 49 | 50 | assert.deepStrictEqual(actual, expected); 51 | }); 52 | 53 | it('should return transformed entry as object when the `stats` option is enabled', () => { 54 | const transformer = getTransformer({ stats: true }); 55 | const entry = tests.entry.builder().path('root/file.txt').file().stats().build(); 56 | 57 | const expected = entry; 58 | 59 | const actual = transformer(entry); 60 | 61 | assert.deepStrictEqual(actual, expected); 62 | }); 63 | 64 | it('should return entry with absolute filepath when the `absolute` option is enabled', () => { 65 | const transformer = getTransformer({ absolute: true }); 66 | const entry = tests.entry.builder().path('root/file.txt').file().build(); 67 | 68 | const expected = path.join(process.cwd(), 'root', 'file.txt'); 69 | 70 | const actual = transformer(entry); 71 | 72 | assert.strictEqual(actual, expected); 73 | }); 74 | 75 | it('should return entry with trailing slash when the `markDirectories` is enabled', () => { 76 | const transformer = getTransformer({ markDirectories: true }); 77 | const entry = tests.entry.builder().path('root/directory').directory().build(); 78 | 79 | const expected = 'root/directory/'; 80 | 81 | const actual = transformer(entry); 82 | 83 | assert.strictEqual(actual, expected); 84 | }); 85 | 86 | it('should return correct entry when the `absolute` and `markDirectories` options is enabled', () => { 87 | const transformer = getTransformer({ absolute: true, markDirectories: true }); 88 | const entry = tests.entry.builder().path('root/directory').directory().build(); 89 | 90 | const expected = path.join(process.cwd(), 'root', 'directory', '/'); 91 | 92 | const actual = transformer(entry); 93 | 94 | assert.strictEqual(actual, expected); 95 | }); 96 | 97 | it('should do not mutate the entry when the `markDirectories` option is enabled', () => { 98 | const transformer = getTransformer({ markDirectories: true }); 99 | const entry = tests.entry.builder().path('root/directory').directory().build(); 100 | 101 | const actual = transformer(entry); 102 | 103 | assert.notStrictEqual(actual, entry.path); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /src/providers/transformers/entry.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | import * as utils from '../../utils'; 4 | 5 | import type Settings from '../../settings'; 6 | import type { Entry, EntryItem, EntryTransformerFunction } from '../../types'; 7 | 8 | export default class EntryTransformer { 9 | readonly #settings: Settings; 10 | readonly #pathSeparatorSymbol: string; 11 | 12 | constructor(settings: Settings) { 13 | this.#settings = settings; 14 | 15 | this.#pathSeparatorSymbol = this.#getPathSeparatorSymbol(); 16 | } 17 | 18 | public getTransformer(): EntryTransformerFunction { 19 | return (entry) => this.#transform(entry); 20 | } 21 | 22 | #transform(entry: Entry): EntryItem { 23 | let filepath = entry.path; 24 | 25 | if (this.#settings.absolute) { 26 | filepath = utils.path.makeAbsolute(this.#settings.cwd, filepath); 27 | filepath = utils.string.flatHeavilyConcatenatedString(filepath); 28 | } 29 | 30 | if (this.#settings.markDirectories && entry.dirent.isDirectory()) { 31 | filepath += this.#pathSeparatorSymbol; 32 | } 33 | 34 | if (!this.#settings.objectMode) { 35 | return filepath; 36 | } 37 | 38 | return { 39 | ...entry, 40 | path: filepath, 41 | }; 42 | } 43 | 44 | #getPathSeparatorSymbol(): string { 45 | return this.#settings.absolute ? path.sep : '/'; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/readers/async.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | import { PassThrough } from 'node:stream'; 3 | 4 | import * as sinon from 'sinon'; 5 | import { describe, it } from 'mocha'; 6 | 7 | import Settings from '../settings'; 8 | import * as tests from '../tests'; 9 | import { ReaderAsync } from './async'; 10 | import { ReaderStream } from './stream'; 11 | 12 | import type { Options } from '../settings'; 13 | import type { ReaderOptions } from '../types'; 14 | import type * as fsWalk from '@nodelib/fs.walk'; 15 | 16 | type WalkSignature = typeof fsWalk.walk; 17 | 18 | class TestReader extends ReaderAsync { 19 | protected override _walkAsync: WalkSignature = sinon.stub() as unknown as WalkSignature; 20 | protected override _readerStream: ReaderStream = sinon.createStubInstance(ReaderStream) as unknown as ReaderStream; 21 | 22 | constructor(options?: Options) { 23 | super(new Settings(options)); 24 | } 25 | 26 | public get walkAsync(): sinon.SinonStub { 27 | return this._walkAsync as unknown as sinon.SinonStub; 28 | } 29 | 30 | public get readerStream(): sinon.SinonStubbedInstance { 31 | return this._readerStream as unknown as sinon.SinonStubbedInstance; 32 | } 33 | } 34 | 35 | function getReader(options?: Options): TestReader { 36 | return new TestReader(options); 37 | } 38 | 39 | function getReaderOptions(options: Partial = {}): ReaderOptions { 40 | return { ...options } as unknown as ReaderOptions; 41 | } 42 | 43 | describe('Readers → ReaderAsync', () => { 44 | describe('Constructor', () => { 45 | it('should create instance of class', () => { 46 | const reader = getReader(); 47 | 48 | assert.ok(reader instanceof TestReader); 49 | }); 50 | }); 51 | 52 | describe('.dynamic', () => { 53 | it('should call fs.walk method', async () => { 54 | const reader = getReader(); 55 | const readerOptions = getReaderOptions(); 56 | 57 | reader.walkAsync.yields(null, []); 58 | 59 | await reader.dynamic('root', readerOptions); 60 | 61 | assert.ok(reader.walkAsync.called); 62 | }); 63 | }); 64 | 65 | describe('.static', () => { 66 | it('should call stream reader method', async () => { 67 | const entry = tests.entry.builder().path('root/file.txt').build(); 68 | 69 | const reader = getReader(); 70 | const readerOptions = getReaderOptions(); 71 | const readerStream = new PassThrough({ objectMode: true }); 72 | 73 | readerStream.push(entry); 74 | readerStream.push(null); 75 | 76 | reader.readerStream.static.returns(readerStream); 77 | 78 | await reader.static(['a.txt'], readerOptions); 79 | 80 | assert.ok(reader.readerStream.static.called); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/readers/async.ts: -------------------------------------------------------------------------------- 1 | import * as fsWalk from '@nodelib/fs.walk'; 2 | 3 | import { Reader } from './reader'; 4 | import { ReaderStream } from './stream'; 5 | 6 | import type Settings from '../settings'; 7 | import type { Entry, ReaderOptions, Pattern } from '../types'; 8 | 9 | export interface IReaderAsync { 10 | dynamic: (root: string, options: ReaderOptions) => Promise; 11 | static: (patterns: Pattern[], options: ReaderOptions) => Promise; 12 | } 13 | 14 | export class ReaderAsync extends Reader> implements IReaderAsync { 15 | protected _walkAsync: typeof fsWalk.walk = fsWalk.walk; 16 | protected _readerStream: ReaderStream; 17 | 18 | constructor(settings: Settings) { 19 | super(settings); 20 | 21 | this._readerStream = new ReaderStream(settings); 22 | } 23 | 24 | public dynamic(root: string, options: ReaderOptions): Promise { 25 | return new Promise((resolve, reject) => { 26 | this._walkAsync(root, options, (error, entries) => { 27 | if (error === null) { 28 | resolve(entries); 29 | } else { 30 | reject(error); 31 | } 32 | }); 33 | }); 34 | } 35 | 36 | public async static(patterns: Pattern[], options: ReaderOptions): Promise { 37 | const entries: Entry[] = []; 38 | 39 | for await (const entry of this._readerStream.static(patterns, options)) { 40 | entries.push(entry as Entry); 41 | } 42 | 43 | return entries; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/readers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './async'; 2 | export * from './stream'; 3 | export * from './sync'; 4 | -------------------------------------------------------------------------------- /src/readers/reader.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | import * as path from 'node:path'; 3 | 4 | import { Stats, StatsMode } from '@nodelib/fs.macchiato'; 5 | import { describe, it } from 'mocha'; 6 | 7 | import Settings from '../settings'; 8 | import { Reader } from './reader'; 9 | 10 | import type { Options } from '../settings'; 11 | import type { Entry, FsStats, Pattern } from '../types'; 12 | 13 | class TestReader extends Reader { 14 | constructor(options?: Options) { 15 | super(new Settings(options)); 16 | } 17 | 18 | public dynamic(): never[] { 19 | return []; 20 | } 21 | 22 | public static(): never[] { 23 | return []; 24 | } 25 | 26 | public getFullEntryPath(filepath: string): string { 27 | return this._getFullEntryPath(filepath); 28 | } 29 | 30 | public makeEntry(stats: FsStats, pattern: Pattern): Entry { 31 | return this._makeEntry(stats, pattern); 32 | } 33 | } 34 | 35 | function getReader(options?: Options): TestReader { 36 | return new TestReader(options); 37 | } 38 | 39 | describe('Readers → Reader', () => { 40 | describe('Constructor', () => { 41 | it('should create instance of class', () => { 42 | const reader = getReader(); 43 | 44 | assert.ok(reader instanceof TestReader); 45 | }); 46 | }); 47 | 48 | describe('.getFullEntryPath', () => { 49 | it('should return path to entry', () => { 50 | const reader = getReader(); 51 | 52 | const expected = path.join(process.cwd(), 'config.json'); 53 | 54 | const actual = reader.getFullEntryPath('config.json'); 55 | 56 | assert.strictEqual(actual, expected); 57 | }); 58 | }); 59 | 60 | describe('.makeEntry', () => { 61 | it('should return created entry', () => { 62 | const reader = getReader(); 63 | const pattern = 'config.json'; 64 | const stats = new Stats({ mode: StatsMode.File }); 65 | 66 | const actual = reader.makeEntry(stats, pattern); 67 | 68 | assert.strictEqual(actual.name, pattern); 69 | assert.strictEqual(actual.path, pattern); 70 | assert.ok(actual.dirent.isFile()); 71 | }); 72 | 73 | it('should return created entry with fs.Stats', () => { 74 | const reader = getReader({ stats: true }); 75 | const pattern = 'config.json'; 76 | 77 | const actual = reader.makeEntry(new Stats(), pattern); 78 | 79 | assert.ok(actual.stats); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/readers/reader.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | import * as fsStat from '@nodelib/fs.stat'; 4 | 5 | import * as utils from '../utils'; 6 | 7 | import type Settings from '../settings'; 8 | import type { Entry, ErrnoException, FsStats, Pattern, ReaderOptions } from '../types'; 9 | 10 | export abstract class Reader { 11 | protected readonly _fsStatSettings: fsStat.Settings; 12 | 13 | readonly #settings: Settings; 14 | 15 | constructor(settings: Settings) { 16 | this.#settings = settings; 17 | 18 | this._fsStatSettings = new fsStat.Settings({ 19 | followSymbolicLink: settings.followSymbolicLinks, 20 | fs: settings.fs, 21 | throwErrorOnBrokenSymbolicLink: settings.throwErrorOnBrokenSymbolicLink, 22 | }); 23 | } 24 | 25 | public abstract dynamic(root: string, options: ReaderOptions): T; 26 | public abstract static(patterns: Pattern[], options: ReaderOptions): T; 27 | 28 | protected _getFullEntryPath(filepath: string): string { 29 | return path.resolve(this.#settings.cwd, filepath); 30 | } 31 | 32 | protected _makeEntry(stats: FsStats, pattern: Pattern): Entry { 33 | const entry: Entry = { 34 | name: pattern, 35 | path: pattern, 36 | dirent: utils.fs.createDirentFromStats(pattern, stats), 37 | }; 38 | 39 | if (this.#settings.stats) { 40 | entry.stats = stats; 41 | } 42 | 43 | return entry; 44 | } 45 | 46 | protected _isFatalError(error: ErrnoException): boolean { 47 | return !utils.errno.isEnoentCodeError(error) && !this.#settings.suppressErrors; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/readers/stream.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | 3 | import { Stats } from '@nodelib/fs.macchiato'; 4 | import * as sinon from 'sinon'; 5 | import { describe, it } from 'mocha'; 6 | 7 | import Settings from '../settings'; 8 | import * as tests from '../tests'; 9 | import { ReaderStream } from './stream'; 10 | 11 | import type { Options } from '../settings'; 12 | import type { Entry, ErrnoException, ReaderOptions } from '../types'; 13 | import type * as fsWalk from '@nodelib/fs.walk'; 14 | import type * as fsStat from '@nodelib/fs.stat'; 15 | 16 | type WalkSignature = typeof fsWalk.walkStream; 17 | type StatSignature = typeof fsStat.stat; 18 | 19 | class TestReader extends ReaderStream { 20 | protected override _walkStream: WalkSignature = sinon.stub() as unknown as WalkSignature; 21 | protected override _stat: StatSignature = sinon.stub() as unknown as StatSignature; 22 | 23 | constructor(options?: Options) { 24 | super(new Settings(options)); 25 | } 26 | 27 | public get walkStream(): sinon.SinonStub { 28 | return this._walkStream as unknown as sinon.SinonStub; 29 | } 30 | 31 | public get stat(): sinon.SinonStub { 32 | return this._stat as unknown as sinon.SinonStub; 33 | } 34 | } 35 | 36 | function getReader(options?: Options): TestReader { 37 | return new TestReader(options); 38 | } 39 | 40 | function getReaderOptions(options: Partial = {}): ReaderOptions { 41 | return { ...options } as unknown as ReaderOptions; 42 | } 43 | 44 | describe('Readers → ReaderStream', () => { 45 | describe('Constructor', () => { 46 | it('should create instance of class', () => { 47 | const reader = getReader(); 48 | 49 | assert.ok(reader instanceof TestReader); 50 | }); 51 | }); 52 | 53 | describe('.dynamic', () => { 54 | it('should call fs.walk method', () => { 55 | const reader = getReader(); 56 | const readerOptions = getReaderOptions(); 57 | 58 | reader.dynamic('root', readerOptions); 59 | 60 | assert.ok(reader.walkStream.called); 61 | }); 62 | }); 63 | 64 | describe('.static', () => { 65 | it('should return entries', (done) => { 66 | const reader = getReader(); 67 | const readerOptions = getReaderOptions({ entryFilter: () => true }); 68 | 69 | reader.stat.onFirstCall().yields(null, new Stats()); 70 | reader.stat.onSecondCall().yields(null, new Stats()); 71 | 72 | const entries: Entry[] = []; 73 | 74 | const stream = reader.static(['a.txt', 'b.txt'], readerOptions); 75 | 76 | stream.on('data', (entry: Entry) => entries.push(entry)); 77 | stream.once('end', () => { 78 | assert.strictEqual(entries[0].name, 'a.txt'); 79 | assert.strictEqual(entries[1].name, 'b.txt'); 80 | done(); 81 | }); 82 | }); 83 | 84 | it('should throw an error when the filter does not suppress the error', (done) => { 85 | const reader = getReader(); 86 | const readerOptions = getReaderOptions({ 87 | errorFilter: () => false, 88 | entryFilter: () => true, 89 | }); 90 | 91 | reader.stat.onFirstCall().yields(tests.errno.getEperm()); 92 | reader.stat.onSecondCall().yields(null, new Stats()); 93 | 94 | const entries: Entry[] = []; 95 | 96 | const stream = reader.static(['a.txt', 'b.txt'], readerOptions); 97 | 98 | stream.on('data', (entry: Entry) => entries.push(entry)); 99 | stream.once('error', (error: ErrnoException) => { 100 | assert.strictEqual(error.code, 'EPERM'); 101 | done(); 102 | }); 103 | }); 104 | 105 | it('should do not throw an error when the filter suppress the error', (done) => { 106 | const reader = getReader(); 107 | const readerOptions = getReaderOptions({ 108 | errorFilter: () => true, 109 | entryFilter: () => true, 110 | }); 111 | 112 | reader.stat.onFirstCall().yields(tests.errno.getEnoent()); 113 | reader.stat.onSecondCall().yields(null, new Stats()); 114 | 115 | const entries: Entry[] = []; 116 | 117 | const stream = reader.static(['a.txt', 'b.txt'], readerOptions); 118 | 119 | stream.on('data', (entry: Entry) => entries.push(entry)); 120 | stream.once('end', () => { 121 | assert.strictEqual(entries.length, 1); 122 | assert.strictEqual(entries[0].name, 'b.txt'); 123 | done(); 124 | }); 125 | }); 126 | 127 | it('should do not include entry when the filter excludes it', (done) => { 128 | const reader = getReader(); 129 | const readerOptions = getReaderOptions({ entryFilter: () => false }); 130 | 131 | reader.stat.yields(null, new Stats()); 132 | 133 | const entries: Entry[] = []; 134 | 135 | const stream = reader.static(['a.txt'], readerOptions); 136 | 137 | stream.on('data', (entry: Entry) => entries.push(entry)); 138 | stream.once('end', () => { 139 | assert.strictEqual(entries.length, 0); 140 | done(); 141 | }); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/readers/stream.ts: -------------------------------------------------------------------------------- 1 | import { PassThrough } from 'node:stream'; 2 | 3 | import * as fsStat from '@nodelib/fs.stat'; 4 | import * as fsWalk from '@nodelib/fs.walk'; 5 | 6 | import { Reader } from './reader'; 7 | 8 | import type { Entry, ErrnoException, FsStats, Pattern, ReaderOptions } from '../types'; 9 | import type { Readable } from 'node:stream'; 10 | 11 | export interface IReaderStream { 12 | dynamic: (root: string, options: ReaderOptions) => Readable; 13 | static: (patterns: Pattern[], options: ReaderOptions) => Readable; 14 | } 15 | 16 | export class ReaderStream extends Reader implements IReaderStream { 17 | protected _walkStream: typeof fsWalk.walkStream = fsWalk.walkStream; 18 | protected _stat: typeof fsStat.stat = fsStat.stat; 19 | 20 | public dynamic(root: string, options: ReaderOptions): Readable { 21 | return this._walkStream(root, options); 22 | } 23 | 24 | public static(patterns: Pattern[], options: ReaderOptions): Readable { 25 | const filepaths = patterns.map((pattern) => this._getFullEntryPath(pattern)); 26 | 27 | const stream = new PassThrough({ objectMode: true, signal: options.signal }); 28 | 29 | stream._write = (index: number, _enc, done) => { 30 | this.#getEntry(filepaths[index], patterns[index], options) 31 | .then((entry) => { 32 | if (entry !== null && options.entryFilter(entry)) { 33 | stream.push(entry); 34 | } 35 | 36 | if (index === filepaths.length - 1) { 37 | stream.end(); 38 | } 39 | 40 | done(); 41 | }) 42 | .catch(done); 43 | }; 44 | 45 | for (let index = 0; index < filepaths.length; index++) { 46 | stream.write(index); 47 | } 48 | 49 | return stream; 50 | } 51 | 52 | #getEntry(filepath: string, pattern: Pattern, options: ReaderOptions): Promise { 53 | return this.#getStat(filepath) 54 | .then((stats) => this._makeEntry(stats, pattern)) 55 | .catch((error: ErrnoException) => { 56 | if (options.errorFilter(error)) { 57 | return null; 58 | } 59 | 60 | throw error; 61 | }); 62 | } 63 | 64 | #getStat(filepath: string): Promise { 65 | return new Promise((resolve, reject) => { 66 | this._stat(filepath, this._fsStatSettings, (error: NodeJS.ErrnoException | null, stats) => { 67 | if (error === null) { 68 | resolve(stats); 69 | } else { 70 | reject(error); 71 | } 72 | }); 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/readers/sync.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | 3 | import { Stats } from '@nodelib/fs.macchiato'; 4 | import * as sinon from 'sinon'; 5 | import { describe, it } from 'mocha'; 6 | 7 | import Settings from '../settings'; 8 | import * as tests from '../tests'; 9 | import { ReaderSync } from './sync'; 10 | 11 | import type { Options } from '../settings'; 12 | import type { ReaderOptions } from '../types'; 13 | import type * as fsWalk from '@nodelib/fs.walk'; 14 | import type * as fsStat from '@nodelib/fs.stat'; 15 | 16 | type WalkSignature = typeof fsWalk.walkSync; 17 | type StatSignature = typeof fsStat.statSync; 18 | 19 | class TestReader extends ReaderSync { 20 | protected override _walkSync: WalkSignature = sinon.stub() as unknown as WalkSignature; 21 | protected override _statSync: StatSignature = sinon.stub() as unknown as StatSignature; 22 | 23 | constructor(options?: Options) { 24 | super(new Settings(options)); 25 | } 26 | 27 | public get walkSync(): sinon.SinonStub { 28 | return this._walkSync as unknown as sinon.SinonStub; 29 | } 30 | 31 | public get statSync(): sinon.SinonStub { 32 | return this._statSync as unknown as sinon.SinonStub; 33 | } 34 | } 35 | 36 | function getReader(options?: Options): TestReader { 37 | return new TestReader(options); 38 | } 39 | 40 | function getReaderOptions(options: Partial = {}): ReaderOptions { 41 | return { ...options } as unknown as ReaderOptions; 42 | } 43 | 44 | describe('Readers → ReaderSync', () => { 45 | describe('Constructor', () => { 46 | it('should create instance of class', () => { 47 | const reader = getReader(); 48 | 49 | assert.ok(reader instanceof TestReader); 50 | }); 51 | }); 52 | 53 | describe('.dynamic', () => { 54 | it('should call fs.walk method', () => { 55 | const reader = getReader(); 56 | const readerOptions = getReaderOptions(); 57 | 58 | reader.dynamic('root', readerOptions); 59 | 60 | assert.ok(reader.walkSync.called); 61 | }); 62 | }); 63 | 64 | describe('.static', () => { 65 | it('should return entries', () => { 66 | const reader = getReader(); 67 | const readerOptions = getReaderOptions({ entryFilter: () => true }); 68 | 69 | reader.statSync.onFirstCall().returns(new Stats()); 70 | reader.statSync.onSecondCall().returns(new Stats()); 71 | 72 | const actual = reader.static(['a.txt', 'b.txt'], readerOptions); 73 | 74 | assert.strictEqual(actual[0].name, 'a.txt'); 75 | assert.strictEqual(actual[1].name, 'b.txt'); 76 | }); 77 | 78 | it('should throw an error when the filter does not suppress the error', () => { 79 | const reader = getReader(); 80 | const readerOptions = getReaderOptions({ 81 | errorFilter: () => false, 82 | entryFilter: () => true, 83 | }); 84 | 85 | reader.statSync.onFirstCall().throws(tests.errno.getEperm()); 86 | reader.statSync.onSecondCall().returns(new Stats()); 87 | 88 | const expectedErrorMessageRe = /Error: EPERM: operation not permitted/; 89 | 90 | assert.throws(() => reader.static(['a.txt', 'b.txt'], readerOptions), expectedErrorMessageRe); 91 | }); 92 | 93 | it('should do not throw an error when the filter suppress the error', () => { 94 | const reader = getReader(); 95 | const readerOptions = getReaderOptions({ 96 | errorFilter: () => true, 97 | entryFilter: () => true, 98 | }); 99 | 100 | reader.statSync.onFirstCall().throws(tests.errno.getEnoent()); 101 | reader.statSync.onSecondCall().returns(new Stats()); 102 | 103 | const actual = reader.static(['a.txt', 'b.txt'], readerOptions); 104 | 105 | assert.strictEqual(actual.length, 1); 106 | assert.strictEqual(actual[0].name, 'b.txt'); 107 | }); 108 | 109 | it('should do not include entry when the filter excludes it', () => { 110 | const reader = getReader(); 111 | const readerOptions = getReaderOptions({ entryFilter: () => false }); 112 | 113 | reader.statSync.returns(new Stats()); 114 | 115 | const actual = reader.static(['a.txt'], readerOptions); 116 | 117 | assert.strictEqual(actual.length, 0); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /src/readers/sync.ts: -------------------------------------------------------------------------------- 1 | import * as fsStat from '@nodelib/fs.stat'; 2 | import * as fsWalk from '@nodelib/fs.walk'; 3 | 4 | import { Reader } from './reader'; 5 | 6 | import type { Entry, ErrnoException, FsStats, Pattern, ReaderOptions } from '../types'; 7 | 8 | export interface IReaderSync { 9 | dynamic: (root: string, options: ReaderOptions) => Entry[]; 10 | static: (patterns: Pattern[], options: ReaderOptions) => Entry[]; 11 | } 12 | 13 | export class ReaderSync extends Reader implements IReaderSync { 14 | protected _walkSync: typeof fsWalk.walkSync = fsWalk.walkSync; 15 | protected _statSync: typeof fsStat.statSync = fsStat.statSync; 16 | 17 | public dynamic(root: string, options: ReaderOptions): Entry[] { 18 | return this._walkSync(root, options); 19 | } 20 | 21 | public static(patterns: Pattern[], options: ReaderOptions): Entry[] { 22 | const entries: Entry[] = []; 23 | 24 | for (const pattern of patterns) { 25 | const filepath = this._getFullEntryPath(pattern); 26 | const entry = this.#getEntry(filepath, pattern, options); 27 | 28 | if (entry === null || !options.entryFilter(entry)) { 29 | continue; 30 | } 31 | 32 | entries.push(entry); 33 | } 34 | 35 | return entries; 36 | } 37 | 38 | #getEntry(filepath: string, pattern: Pattern, options: ReaderOptions): Entry | null { 39 | try { 40 | const stats = this.#getStat(filepath); 41 | 42 | return this._makeEntry(stats, pattern); 43 | } catch (error) { 44 | if (options.errorFilter(error as ErrnoException)) { 45 | return null; 46 | } 47 | 48 | throw error; 49 | } 50 | } 51 | 52 | #getStat(filepath: string): FsStats { 53 | return this._statSync(filepath, this._fsStatSettings); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/settings.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | 3 | import { describe, it } from 'mocha'; 4 | 5 | import Settings, { DEFAULT_FILE_SYSTEM_ADAPTER } from './settings'; 6 | 7 | describe('Settings', () => { 8 | it('should return instance with default values', () => { 9 | const settings = new Settings(); 10 | 11 | assert.deepStrictEqual(settings.fs, DEFAULT_FILE_SYSTEM_ADAPTER); 12 | assert.deepStrictEqual(settings.ignore, []); 13 | assert.ok(!settings.absolute); 14 | assert.ok(!settings.baseNameMatch); 15 | assert.ok(!settings.dot); 16 | assert.ok(!settings.markDirectories); 17 | assert.ok(!settings.objectMode); 18 | assert.ok(!settings.onlyDirectories); 19 | assert.ok(!settings.stats); 20 | assert.ok(!settings.suppressErrors); 21 | assert.ok(!settings.throwErrorOnBrokenSymbolicLink); 22 | assert.ok(settings.braceExpansion); 23 | assert.ok(settings.caseSensitiveMatch); 24 | assert.ok(settings.deep); 25 | assert.ok(settings.extglob); 26 | assert.ok(settings.followSymbolicLinks); 27 | assert.ok(settings.globstar); 28 | assert.ok(settings.onlyFiles); 29 | assert.ok(settings.unique); 30 | assert.strictEqual(settings.cwd, process.cwd()); 31 | assert.strictEqual(settings.signal, undefined); 32 | }); 33 | 34 | it('should return instance with custom values', () => { 35 | const settings = new Settings({ 36 | onlyFiles: false, 37 | }); 38 | 39 | assert.ok(!settings.onlyFiles); 40 | }); 41 | 42 | it('should set the "onlyFiles" option when the "onlyDirectories" is enabled', () => { 43 | const settings = new Settings({ 44 | onlyDirectories: true, 45 | }); 46 | 47 | assert.ok(!settings.onlyFiles); 48 | assert.ok(settings.onlyDirectories); 49 | }); 50 | 51 | it('should set the "objectMode" option when the "stats" is enabled', () => { 52 | const settings = new Settings({ 53 | stats: true, 54 | }); 55 | 56 | assert.ok(settings.objectMode); 57 | assert.ok(settings.stats); 58 | }); 59 | 60 | it('should return the `fs` option with custom method', () => { 61 | const customReaddirSync = (): never[] => []; 62 | 63 | const settings = new Settings({ 64 | fs: { readdirSync: customReaddirSync }, 65 | }); 66 | 67 | assert.strictEqual(settings.fs.readdirSync, customReaddirSync); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | 3 | import type { FileSystemAdapter, Pattern } from './types'; 4 | 5 | export const DEFAULT_FILE_SYSTEM_ADAPTER: FileSystemAdapter = { 6 | lstat: fs.lstat, 7 | lstatSync: fs.lstatSync, 8 | stat: fs.stat, 9 | statSync: fs.statSync, 10 | readdir: fs.readdir, 11 | readdirSync: fs.readdirSync, 12 | }; 13 | 14 | export interface Options { 15 | /** 16 | * Return the absolute path for entries. 17 | * 18 | * @default false 19 | */ 20 | absolute?: boolean; 21 | /** 22 | * If set to `true`, then patterns without slashes will be matched against 23 | * the basename of the path if it contains slashes. 24 | * 25 | * @default false 26 | */ 27 | baseNameMatch?: boolean; 28 | /** 29 | * Enables Bash-like brace expansion. 30 | * 31 | * @default true 32 | */ 33 | braceExpansion?: boolean; 34 | /** 35 | * Enables a case-sensitive mode for matching files. 36 | * 37 | * @default true 38 | */ 39 | caseSensitiveMatch?: boolean; 40 | /** 41 | * The current working directory in which to search. 42 | * 43 | * @default process.cwd() 44 | */ 45 | cwd?: string; 46 | /** 47 | * Specifies the maximum depth of a read directory relative to the start 48 | * directory. 49 | * 50 | * @default Infinity 51 | */ 52 | deep?: number; 53 | /** 54 | * Allow patterns to match entries that begin with a period (`.`). 55 | * 56 | * @default false 57 | */ 58 | dot?: boolean; 59 | /** 60 | * Enables Bash-like `extglob` functionality. 61 | * 62 | * @default true 63 | */ 64 | extglob?: boolean; 65 | /** 66 | * Indicates whether to traverse descendants of symbolic link directories. 67 | * 68 | * @default true 69 | */ 70 | followSymbolicLinks?: boolean; 71 | /** 72 | * Custom implementation of methods for working with the file system. 73 | * 74 | * @default fs.* 75 | */ 76 | fs?: Partial; 77 | /** 78 | * Enables recursively repeats a pattern containing `**`. 79 | * If `false`, `**` behaves exactly like `*`. 80 | * 81 | * @default true 82 | */ 83 | globstar?: boolean; 84 | /** 85 | * An array of glob patterns to exclude matches. 86 | * This is an alternative way to use negative patterns. 87 | * 88 | * @default [] 89 | */ 90 | ignore?: readonly Pattern[]; 91 | /** 92 | * Mark the directory path with the final slash. 93 | * 94 | * @default false 95 | */ 96 | markDirectories?: boolean; 97 | /** 98 | * Returns objects (instead of strings) describing entries. 99 | * 100 | * @default false 101 | */ 102 | objectMode?: boolean; 103 | /** 104 | * Return only directories. 105 | * 106 | * @default false 107 | */ 108 | onlyDirectories?: boolean; 109 | /** 110 | * Return only files. 111 | * 112 | * @default true 113 | */ 114 | onlyFiles?: boolean; 115 | /** 116 | * Enables an object mode (`objectMode`) with an additional `stats` field. 117 | * 118 | * @default false 119 | */ 120 | stats?: boolean; 121 | /** 122 | * By default this package suppress only `ENOENT` errors. 123 | * Set to `true` to suppress any error. 124 | * 125 | * @default false 126 | */ 127 | suppressErrors?: boolean; 128 | /** 129 | * Throw an error when symbolic link is broken if `true` or safely 130 | * return `lstat` call if `false`. 131 | * 132 | * @default false 133 | */ 134 | throwErrorOnBrokenSymbolicLink?: boolean; 135 | /** 136 | * Ensures that the returned entries are unique. 137 | * 138 | * @default true 139 | */ 140 | unique?: boolean; 141 | /** 142 | * A signal to abort searching for entries on the file system. 143 | * Works only with asynchronous methods for dynamic and static patterns. 144 | * 145 | * @default undefined 146 | */ 147 | signal?: AbortSignal; 148 | } 149 | 150 | export default class Settings { 151 | public readonly absolute: boolean; 152 | public readonly baseNameMatch: boolean; 153 | public readonly braceExpansion: boolean; 154 | public readonly caseSensitiveMatch: boolean; 155 | public readonly cwd: string; 156 | public readonly deep: number; 157 | public readonly dot: boolean; 158 | public readonly extglob: boolean; 159 | public readonly followSymbolicLinks: boolean; 160 | public readonly fs: FileSystemAdapter; 161 | public readonly globstar: boolean; 162 | public readonly ignore: readonly Pattern[]; 163 | public readonly markDirectories: boolean; 164 | public readonly objectMode: boolean; 165 | public readonly onlyDirectories: boolean; 166 | public readonly onlyFiles: boolean; 167 | public readonly stats: boolean; 168 | public readonly suppressErrors: boolean; 169 | public readonly throwErrorOnBrokenSymbolicLink: boolean; 170 | public readonly unique: boolean; 171 | public readonly signal?: AbortSignal; 172 | 173 | // eslint-disable-next-line complexity 174 | constructor(options: Options = {}) { 175 | this.absolute = options.absolute ?? false; 176 | this.baseNameMatch = options.baseNameMatch ?? false; 177 | this.braceExpansion = options.braceExpansion ?? true; 178 | this.caseSensitiveMatch = options.caseSensitiveMatch ?? true; 179 | this.cwd = options.cwd ?? process.cwd(); 180 | this.deep = options.deep ?? Number.POSITIVE_INFINITY; 181 | this.dot = options.dot ?? false; 182 | this.extglob = options.extglob ?? true; 183 | this.followSymbolicLinks = options.followSymbolicLinks ?? true; 184 | this.fs = this.#getFileSystemMethods(options.fs); 185 | this.globstar = options.globstar ?? true; 186 | this.ignore = options.ignore ?? []; 187 | this.markDirectories = options.markDirectories ?? false; 188 | this.objectMode = options.objectMode ?? false; 189 | this.onlyDirectories = options.onlyDirectories ?? false; 190 | this.onlyFiles = options.onlyFiles ?? true; 191 | this.stats = options.stats ?? false; 192 | this.suppressErrors = options.suppressErrors ?? false; 193 | this.throwErrorOnBrokenSymbolicLink = options.throwErrorOnBrokenSymbolicLink ?? false; 194 | this.unique = options.unique ?? true; 195 | this.signal = options.signal; 196 | 197 | if (this.onlyDirectories) { 198 | this.onlyFiles = false; 199 | } 200 | 201 | if (this.stats) { 202 | this.objectMode = true; 203 | } 204 | } 205 | 206 | #getFileSystemMethods(methods: Partial = {}): FileSystemAdapter { 207 | return { 208 | ...DEFAULT_FILE_SYSTEM_ADAPTER, 209 | ...methods, 210 | }; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/tests/e2e/errors.e2e.ts: -------------------------------------------------------------------------------- 1 | import * as runner from './runner'; 2 | 3 | runner.suite('Errors', { 4 | tests: [ 5 | { 6 | pattern: 'non-exist-directory/**', 7 | }, 8 | { 9 | pattern: 'non-exist-file.txt', 10 | }, 11 | ], 12 | }); 13 | 14 | runner.suite('Errors (cwd)', { 15 | tests: [ 16 | { 17 | pattern: '**', 18 | options: { 19 | cwd: 'non-exist-directory', 20 | }, 21 | }, 22 | ], 23 | }); 24 | -------------------------------------------------------------------------------- /src/tests/e2e/options/absolute.e2e.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | import * as runner from '../runner'; 4 | 5 | const CWD = process.cwd(); 6 | const CWD_POSIX = CWD.replaceAll('\\', '/'); 7 | 8 | function resultTransform(item: string): string { 9 | return item 10 | .replace(CWD, '') 11 | // Backslashes are used on Windows. 12 | // The `fixtures` directory is under our control, so we are confident that the conversions are correct. 13 | .replaceAll(/[/\\]/g, '/'); 14 | } 15 | 16 | runner.suite('Options Absolute', { 17 | resultTransform, 18 | tests: [ 19 | { 20 | pattern: 'fixtures/*', 21 | options: { 22 | absolute: true, 23 | }, 24 | }, 25 | { 26 | pattern: 'fixtures/**', 27 | options: { 28 | absolute: true, 29 | }, 30 | issue: 47, 31 | }, 32 | { 33 | pattern: 'fixtures/**/*', 34 | options: { 35 | absolute: true, 36 | }, 37 | }, 38 | { 39 | pattern: 'fixtures/../*', 40 | options: { 41 | absolute: true, 42 | }, 43 | }, 44 | ], 45 | }); 46 | 47 | runner.suite('Options Absolute (ignore)', { 48 | resultTransform, 49 | tests: [ 50 | { 51 | pattern: 'fixtures/*/*', 52 | options: { 53 | ignore: ['fixtures/*/nested'], 54 | absolute: true, 55 | }, 56 | }, 57 | { 58 | pattern: 'fixtures/*/*', 59 | options: { 60 | ignore: ['**/nested'], 61 | absolute: true, 62 | }, 63 | }, 64 | 65 | { 66 | pattern: 'fixtures/*', 67 | options: { 68 | ignore: [path.posix.join(CWD_POSIX, 'fixtures', '*')], 69 | absolute: true, 70 | }, 71 | }, 72 | { 73 | pattern: 'fixtures/**', 74 | options: { 75 | ignore: [path.posix.join(CWD_POSIX, 'fixtures', '*')], 76 | absolute: true, 77 | }, 78 | issue: 47, 79 | }, 80 | ], 81 | }); 82 | 83 | runner.suite('Options Absolute (cwd)', { 84 | resultTransform, 85 | tests: [ 86 | { 87 | pattern: '*', 88 | options: { 89 | cwd: 'fixtures', 90 | absolute: true, 91 | }, 92 | }, 93 | { 94 | pattern: '**', 95 | options: { 96 | cwd: 'fixtures', 97 | absolute: true, 98 | }, 99 | }, 100 | { 101 | pattern: '**/*', 102 | options: { 103 | cwd: 'fixtures', 104 | absolute: true, 105 | }, 106 | }, 107 | ], 108 | }); 109 | 110 | runner.suite('Options Absolute (cwd & ignore)', { 111 | resultTransform, 112 | tests: [ 113 | { 114 | pattern: '*/*', 115 | options: { 116 | ignore: ['*/nested'], 117 | cwd: 'fixtures', 118 | absolute: true, 119 | }, 120 | }, 121 | { 122 | pattern: '*/*', 123 | options: { 124 | ignore: ['**/nested'], 125 | cwd: 'fixtures', 126 | absolute: true, 127 | }, 128 | }, 129 | { 130 | pattern: 'file.md', 131 | options: { 132 | ignore: [path.posix.join('**', 'fixtures', '**')], 133 | cwd: 'fixtures', 134 | absolute: true, 135 | }, 136 | }, 137 | 138 | { 139 | pattern: '*', 140 | options: { 141 | ignore: [path.posix.join(CWD_POSIX, 'fixtures', '*')], 142 | cwd: 'fixtures', 143 | absolute: true, 144 | }, 145 | }, 146 | { 147 | pattern: '**', 148 | options: { 149 | ignore: [path.posix.join(CWD_POSIX, 'fixtures', '*')], 150 | cwd: 'fixtures', 151 | absolute: true, 152 | }, 153 | }, 154 | { 155 | pattern: '**', 156 | options: { 157 | ignore: [path.posix.join(CWD_POSIX, 'fixtures', '**')], 158 | cwd: 'fixtures', 159 | absolute: true, 160 | }, 161 | }, 162 | ], 163 | }); 164 | -------------------------------------------------------------------------------- /src/tests/e2e/options/base-name-match.e2e.ts: -------------------------------------------------------------------------------- 1 | import * as runner from '../runner'; 2 | 3 | runner.suite('Options MatchBase', { 4 | tests: [ 5 | { 6 | pattern: 'file.md', 7 | options: { 8 | cwd: 'fixtures', 9 | baseNameMatch: true, 10 | }, 11 | }, 12 | { 13 | pattern: 'first/*/file.md', 14 | options: { 15 | cwd: 'fixtures', 16 | baseNameMatch: true, 17 | }, 18 | }, 19 | { 20 | pattern: 'first/**/file.md', 21 | options: { 22 | cwd: 'fixtures', 23 | baseNameMatch: true, 24 | }, 25 | }, 26 | ], 27 | }); 28 | -------------------------------------------------------------------------------- /src/tests/e2e/options/case-sensitive-match.e2e.ts: -------------------------------------------------------------------------------- 1 | import * as runner from '../runner'; 2 | import * as utils from '../..'; 3 | 4 | runner.suite('Options CaseSensitiveMatch', { 5 | tests: [ 6 | { 7 | pattern: 'fixtures/File.md', 8 | expected: () => utils.platform.isUnix() ? [] : ['fixtures/File.md'], 9 | }, 10 | { 11 | pattern: 'fixtures/File.md', 12 | options: { caseSensitiveMatch: false }, 13 | }, 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /src/tests/e2e/options/deep.e2e.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as runner from '../runner'; 3 | 4 | runner.suite('Options Deep', { 5 | tests: [ 6 | { 7 | pattern: 'fixtures/**', 8 | options: { 9 | deep: 0, 10 | }, 11 | }, 12 | { 13 | pattern: 'fixtures/**', 14 | options: { 15 | deep: 2, 16 | }, 17 | }, 18 | ], 19 | }); 20 | 21 | runner.suite('Options Deep (cwd)', { 22 | tests: [ 23 | { 24 | pattern: '**', 25 | options: { 26 | cwd: 'fixtures', 27 | deep: 0, 28 | }, 29 | }, 30 | { 31 | pattern: '**', 32 | options: { 33 | cwd: 'fixtures', 34 | deep: 2, 35 | }, 36 | }, 37 | ], 38 | }); 39 | -------------------------------------------------------------------------------- /src/tests/e2e/options/dot.e2e.ts: -------------------------------------------------------------------------------- 1 | import * as runner from '../runner'; 2 | 3 | runner.suite('Options Dot', { 4 | tests: [ 5 | { 6 | pattern: 'fixtures/*', 7 | options: { 8 | dot: true, 9 | }, 10 | }, 11 | { 12 | pattern: 'fixtures/**', 13 | options: { 14 | dot: true, 15 | }, 16 | issue: 47, 17 | }, 18 | { 19 | pattern: 'fixtures/**/*', 20 | options: { 21 | dot: true, 22 | }, 23 | }, 24 | 25 | { 26 | pattern: 'fixtures/{.,}*', 27 | }, 28 | { 29 | pattern: 'fixtures/{.*,*}', 30 | }, 31 | { 32 | pattern: 'fixtures/**/{.,}*', 33 | }, 34 | { 35 | pattern: 'fixtures/{.**,**}', 36 | issue: 47, 37 | }, 38 | { 39 | pattern: 'fixtures/{**/.*,**}', 40 | issue: 47, 41 | }, 42 | ], 43 | }); 44 | -------------------------------------------------------------------------------- /src/tests/e2e/options/ignore.e2e.ts: -------------------------------------------------------------------------------- 1 | import * as runner from '../runner'; 2 | 3 | runner.suite('Options Ignore', { 4 | tests: [ 5 | { 6 | pattern: 'fixtures/**/*', 7 | options: { 8 | ignore: ['**/*.md'], 9 | }, 10 | }, 11 | { 12 | pattern: 'fixtures/**/*', 13 | options: { 14 | ignore: ['!**/*.md'], 15 | }, 16 | }, 17 | ], 18 | }); 19 | -------------------------------------------------------------------------------- /src/tests/e2e/options/mark-directories.e2e.ts: -------------------------------------------------------------------------------- 1 | import * as runner from '../runner'; 2 | 3 | runner.suite('Options MarkDirectories', { 4 | tests: [ 5 | { 6 | pattern: 'fixtures/**/*', 7 | options: { 8 | markDirectories: true, 9 | }, 10 | }, 11 | ], 12 | }); 13 | -------------------------------------------------------------------------------- /src/tests/e2e/options/only-directories.e2e.ts: -------------------------------------------------------------------------------- 1 | import * as runner from '../runner'; 2 | 3 | runner.suite('Options OnlyDirectories', { 4 | tests: [ 5 | { 6 | pattern: 'fixtures/*', 7 | options: { 8 | onlyDirectories: true, 9 | }, 10 | }, 11 | { 12 | pattern: 'fixtures/**', 13 | options: { 14 | onlyDirectories: true, 15 | }, 16 | }, 17 | { 18 | pattern: 'fixtures/**/*', 19 | options: { 20 | onlyDirectories: true, 21 | }, 22 | }, 23 | { 24 | pattern: 'fixtures/*/', 25 | options: { 26 | onlyDirectories: true, 27 | }, 28 | }, 29 | { 30 | pattern: 'fixtures/**/', 31 | options: { 32 | onlyDirectories: true, 33 | }, 34 | }, 35 | { 36 | pattern: 'fixtures/**/*/', 37 | options: { 38 | onlyDirectories: true, 39 | }, 40 | }, 41 | ], 42 | }); 43 | 44 | runner.suite('Options OnlyDirectories (cwd)', { 45 | tests: [ 46 | { 47 | pattern: '*', 48 | options: { 49 | cwd: 'fixtures', 50 | onlyDirectories: true, 51 | }, 52 | }, 53 | { 54 | pattern: '**', 55 | options: { 56 | cwd: 'fixtures', 57 | onlyDirectories: true, 58 | }, 59 | }, 60 | { 61 | pattern: '**/*', 62 | options: { 63 | cwd: 'fixtures', 64 | onlyDirectories: true, 65 | }, 66 | }, 67 | { 68 | pattern: '*/', 69 | options: { 70 | cwd: 'fixtures', 71 | onlyDirectories: true, 72 | }, 73 | }, 74 | { 75 | pattern: '**/', 76 | options: { 77 | cwd: 'fixtures', 78 | onlyDirectories: true, 79 | }, 80 | }, 81 | { 82 | pattern: '**/*/', 83 | options: { 84 | cwd: 'fixtures', 85 | onlyDirectories: true, 86 | }, 87 | }, 88 | ], 89 | }); 90 | -------------------------------------------------------------------------------- /src/tests/e2e/options/only-files.e2e.ts: -------------------------------------------------------------------------------- 1 | import * as runner from '../runner'; 2 | 3 | runner.suite('Options OnlyFiles', { 4 | tests: [ 5 | { 6 | pattern: 'fixtures/*', 7 | options: { 8 | onlyFiles: true, 9 | }, 10 | }, 11 | { 12 | pattern: 'fixtures/**', 13 | options: { 14 | onlyFiles: true, 15 | }, 16 | }, 17 | { 18 | pattern: 'fixtures/**/*', 19 | options: { 20 | onlyFiles: true, 21 | }, 22 | }, 23 | ], 24 | }); 25 | 26 | runner.suite('Options Files (cwd)', { 27 | tests: [ 28 | { 29 | pattern: '*', 30 | options: { 31 | cwd: 'fixtures', 32 | onlyFiles: true, 33 | }, 34 | }, 35 | { 36 | pattern: '**', 37 | options: { 38 | cwd: 'fixtures', 39 | onlyFiles: true, 40 | }, 41 | }, 42 | { 43 | pattern: '**/*', 44 | options: { 45 | cwd: 'fixtures', 46 | onlyFiles: true, 47 | }, 48 | }, 49 | ], 50 | }); 51 | -------------------------------------------------------------------------------- /src/tests/e2e/options/unique.e2e.ts: -------------------------------------------------------------------------------- 1 | import * as runner from '../runner'; 2 | 3 | runner.suite('Options Unique', { 4 | tests: [ 5 | { 6 | pattern: ['./file.md', 'file.md', '*'], 7 | options: { 8 | cwd: 'fixtures', 9 | unique: false, 10 | }, 11 | }, 12 | { 13 | pattern: ['./file.md', 'file.md', '*'], 14 | options: { 15 | cwd: 'fixtures', 16 | unique: true, 17 | }, 18 | // There's a race going on here. On some OS the values may float. 19 | resultTransform: (entry) => { 20 | return entry.replace('./', ''); 21 | }, 22 | }, 23 | ], 24 | }); 25 | -------------------------------------------------------------------------------- /src/tests/e2e/patterns/root.e2e.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import * as fs from 'node:fs'; 3 | 4 | import * as runner from '../runner'; 5 | import * as utils from '../..'; 6 | 7 | const CWD = process.cwd().replaceAll('\\', '/'); 8 | const ROOT = path.parse(CWD).root; 9 | 10 | function getRootEntries(root: string, withBase: boolean = false): string[] { 11 | let result = getRootEntriesWithFileTypes(root); 12 | 13 | if (withBase) { 14 | const separator = root.endsWith('/') ? '' : '/'; 15 | 16 | result = result.map((item) => `${root}${separator}${item}`); 17 | } 18 | 19 | return result; 20 | } 21 | 22 | function getRootEntriesWithFileTypes(root: string): string[] { 23 | return fs.readdirSync(root, { withFileTypes: true }) 24 | .filter((item) => !item.name.startsWith('.')) 25 | .filter((item) => !item.isDirectory()) 26 | .map((item) => item.name); 27 | } 28 | 29 | runner.suite('Patterns Root', { 30 | tests: [ 31 | { 32 | pattern: '/*', 33 | options: { followSymbolicLinks: false }, 34 | condition: () => !utils.platform.isWindows(), 35 | expected: () => getRootEntries(ROOT, /** withBase */ true), 36 | }, 37 | { 38 | pattern: '/usr/*', 39 | condition: () => !utils.platform.isWindows(), 40 | expected: () => getRootEntries('/usr', /** withBase */ true), 41 | }, 42 | { 43 | pattern: '/*', 44 | condition: () => utils.platform.isWindows(), 45 | expected: () => getRootEntries('/', /** withBase */ true), 46 | }, 47 | // UNC pattern without dynamic sections in the base section 48 | { 49 | pattern: `//?/${ROOT}*`, 50 | condition: () => utils.platform.isWindows(), 51 | expected: () => getRootEntries(`//?/${ROOT}`, /** withBase */ true), 52 | }, 53 | ], 54 | }); 55 | 56 | runner.suite('Patterns Root (cwd)', { 57 | tests: [ 58 | { 59 | pattern: '*', 60 | options: { 61 | cwd: ROOT, 62 | followSymbolicLinks: false, 63 | }, 64 | condition: () => !utils.platform.isWindows(), 65 | expected: () => getRootEntries(ROOT), 66 | }, 67 | // UNC on Windows 68 | { 69 | pattern: '*', 70 | options: { 71 | cwd: `//?/${ROOT}`, 72 | }, 73 | condition: () => utils.platform.isWindows(), 74 | expected: () => getRootEntries(`//?/${ROOT}`), 75 | }, 76 | ], 77 | }); 78 | -------------------------------------------------------------------------------- /src/tests/e2e/patterns/static.e2e.ts: -------------------------------------------------------------------------------- 1 | import * as runner from '../runner'; 2 | 3 | runner.suite('Patterns Static', { 4 | tests: [ 5 | { pattern: 'fixtures' }, 6 | { pattern: 'fixtures/file.md' }, 7 | { pattern: 'fixtures/first' }, 8 | ], 9 | }); 10 | 11 | runner.suite('Patterns Static (cwd)', { 12 | tests: [ 13 | { pattern: 'file.md', options: { cwd: 'fixtures' } }, 14 | { pattern: 'first', options: { cwd: 'fixtures' } }, 15 | ], 16 | }); 17 | 18 | runner.suite('Patterns Static (ignore)', { 19 | tests: [ 20 | // Files 21 | [ 22 | { pattern: 'fixtures/file.md', options: { ignore: ['file.md'] } }, 23 | { pattern: 'fixtures/file.md', options: { ignore: ['*.md'] } }, 24 | { pattern: 'fixtures/file.md', options: { ignore: ['*'] } }, 25 | { pattern: 'fixtures/file.md', options: { ignore: ['**'] } }, 26 | { pattern: 'fixtures/file.md', options: { ignore: ['**/*'] } }, 27 | { pattern: 'fixtures/file.md', options: { ignore: ['fixtures/file.md'] } }, 28 | { pattern: 'fixtures/file.md', options: { ignore: ['fixtures/*.md'] } }, 29 | { pattern: 'fixtures/file.md', options: { ignore: ['fixtures/*'] } }, 30 | { pattern: 'fixtures/file.md', options: { ignore: ['fixtures/**'] } }, 31 | { pattern: 'fixtures/file.md', options: { ignore: ['fixtures/**/*'] } }, 32 | ], 33 | 34 | // Directories 35 | [ 36 | { pattern: 'fixtures/first', options: { ignore: ['first'] } }, 37 | { pattern: 'fixtures/first', options: { ignore: ['*'] } }, 38 | { pattern: 'fixtures/first', options: { ignore: ['**'] } }, 39 | { pattern: 'fixtures/first', options: { ignore: ['**/*'] } }, 40 | { pattern: 'fixtures/first', options: { ignore: ['fixtures/first'] } }, 41 | { pattern: 'fixtures/first', options: { ignore: ['fixtures/*'] } }, 42 | { pattern: 'fixtures/first', options: { ignore: ['fixtures/**'] } }, 43 | { pattern: 'fixtures/first', options: { ignore: ['fixtures/**/*'] } }, 44 | ], 45 | ], 46 | }); 47 | 48 | runner.suite('Patterns Static (ignore & cwd)', { 49 | tests: [ 50 | // Files 51 | [ 52 | { pattern: 'fixtures/file.md', options: { ignore: ['file.md'], cwd: 'fixtures' } }, 53 | { pattern: 'fixtures/file.md', options: { ignore: ['*.md'], cwd: 'fixtures' } }, 54 | { pattern: 'fixtures/file.md', options: { ignore: ['*'], cwd: 'fixtures' } }, 55 | { pattern: 'fixtures/file.md', options: { ignore: ['**'], cwd: 'fixtures' } }, 56 | { pattern: 'fixtures/file.md', options: { ignore: ['**/*'], cwd: 'fixtures' } }, 57 | ], 58 | 59 | // Directories 60 | [ 61 | { pattern: 'fixtures/first', options: { ignore: ['first'], cwd: 'fixtures' } }, 62 | { pattern: 'fixtures/first', options: { ignore: ['*'], cwd: 'fixtures' } }, 63 | { pattern: 'fixtures/first', options: { ignore: ['**'], cwd: 'fixtures' } }, 64 | { pattern: 'fixtures/first', options: { ignore: ['**/*'], cwd: 'fixtures' } }, 65 | ], 66 | ], 67 | }); 68 | 69 | runner.suite('Patterns Static (relative)', { 70 | tests: [ 71 | { pattern: '../file.md', options: { cwd: 'fixtures/first' } }, 72 | { pattern: '../../file.md', options: { cwd: 'fixtures/first/nested' } }, 73 | ], 74 | }); 75 | -------------------------------------------------------------------------------- /src/tests/e2e/runner.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable mocha/no-setup-in-describe */ 2 | import * as assert from 'node:assert'; 3 | 4 | import * as snapshotIt from 'snap-shot-it'; 5 | import { describe, it } from 'mocha'; 6 | 7 | import * as fg from '../..'; 8 | 9 | import type { Pattern } from '../../types'; 10 | 11 | const CWD = process.cwd().replaceAll('\\', '/'); 12 | 13 | type TransformFunction = (entry: string) => string; 14 | 15 | interface Suite { 16 | tests: Test[] | Test[][]; 17 | /** 18 | * Allow to run only one test case with debug information. 19 | */ 20 | debug?: boolean | fg.Options; 21 | /** 22 | * The ability to conditionally run the test. 23 | */ 24 | condition?: () => boolean; 25 | resultTransform?: TransformFunction; 26 | } 27 | 28 | interface Test { 29 | pattern: Pattern | Pattern[]; 30 | options?: fg.Options; 31 | /** 32 | * Allow to run only one test case with debug information. 33 | */ 34 | debug?: boolean | fg.Options; 35 | /** 36 | * The ability to conditionally run the test. 37 | */ 38 | condition?: () => boolean; 39 | resultTransform?: TransformFunction; 40 | /** 41 | * The issue related to this test. 42 | */ 43 | issue?: number | number[]; 44 | expected?: () => string[]; 45 | } 46 | 47 | type MochaDefinition = Mocha.ExclusiveTestFunction | Mocha.TestFunction; 48 | 49 | export function suite(name: string, suite: Suite): void { 50 | describe(name, () => { 51 | for (const test of getSuiteTests(suite.tests)) { 52 | const title = getTestTitle(test); 53 | const definition = getTestMochaDefinition(suite, test); 54 | const transformers = getResultTransformers(suite, test); 55 | const patterns = getTestPatterns(test); 56 | const options = getFastGlobOptions(suite, test); 57 | 58 | definition(`${title} (sync)`, () => { 59 | let actual = getFastGlobEntriesSync(patterns, options); 60 | 61 | actual = transform(actual, transformers); 62 | 63 | debug(actual, suite, test); 64 | assertResult(actual, test); 65 | }); 66 | 67 | definition(`${title} (async)`, async () => { 68 | let actual = await getFastGlobEntriesAsync(patterns, options); 69 | 70 | actual = transform(actual, transformers); 71 | 72 | debug(actual, suite, test); 73 | assertResult(actual, test); 74 | }); 75 | 76 | definition(`${title} (stream)`, async () => { 77 | let actual = await getFastGlobEntriesStream(patterns, options); 78 | 79 | actual = transform(actual, transformers); 80 | 81 | debug(actual, suite, test); 82 | assertResult(actual, test); 83 | }); 84 | } 85 | }); 86 | } 87 | 88 | function getSuiteTests(tests: Test[] | Test[][]): Test[] { 89 | return ([] as Test[]).concat(...tests); 90 | } 91 | 92 | function getTestPatterns(test: Test): Pattern[] { 93 | return ([] as Pattern[]).concat(test.pattern); 94 | } 95 | 96 | function getTestTitle(test: Test): string { 97 | // Replacing placeholders to hide absolute paths from snapshots. 98 | const replacements = { 99 | cwd: test.options?.cwd?.replace(CWD, ''), 100 | ignore: test.options?.ignore?.map((pattern) => pattern.replace(CWD, '')), 101 | }; 102 | 103 | return JSON.stringify({ 104 | pattern: test.pattern, 105 | options: { 106 | ...test.options, 107 | ...replacements, 108 | }, 109 | }); 110 | } 111 | 112 | function getTestMochaDefinition(suite: Suite, test: Test): MochaDefinition { 113 | const isDebugDefined = suite.debug !== undefined || test.debug !== undefined; 114 | const isDebugEnabled = suite.debug !== false || test.debug !== false; 115 | 116 | if (isDebugDefined && isDebugEnabled) { 117 | return it.only; 118 | } 119 | 120 | if (suite.condition?.() === false || test.condition?.() === false) { 121 | return it.skip; 122 | } 123 | 124 | return it; 125 | } 126 | 127 | function getFastGlobOptions(suite: Suite, test: Test): fg.Options | undefined { 128 | let options = test.options; 129 | 130 | if (typeof suite.debug !== 'boolean') { 131 | options = { ...options, ...suite.debug }; 132 | } 133 | 134 | if (typeof test.debug !== 'boolean') { 135 | options = { ...options, ...test.debug }; 136 | } 137 | 138 | return options; 139 | } 140 | 141 | function getResultTransformers(suite: Suite, test: Test): TransformFunction[] { 142 | const transformers: TransformFunction[] = []; 143 | 144 | if (suite.resultTransform !== undefined) { 145 | transformers.push(suite.resultTransform); 146 | } 147 | 148 | if (test.resultTransform !== undefined) { 149 | transformers.push(test.resultTransform); 150 | } 151 | 152 | return transformers; 153 | } 154 | 155 | function getFastGlobEntriesSync(patterns: Pattern[], options?: fg.Options): string[] { 156 | return fg.globSync(patterns, options); 157 | } 158 | 159 | async function getFastGlobEntriesAsync(patterns: Pattern[], options?: fg.Options): Promise { 160 | return fg.glob(patterns, options); 161 | } 162 | 163 | async function getFastGlobEntriesStream(patterns: Pattern[], options?: fg.Options): Promise { 164 | const entries: string[] = []; 165 | 166 | const stream = fg.globStream(patterns, options); 167 | 168 | await new Promise((resolve, reject) => { 169 | stream.on('data', (entry: string) => entries.push(entry)); 170 | stream.once('error', reject); 171 | stream.once('end', resolve); 172 | }); 173 | 174 | return entries; 175 | } 176 | 177 | function transform(entries: string[], transformers: TransformFunction[]): string[] { 178 | let result = entries; 179 | 180 | for (const transformer of transformers) { 181 | result = result.map((item) => transformer(item)); 182 | } 183 | 184 | return result; 185 | } 186 | 187 | function assertResult(entries: string[], test: Test): void { 188 | entries.sort((a, b) => a.localeCompare(b)); 189 | 190 | if (test.expected === undefined) { 191 | snapshotIt(entries); 192 | } else { 193 | const expected = test.expected(); 194 | 195 | expected.sort((a, b) => a.localeCompare(b)); 196 | 197 | assert.deepStrictEqual(entries, expected); 198 | } 199 | } 200 | 201 | function debug(current: string[], suite: Suite, test: Test): void { 202 | const isDebug = suite.debug !== undefined || test.debug !== undefined; 203 | 204 | if (isDebug) { 205 | console.dir({ 206 | current, 207 | suite: { debug: suite.debug }, 208 | test: { debug: test.debug, options: test.options }, 209 | }, { colors: true }); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/tests/index.ts: -------------------------------------------------------------------------------- 1 | export * as entry from './utils/entry'; 2 | export * as errno from './utils/errno'; 3 | export * as pattern from './utils/pattern'; 4 | export * as platform from './utils/platform'; 5 | export * as task from './utils/task'; 6 | -------------------------------------------------------------------------------- /src/tests/utils/entry.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | import { Dirent, DirentType, Stats } from '@nodelib/fs.macchiato'; 4 | 5 | import type { Entry } from '../../types'; 6 | 7 | class EntryBuilder { 8 | #entryType: DirentType = DirentType.Unknown; 9 | 10 | readonly #entry: Entry = { 11 | name: '', 12 | path: '', 13 | dirent: new Dirent(), 14 | }; 15 | 16 | public path(filepath: string): this { 17 | this.#entry.name = path.basename(filepath); 18 | this.#entry.path = filepath; 19 | 20 | return this; 21 | } 22 | 23 | public file(): this { 24 | this.#entryType = DirentType.File; 25 | 26 | return this; 27 | } 28 | 29 | public directory(): this { 30 | this.#entryType = DirentType.Directory; 31 | 32 | return this; 33 | } 34 | 35 | public symlink(): this { 36 | this.#entryType = DirentType.Link; 37 | 38 | return this; 39 | } 40 | 41 | public socket(): this { 42 | this.#entryType = DirentType.Socket; 43 | 44 | return this; 45 | } 46 | 47 | public stats(): this { 48 | this.#entry.stats = new Stats(); 49 | 50 | return this; 51 | } 52 | 53 | public build(): Entry { 54 | this.#entry.dirent = new Dirent(this.#entry.name, this.#entryType); 55 | 56 | return this.#entry; 57 | } 58 | } 59 | 60 | export function builder(): EntryBuilder { 61 | return new EntryBuilder(); 62 | } 63 | -------------------------------------------------------------------------------- /src/tests/utils/errno.ts: -------------------------------------------------------------------------------- 1 | import type { ErrnoException } from '../../types'; 2 | 3 | class SystemError extends Error implements ErrnoException { 4 | constructor(public readonly code: string, message: string) { 5 | super(`${code}: ${message}`); 6 | this.name = 'SystemError'; 7 | } 8 | } 9 | 10 | export function getEnoent(): ErrnoException { 11 | return new SystemError('ENOENT', 'no such file or directory'); 12 | } 13 | 14 | export function getEperm(): ErrnoException { 15 | return new SystemError('EPERM', 'operation not permitted'); 16 | } 17 | -------------------------------------------------------------------------------- /src/tests/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | 3 | export function isDirectory(filepath: string): boolean { 4 | const stats = fs.lstatSync(filepath); 5 | 6 | return stats.isDirectory(); 7 | } 8 | -------------------------------------------------------------------------------- /src/tests/utils/pattern.ts: -------------------------------------------------------------------------------- 1 | import * as utils from '../../utils'; 2 | 3 | import type { Pattern, MicromatchOptions } from '../../types'; 4 | import type { PatternSegment, PatternInfo } from '../../providers/matchers/matcher'; 5 | 6 | class PatternSegmentBuilder { 7 | readonly #segment: PatternSegment = { 8 | dynamic: false, 9 | pattern: '', 10 | }; 11 | 12 | public dynamic(): this { 13 | this.#segment.dynamic = true; 14 | 15 | return this; 16 | } 17 | 18 | public pattern(pattern: Pattern): this { 19 | this.#segment.pattern = pattern; 20 | 21 | return this; 22 | } 23 | 24 | public build(options: MicromatchOptions = {}): PatternSegment { 25 | if (!this.#segment.dynamic) { 26 | return this.#segment; 27 | } 28 | 29 | return { 30 | ...this.#segment, 31 | patternRe: utils.pattern.makeRe(this.#segment.pattern, options), 32 | }; 33 | } 34 | } 35 | 36 | class PatternInfoBuilder { 37 | readonly #section: PatternInfo = { 38 | complete: true, 39 | pattern: '', 40 | segments: [], 41 | sections: [], 42 | }; 43 | 44 | public section(...segments: PatternSegment[]): this { 45 | this.#section.sections.push(segments); 46 | 47 | if (this.#section.segments.length === 0) { 48 | this.#section.complete = true; 49 | this.#section.segments.push(...segments); 50 | } else { 51 | this.#section.complete = false; 52 | const globstar = segment().dynamic().pattern('**').build(); 53 | 54 | this.#section.segments.push(globstar, ...segments); 55 | } 56 | 57 | return this; 58 | } 59 | 60 | public build(): PatternInfo { 61 | return { 62 | ...this.#section, 63 | pattern: this.#buildPattern(), 64 | }; 65 | } 66 | 67 | #buildPattern(): Pattern { 68 | return this.#section.segments.map((segment) => segment.pattern).join('/'); 69 | } 70 | } 71 | 72 | export function segment(): PatternSegmentBuilder { 73 | return new PatternSegmentBuilder(); 74 | } 75 | 76 | export function info(): PatternInfoBuilder { 77 | return new PatternInfoBuilder(); 78 | } 79 | -------------------------------------------------------------------------------- /src/tests/utils/platform.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'node:os'; 2 | 3 | export function isWindows(): boolean { 4 | return os.platform() === 'win32'; 5 | } 6 | 7 | export function isMacos(): boolean { 8 | return os.platform() === 'darwin'; 9 | } 10 | 11 | export function isUnix(): boolean { 12 | return os.platform() === 'linux'; 13 | } 14 | -------------------------------------------------------------------------------- /src/tests/utils/task.ts: -------------------------------------------------------------------------------- 1 | import type { Task } from '../../managers/tasks'; 2 | import type { Pattern } from '../../types'; 3 | 4 | class TaskBuilder { 5 | readonly #task: Task = { 6 | base: '', 7 | dynamic: true, 8 | patterns: [], 9 | positive: [], 10 | negative: [], 11 | }; 12 | 13 | public base(base: string): this { 14 | this.#task.base = base; 15 | 16 | return this; 17 | } 18 | 19 | public static(): this { 20 | this.#task.dynamic = false; 21 | 22 | return this; 23 | } 24 | 25 | public positive(pattern: Pattern): this { 26 | this.#task.patterns.push(pattern); 27 | this.#task.positive.push(pattern); 28 | 29 | return this; 30 | } 31 | 32 | public negative(pattern: Pattern): this { 33 | this.#task.patterns.push(`!${pattern}`); 34 | this.#task.negative.push(pattern); 35 | 36 | return this; 37 | } 38 | 39 | public build(): Task { 40 | return this.#task; 41 | } 42 | } 43 | 44 | export function builder(): TaskBuilder { 45 | return new TaskBuilder(); 46 | } 47 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type * as fs from 'node:fs'; 2 | import type * as fsWalk from '@nodelib/fs.walk'; 3 | 4 | export type Dictionary = Record; 5 | 6 | export type ErrnoException = NodeJS.ErrnoException; 7 | 8 | export type FsDirent = fs.Dirent; 9 | export type FsStats = fs.Stats; 10 | export type Entry = fsWalk.Entry; 11 | export type EntryItem = Entry | string; 12 | 13 | export type Pattern = string; 14 | export type PatternRe = RegExp; 15 | export type PatternsGroup = Dictionary; 16 | 17 | export type ReaderOptions = { 18 | transform: (entry: Entry) => EntryItem; 19 | deepFilter: DeepFilterFunction; 20 | entryFilter: EntryFilterFunction; 21 | errorFilter: ErrorFilterFunction; 22 | fs: FileSystemAdapter; 23 | stats: boolean; 24 | } & fsWalk.Options; 25 | 26 | export type ErrorFilterFunction = fsWalk.ErrorFilterFunction; 27 | export type EntryFilterFunction = fsWalk.EntryFilterFunction; 28 | export type DeepFilterFunction = fsWalk.DeepFilterFunction; 29 | export type EntryTransformerFunction = (entry: Entry) => EntryItem; 30 | 31 | export interface MicromatchOptions { 32 | dot?: boolean; 33 | matchBase?: boolean; 34 | nobrace?: boolean; 35 | nocase?: boolean; 36 | noext?: boolean; 37 | noglobstar?: boolean; 38 | posix?: boolean; 39 | strictSlashes?: boolean; 40 | } 41 | 42 | export type FileSystemAdapter = fsWalk.FileSystemAdapter; 43 | -------------------------------------------------------------------------------- /src/utils/array.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | 3 | import { describe, it } from 'mocha'; 4 | 5 | import * as util from './array'; 6 | 7 | describe('Utils → Array', () => { 8 | describe('.flatFirstLevel', () => { 9 | it('should return non-nested array', () => { 10 | const expected = ['a', 'b']; 11 | 12 | const actual = util.flatFirstLevel([['a'], ['b']]); 13 | 14 | assert.deepStrictEqual(actual, expected); 15 | }); 16 | }); 17 | 18 | describe('.splitWhen', () => { 19 | it('should return one group', () => { 20 | const expected = [[1, 2]]; 21 | 22 | const actual = util.splitWhen([1, 2], () => false); 23 | 24 | assert.deepStrictEqual(actual, expected); 25 | }); 26 | 27 | it('should return group for each item of array', () => { 28 | const expected = [[], [], [], []]; 29 | 30 | const actual = util.splitWhen([1, 2, 3], () => true); 31 | 32 | assert.deepStrictEqual(actual, expected); 33 | }); 34 | 35 | it('should return two group', () => { 36 | const expected = [[1, 2], [4, 5]]; 37 | 38 | const actual = util.splitWhen([1, 2, 3, 4, 5], (item) => item === 3); 39 | 40 | assert.deepStrictEqual(actual, expected); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/utils/array.ts: -------------------------------------------------------------------------------- 1 | export function flatFirstLevel(items: T[][]): T[] { 2 | // We do not use `Array.flat` because this is slower than current implementation for your case. 3 | return ([] as T[]).concat(...items); 4 | } 5 | 6 | export function splitWhen(items: T[], predicate: (item: T) => boolean): T[][] { 7 | const result: T[][] = [[]]; 8 | 9 | let groupIndex = 0; 10 | 11 | for (const item of items) { 12 | if (predicate(item)) { 13 | groupIndex++; 14 | result[groupIndex] = []; 15 | } else { 16 | result[groupIndex].push(item); 17 | } 18 | } 19 | 20 | return result; 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/errno.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | 3 | import { describe, it } from 'mocha'; 4 | 5 | import * as tests from '../tests'; 6 | import * as util from './errno'; 7 | 8 | describe('Utils → Errno', () => { 9 | describe('.isEnoentCodeError', () => { 10 | it('should return true for ENOENT error', () => { 11 | assert.ok(util.isEnoentCodeError(tests.errno.getEnoent())); 12 | }); 13 | 14 | it('should return false for EPERM error', () => { 15 | assert.ok(!util.isEnoentCodeError(tests.errno.getEperm())); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/utils/errno.ts: -------------------------------------------------------------------------------- 1 | import type { ErrnoException } from '../types'; 2 | 3 | export function isEnoentCodeError(error: ErrnoException): boolean { 4 | return error.code === 'ENOENT'; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/fs.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | 3 | import { Stats, StatsMode } from '@nodelib/fs.macchiato'; 4 | import { describe, it } from 'mocha'; 5 | 6 | import * as util from './fs'; 7 | 8 | describe('Utils → FS', () => { 9 | describe('.createDirentFromStats', () => { 10 | it('should convert fs.Stats to fs.Dirent', () => { 11 | const stats = new Stats({ mode: StatsMode.File }); 12 | const actual = util.createDirentFromStats('name', stats); 13 | 14 | assert.strictEqual(actual.name, 'name'); 15 | assert.ok(!actual.isBlockDevice()); 16 | assert.ok(!actual.isCharacterDevice()); 17 | assert.ok(!actual.isDirectory()); 18 | assert.ok(!actual.isFIFO()); 19 | assert.ok(actual.isFile()); 20 | assert.ok(!actual.isSocket()); 21 | assert.ok(!actual.isSymbolicLink()); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | 3 | import type { FsStats, FsDirent } from '../types'; 4 | 5 | const _kStats = Symbol('stats'); 6 | 7 | type DirentStatsKeysIntersection = 'constructor' | keyof FsDirent & keyof FsStats; 8 | 9 | // Adapting an internal class in Node.js to mimic the behavior of `fs.Dirent` when creating it manually from `fs.Stats`. 10 | // https://github.com/nodejs/node/blob/a4cf6b204f0b160480153dc293ae748bf15225f9/lib/internal/fs/utils.js#L199C1-L213 11 | export class DirentFromStats extends fs.Dirent { 12 | private readonly [_kStats]: FsStats; 13 | 14 | constructor(name: string, stats: FsStats) { 15 | // @ts-expect-error The constructor has parameters, but they are not represented in types. 16 | // https://github.com/nodejs/node/blob/a4cf6b204f0b160480153dc293ae748bf15225f9/lib/internal/fs/utils.js#L164 17 | super(name, null); 18 | 19 | this[_kStats] = stats; 20 | } 21 | } 22 | 23 | for (const key of Reflect.ownKeys(fs.Dirent.prototype)) { 24 | const name = key as 'constructor' | DirentStatsKeysIntersection; 25 | 26 | if (name === 'constructor') { 27 | continue; 28 | } 29 | 30 | DirentFromStats.prototype[name] = function () { 31 | return this[_kStats][name](); 32 | }; 33 | } 34 | 35 | export function createDirentFromStats(name: string, stats: FsStats): FsDirent { 36 | return new DirentFromStats(name, stats); 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * as array from './array'; 2 | export * as errno from './errno'; 3 | export * as fs from './fs'; 4 | export * as path from './path'; 5 | export * as pattern from './pattern'; 6 | export * as stream from './stream'; 7 | export * as string from './string'; 8 | -------------------------------------------------------------------------------- /src/utils/path.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'node:os'; 2 | import * as path from 'node:path'; 3 | 4 | import type { Pattern } from '../types'; 5 | 6 | const IS_WINDOWS_PLATFORM = os.platform() === 'win32'; 7 | const LEADING_DOT_SEGMENT_CHARACTERS_COUNT = 2; // ./ or .\\ 8 | /** 9 | * All non-escaped special characters. 10 | * Posix: ()*?[]{|}, !+@ before (, ! at the beginning, \\ before non-special characters. 11 | * Windows: (){}[], !+@ before (, ! at the beginning. 12 | */ 13 | const POSIX_UNESCAPED_GLOB_SYMBOLS_RE = /(?\\?)(?[()*?[\]{|}]|^!|[!+@](?=\()|\\(?![!()*+?@[\]{|}]))/g; 14 | const WINDOWS_UNESCAPED_GLOB_SYMBOLS_RE = /(?\\?)(?[()[\]{}]|^!|[!+@](?=\())/g; 15 | /** 16 | * The device path (\\.\ or \\?\). 17 | * https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#dos-device-paths 18 | */ 19 | const DOS_DEVICE_PATH_RE = /^\\\\(?[.?])/; 20 | /** 21 | * All backslashes except those escaping special characters. 22 | * Windows: !()+@{} 23 | * https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions 24 | */ 25 | const WINDOWS_BACKSLASHES_RE = /\\(?![!()+@[\]{}])/g; 26 | 27 | export function makeAbsolute(cwd: string, filepath: string): string { 28 | return path.resolve(cwd, filepath); 29 | } 30 | 31 | export function removeLeadingDotSegment(entry: string): string { 32 | // We do not use `startsWith` because this is 10x slower than current implementation for some cases. 33 | // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with 34 | if (entry.charAt(0) === '.') { 35 | const secondCharactery = entry.charAt(1); 36 | 37 | if (secondCharactery === '/' || secondCharactery === '\\') { 38 | return entry.slice(LEADING_DOT_SEGMENT_CHARACTERS_COUNT); 39 | } 40 | } 41 | 42 | return entry; 43 | } 44 | 45 | export function removeBackslashes(entry: string): string { 46 | return entry.replaceAll('\\', ''); 47 | } 48 | 49 | export const escape = IS_WINDOWS_PLATFORM ? escapeWindowsPath : escapePosixPath; 50 | 51 | export function escapeWindowsPath(pattern: Pattern): Pattern { 52 | return pattern.replaceAll(WINDOWS_UNESCAPED_GLOB_SYMBOLS_RE, String.raw`\$2`); 53 | } 54 | 55 | export function escapePosixPath(pattern: Pattern): Pattern { 56 | return pattern.replaceAll(POSIX_UNESCAPED_GLOB_SYMBOLS_RE, String.raw`\$2`); 57 | } 58 | 59 | export const convertPathToPattern = IS_WINDOWS_PLATFORM ? convertWindowsPathToPattern : convertPosixPathToPattern; 60 | 61 | export function convertWindowsPathToPattern(filepath: string): Pattern { 62 | return escapeWindowsPath(filepath) 63 | .replace(DOS_DEVICE_PATH_RE, '//$1') 64 | .replaceAll(WINDOWS_BACKSLASHES_RE, '/'); 65 | } 66 | 67 | export function convertPosixPathToPattern(filepath: string): Pattern { 68 | return escapePosixPath(filepath); 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/pattern.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | // https://stackoverflow.com/a/39415662 4 | // eslint-disable-next-line @typescript-eslint/no-require-imports 5 | import globParent = require('glob-parent'); 6 | import * as micromatch from 'micromatch'; 7 | 8 | import type { MicromatchOptions, Pattern, PatternRe } from '../types'; 9 | 10 | const GLOBSTAR = '**'; 11 | const ESCAPE_SYMBOL = '\\'; 12 | 13 | const COMMON_GLOB_SYMBOLS_RE = /[*?]|^!/; 14 | const REGEX_CHARACTER_CLASS_SYMBOLS_RE = /\[[^[]*]/; 15 | const REGEX_GROUP_SYMBOLS_RE = /(?:^|[^!*+?@])\([^(]*\|[^|]*\)/; 16 | const GLOB_EXTENSION_SYMBOLS_RE = /[!*+?@]\([^(]*\)/; 17 | const BRACE_EXPANSION_SEPARATORS_RE = /,|\.\./; 18 | 19 | /** 20 | * Matches a sequence of two or more consecutive slashes, excluding the first two slashes at the beginning of the string. 21 | * The latter is due to the presence of the device path at the beginning of the UNC path. 22 | */ 23 | const DOUBLE_SLASH_RE = /(?!^)\/{2,}/g; 24 | 25 | interface PatternTypeOptions { 26 | braceExpansion?: boolean; 27 | caseSensitiveMatch?: boolean; 28 | extglob?: boolean; 29 | } 30 | 31 | export function isStaticPattern(pattern: Pattern, options: PatternTypeOptions = {}): boolean { 32 | return !isDynamicPattern(pattern, options); 33 | } 34 | 35 | export function isDynamicPattern(pattern: Pattern, options: PatternTypeOptions = {}): boolean { 36 | /** 37 | * A special case with an empty string is necessary for matching patterns that start with a forward slash. 38 | * An empty string cannot be a dynamic pattern. 39 | * For example, the pattern `/lib/*` will be spread into parts: '', 'lib', '*'. 40 | */ 41 | if (pattern === '') { 42 | return false; 43 | } 44 | 45 | /** 46 | * When the `caseSensitiveMatch` option is disabled, all patterns must be marked as dynamic, because we cannot check 47 | * filepath directly (without read directory). 48 | */ 49 | if (options.caseSensitiveMatch === false || pattern.includes(ESCAPE_SYMBOL)) { 50 | return true; 51 | } 52 | 53 | if (COMMON_GLOB_SYMBOLS_RE.test(pattern) || REGEX_CHARACTER_CLASS_SYMBOLS_RE.test(pattern) || REGEX_GROUP_SYMBOLS_RE.test(pattern)) { 54 | return true; 55 | } 56 | 57 | if (options.extglob !== false && GLOB_EXTENSION_SYMBOLS_RE.test(pattern)) { 58 | return true; 59 | } 60 | 61 | if (options.braceExpansion !== false && hasBraceExpansion(pattern)) { 62 | return true; 63 | } 64 | 65 | return false; 66 | } 67 | 68 | function hasBraceExpansion(pattern: string): boolean { 69 | const openingBraceIndex = pattern.indexOf('{'); 70 | 71 | if (openingBraceIndex === -1) { 72 | return false; 73 | } 74 | 75 | const closingBraceIndex = pattern.indexOf('}', openingBraceIndex + 1); 76 | 77 | if (closingBraceIndex === -1) { 78 | return false; 79 | } 80 | 81 | const braceContent = pattern.slice(openingBraceIndex, closingBraceIndex); 82 | 83 | return BRACE_EXPANSION_SEPARATORS_RE.test(braceContent); 84 | } 85 | 86 | export function convertToPositivePattern(pattern: Pattern): Pattern { 87 | return isNegativePattern(pattern) ? pattern.slice(1) : pattern; 88 | } 89 | 90 | export function convertToNegativePattern(pattern: Pattern): Pattern { 91 | return `!${pattern}`; 92 | } 93 | 94 | export function isNegativePattern(pattern: Pattern): boolean { 95 | return pattern.startsWith('!') && pattern[1] !== '('; 96 | } 97 | 98 | export function isPositivePattern(pattern: Pattern): boolean { 99 | return !isNegativePattern(pattern); 100 | } 101 | 102 | export function getNegativePatterns(patterns: Pattern[]): Pattern[] { 103 | return patterns.filter((pattern) => isNegativePattern(pattern)); 104 | } 105 | 106 | export function getPositivePatterns(patterns: Pattern[]): Pattern[] { 107 | return patterns.filter((pattern) => isPositivePattern(pattern)); 108 | } 109 | 110 | /** 111 | * Returns patterns that can be applied inside the current directory. 112 | * 113 | * @example 114 | * // ['./*', '*', 'a/*'] 115 | * getPatternsInsideCurrentDirectory(['./*', '*', 'a/*', '../*', './../*']) 116 | */ 117 | export function getPatternsInsideCurrentDirectory(patterns: Pattern[]): Pattern[] { 118 | return patterns.filter((pattern) => !isPatternRelatedToParentDirectory(pattern)); 119 | } 120 | 121 | /** 122 | * Returns patterns to be expanded relative to (outside) the current directory. 123 | * 124 | * @example 125 | * // ['../*', './../*'] 126 | * getPatternsInsideCurrentDirectory(['./*', '*', 'a/*', '../*', './../*']) 127 | */ 128 | export function getPatternsOutsideCurrentDirectory(patterns: Pattern[]): Pattern[] { 129 | return patterns.filter((pattern) => isPatternRelatedToParentDirectory(pattern)); 130 | } 131 | 132 | export function isPatternRelatedToParentDirectory(pattern: Pattern): boolean { 133 | return pattern.startsWith('..') || pattern.startsWith('./..'); 134 | } 135 | 136 | export function getBaseDirectory(pattern: Pattern): string { 137 | return globParent(pattern, { flipBackslashes: false }); 138 | } 139 | 140 | export function hasGlobStar(pattern: Pattern): boolean { 141 | return pattern.includes(GLOBSTAR); 142 | } 143 | 144 | export function endsWithSlashGlobStar(pattern: Pattern): boolean { 145 | return pattern.endsWith(`/${GLOBSTAR}`); 146 | } 147 | 148 | export function isAffectDepthOfReadingPattern(pattern: Pattern): boolean { 149 | const basename = path.basename(pattern); 150 | 151 | return endsWithSlashGlobStar(pattern) || isStaticPattern(basename); 152 | } 153 | 154 | export function expandPatternsWithBraceExpansion(patterns: Pattern[]): Pattern[] { 155 | return patterns.reduce((collection, pattern) => { 156 | return collection.concat(expandBraceExpansion(pattern)); 157 | }, []); 158 | } 159 | 160 | export function expandBraceExpansion(pattern: Pattern): Pattern[] { 161 | const patterns = micromatch.braces(pattern, { expand: true, nodupes: true, keepEscaping: true }); 162 | 163 | /** 164 | * Sort the patterns by length so that the same depth patterns are processed side by side. 165 | * `a/{b,}/{c,}/*` – `['a///*', 'a/b//*', 'a//c/*', 'a/b/c/*']` 166 | */ 167 | patterns.sort((a, b) => a.length - b.length); 168 | 169 | /** 170 | * Micromatch can return an empty string in the case of patterns like `{a,}`. 171 | */ 172 | return patterns.filter((pattern) => pattern !== ''); 173 | } 174 | 175 | export function getPatternParts(pattern: Pattern, options: MicromatchOptions): Pattern[] { 176 | let { parts } = micromatch.scan(pattern, { 177 | ...options, 178 | parts: true, 179 | }); 180 | 181 | /** 182 | * The scan method returns an empty array in some cases. 183 | * See micromatch/picomatch#58 for more details. 184 | */ 185 | if (parts.length === 0) { 186 | parts = [pattern]; 187 | } 188 | 189 | /** 190 | * The scan method does not return an empty part for the pattern with a forward slash. 191 | * This is another part of micromatch/picomatch#58. 192 | */ 193 | if (parts[0].startsWith('/')) { 194 | parts[0] = parts[0].slice(1); 195 | parts.unshift(''); 196 | } 197 | 198 | return parts; 199 | } 200 | 201 | export function makeRe(pattern: Pattern, options: MicromatchOptions): PatternRe { 202 | return micromatch.makeRe(pattern, options); 203 | } 204 | 205 | export function convertPatternsToRe(patterns: Pattern[], options: MicromatchOptions): PatternRe[] { 206 | return patterns.map((pattern) => makeRe(pattern, options)); 207 | } 208 | 209 | export function matchAny(entry: string, patternsRe: PatternRe[]): boolean { 210 | return patternsRe.some((patternRe) => patternRe.test(entry)); 211 | } 212 | 213 | /** 214 | * This package only works with forward slashes as a path separator. 215 | * Because of this, we cannot use the standard `path.normalize` method, because on Windows platform it will use of backslashes. 216 | */ 217 | export function removeDuplicateSlashes(pattern: string): string { 218 | return pattern.replaceAll(DOUBLE_SLASH_RE, '/'); 219 | } 220 | 221 | export function partitionAbsoluteAndRelative(patterns: Pattern[]): [Pattern[], Pattern[]] { 222 | const absolute: Pattern[] = []; 223 | const relative: Pattern[] = []; 224 | 225 | for (const pattern of patterns) { 226 | if (isAbsolute(pattern)) { 227 | absolute.push(pattern); 228 | } else { 229 | relative.push(pattern); 230 | } 231 | } 232 | 233 | return [absolute, relative]; 234 | } 235 | 236 | export function isAbsolute(pattern: string): boolean { 237 | return path.isAbsolute(pattern); 238 | } 239 | -------------------------------------------------------------------------------- /src/utils/stream.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | import * as stream from 'node:stream'; 3 | 4 | import { describe, it } from 'mocha'; 5 | 6 | import * as util from './stream'; 7 | 8 | describe('Utils → Stream', () => { 9 | describe('.merge', () => { 10 | it('should merge two streams into one stream', () => { 11 | const first = new stream.PassThrough(); 12 | const second = new stream.PassThrough(); 13 | 14 | const expected = 3; 15 | 16 | const mergedStream = util.merge([first, second]); 17 | 18 | const actual = mergedStream.listenerCount('close'); 19 | 20 | assert.strictEqual(actual, expected); 21 | }); 22 | 23 | it('should propagate errors into merged stream', (done) => { 24 | const first = new stream.PassThrough(); 25 | const second = new stream.PassThrough(); 26 | 27 | const expected = [1, 2, 3]; 28 | 29 | const mergedStream = util.merge([first, second]); 30 | 31 | const actual: number[] = []; 32 | 33 | mergedStream.on('error', (error: number) => actual.push(error)); 34 | 35 | mergedStream.once('finish', () => { 36 | assert.deepStrictEqual(actual, expected); 37 | 38 | done(); 39 | }); 40 | 41 | first.emit('error', 1); 42 | second.emit('error', 2); 43 | mergedStream.emit('error', 3); 44 | }); 45 | 46 | it('should propagate close event to source streams', (done) => { 47 | const first = new stream.PassThrough(); 48 | const second = new stream.PassThrough(); 49 | 50 | const mergedStream = util.merge([first, second]); 51 | 52 | const expected = [1, 2]; 53 | 54 | const actual: number[] = []; 55 | 56 | first.once('close', () => actual.push(1)); 57 | second.once('close', () => actual.push(2)); 58 | 59 | mergedStream.once('finish', () => { 60 | assert.deepStrictEqual(actual, expected); 61 | 62 | done(); 63 | }); 64 | 65 | mergedStream.emit('close'); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/utils/stream.ts: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/39415662 2 | // eslint-disable-next-line @typescript-eslint/no-require-imports 3 | import merge2 = require('merge2'); 4 | 5 | import type { Readable } from 'node:stream'; 6 | 7 | export function merge(streams: Readable[]): NodeJS.ReadableStream { 8 | const mergedStream = merge2(streams); 9 | 10 | streams.forEach((stream) => { 11 | stream.once('error', (error) => mergedStream.emit('error', error)); 12 | }); 13 | 14 | mergedStream.once('close', () => { 15 | propagateCloseEventToSources(streams); 16 | }); 17 | mergedStream.once('end', () => { 18 | propagateCloseEventToSources(streams); 19 | }); 20 | 21 | return mergedStream; 22 | } 23 | 24 | function propagateCloseEventToSources(streams: Readable[]): void { 25 | streams.forEach((stream) => stream.emit('close')); 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/string.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | 3 | import { describe, it } from 'mocha'; 4 | 5 | import * as util from './string'; 6 | 7 | describe('Utils → String', () => { 8 | describe('.isString', () => { 9 | it('should return true', () => { 10 | const actual = util.isString(''); 11 | 12 | assert.ok(actual); 13 | }); 14 | 15 | it('should return false', () => { 16 | const actual = util.isString(undefined as unknown as string); 17 | 18 | assert.ok(!actual); 19 | }); 20 | }); 21 | 22 | describe('.isEmpty', () => { 23 | it('should return true', () => { 24 | const actual = util.isEmpty(''); 25 | 26 | assert.ok(actual); 27 | }); 28 | 29 | it('should return false', () => { 30 | const actual = util.isEmpty('string'); 31 | 32 | assert.ok(!actual); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/utils/string.ts: -------------------------------------------------------------------------------- 1 | export function isString(input: unknown): input is string { 2 | return typeof input === 'string'; 3 | } 4 | 5 | export function isEmpty(input: string): boolean { 6 | return input === ''; 7 | } 8 | 9 | /** 10 | * Flattens the underlying C structures of a concatenated JavaScript string. 11 | * 12 | * More details: https://github.com/davidmarkclements/flatstr 13 | */ 14 | export function flatHeavilyConcatenatedString(input: string): string { 15 | // @ts-expect-error Another solution can be `.trim`, but it changes the string. 16 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions, no-bitwise, unicorn/prefer-math-trunc 17 | input | 0; 18 | 19 | return input; 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2022"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "resolveJsonModule": true, 8 | 9 | "rootDir": "src", 10 | "outDir": "out", 11 | 12 | "strict": true, 13 | "noImplicitOverride": true, 14 | "noImplicitReturns": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noPropertyAccessFromIndexSignature": true, 18 | "allowUnreachableCode": false, 19 | "allowUnusedLabels": false, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | "forceConsistentCasingInFileNames": true, 23 | "verbatimModuleSyntax": false, 24 | 25 | "downlevelIteration": true, 26 | "declaration": true, 27 | "newLine": "lf", 28 | "stripInternal": true 29 | } 30 | } 31 | --------------------------------------------------------------------------------