├── .github └── workflows │ ├── ci.yml │ └── typedoc.yml ├── .gitignore ├── .prettierignore ├── .tshy ├── build.json ├── commonjs.json └── esm.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── benchclean.cjs ├── benchmark.sh ├── changelog.md ├── examples ├── g.js └── usr-local.js ├── fixup.sh ├── logo ├── glob-solo.png ├── glob.png └── glob.svg ├── make-benchmark-fixture.sh ├── oh-my-glob.gif ├── package-lock.json ├── package.json ├── patterns.sh ├── prof.sh ├── scripts └── make-big-tree.js ├── src ├── bin.mts ├── glob.ts ├── has-magic.ts ├── ignore.ts ├── index.ts ├── pattern.ts ├── processor.ts └── walker.ts ├── tap-snapshots └── test │ ├── bin.ts.test.cjs │ └── root.ts.test.cjs ├── test ├── 00-setup.ts ├── absolute-must-be-strings.ts ├── absolute.ts ├── bash-comparison.ts ├── bash-results.ts ├── bin.ts ├── broken-symlink.ts ├── custom-fs.ts ├── custom-ignore.ts ├── cwd-noent.ts ├── cwd-test.ts ├── dot-relative.ts ├── empty-set.ts ├── escape.ts ├── follow.ts ├── has-magic.ts ├── ignore.ts ├── include-child-matches.ts ├── mark.ts ├── match-base.ts ├── match-parent.ts ├── match-root.ts ├── max-depth.ts ├── memfs.ts ├── nocase-magic-only.ts ├── nodir.ts ├── pattern.ts ├── platform.ts ├── progra-tilde.ts ├── readme-issue.ts ├── realpath.ts ├── root.ts ├── signal.ts ├── slash-cwd.ts ├── stat.ts ├── stream.ts ├── url-cwd.ts ├── windows-paths-fs.ts └── windows-paths-no-escape.ts ├── tsconfig.json └── typedoc.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | node-version: [20.x, 22.x] 10 | platform: 11 | - os: ubuntu-latest 12 | shell: bash 13 | - os: macos-latest 14 | shell: bash 15 | - os: windows-latest 16 | shell: bash 17 | - os: windows-latest 18 | shell: powershell 19 | fail-fast: false 20 | 21 | runs-on: ${{ matrix.platform.os }} 22 | defaults: 23 | run: 24 | shell: ${{ matrix.platform.shell }} 25 | 26 | steps: 27 | - name: Checkout Repository 28 | uses: actions/checkout@v3 29 | 30 | - name: Use Nodejs ${{ matrix.node-version }} 31 | uses: actions/setup-node@v3 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | cache: npm 35 | 36 | - name: Install dependencies 37 | run: npm ci 38 | 39 | - name: Run Tests Windows (incomplete coverage) 40 | if: matrix.platform.os == 'windows-latest' 41 | run: npm test -- -c -t0 --allow-incomplete-coverage 42 | 43 | - name: Run Tests Unix (complete coverage) 44 | if: matrix.platform.os != 'windows-latest' 45 | run: npm test -- -c -t0 46 | -------------------------------------------------------------------------------- /.github/workflows/typedoc.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Use Nodejs ${{ matrix.node-version }} 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: 18.x 37 | cache: npm 38 | - name: Install dependencies 39 | run: npm ci 40 | - name: Generate typedocs 41 | run: npm run typedoc 42 | 43 | - name: Setup Pages 44 | uses: actions/configure-pages@v3 45 | - name: Upload artifact 46 | uses: actions/upload-pages-artifact@v1 47 | with: 48 | path: './docs' 49 | - name: Deploy to GitHub Pages 50 | id: deployment 51 | uses: actions/deploy-pages@v1 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | /deleteme 3 | /old 4 | /*.tap 5 | /dist 6 | /node_modules 7 | /v8.log 8 | /profile.txt 9 | /nyc_output 10 | /.nyc_output 11 | /coverage 12 | /test/fixtures 13 | /bench-working-dir 14 | /scripts/fixture 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.github 2 | /.tap 3 | /node_modules 4 | /tap-snapshots 5 | 6 | /scripts/fixtures 7 | /.tap 8 | /.tshy 9 | /dist 10 | /docs 11 | /example 12 | /node_modules 13 | /tap-snapshots 14 | /test/**/fixture 15 | /test/**/fixtures 16 | -------------------------------------------------------------------------------- /.tshy/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "../src", 5 | "module": "nodenext", 6 | "moduleResolution": "nodenext" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.tshy/commonjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./build.json", 3 | "include": [ 4 | "../src/**/*.ts", 5 | "../src/**/*.cts", 6 | "../src/**/*.tsx", 7 | "../src/**/*.json" 8 | ], 9 | "exclude": [ 10 | "../src/**/*.mts", 11 | "../src/package.json" 12 | ], 13 | "compilerOptions": { 14 | "outDir": "../.tshy-build/commonjs" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.tshy/esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./build.json", 3 | "include": [ 4 | "../src/**/*.ts", 5 | "../src/**/*.mts", 6 | "../src/**/*.tsx", 7 | "../src/**/*.json" 8 | ], 9 | "exclude": [ 10 | "../src/package.json" 11 | ], 12 | "compilerOptions": { 13 | "outDir": "../.tshy-build/esm" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Any change to behavior (including bugfixes) must come with a test. 2 | 3 | Patches that fail tests or reduce performance will be rejected. 4 | 5 | ```sh 6 | # to run tests 7 | npm test 8 | 9 | # to re-generate test fixtures 10 | npm run test-regen 11 | 12 | # to benchmark against bash/zsh 13 | npm run bench 14 | 15 | # to profile javascript 16 | npm run prof 17 | ``` 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) 2009-2023 Isaac Z. Schlueter and Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /benchclean.cjs: -------------------------------------------------------------------------------- 1 | var rimraf = require('rimraf') 2 | var bf = './bench-working-dir/fixture' 3 | rimraf.sync(bf) 4 | -------------------------------------------------------------------------------- /benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export CDPATH= 3 | set -e 4 | 5 | . patterns.sh 6 | 7 | bash make-benchmark-fixture.sh 8 | wd=$PWD 9 | 10 | mkdir -p "$wd/bench-working-dir/fixture" 11 | cd "$wd/bench-working-dir" 12 | cat > "$wd/bench-working-dir/package.json" </dev/null; npm i --silent) 27 | fi 28 | 29 | tt () { 30 | time "$@" 31 | } 32 | 33 | t () { 34 | rm -f stderr stdout 35 | tt "$@" 2>stderr >stdout || (cat stderr >&2 ; exit 1 ) 36 | echo $(cat stderr | grep real | awk -F $'\t' '{ print $2 }' || true)' '\ 37 | $(cat stdout) 38 | # rm -f stderr stdout 39 | } 40 | 41 | # warm up the fs cache so we don't get a spurious slow first result 42 | bash -c 'for i in **; do :; done' 43 | 44 | cd "$wd/bench-working-dir/fixture" 45 | 46 | for p in "${patterns[@]}"; do 47 | echo 48 | echo "--- pattern: '$p' ---" 49 | 50 | # if [[ "`bash --version`" =~ version\ 4 ]] || [[ "`bash --version`" =~ version\ 5 ]]; then 51 | # echo -n $'bash \t' 52 | # t bash -c 'shopt -s globstar; echo '"$p"' | wc -w' 53 | # fi 54 | 55 | # if type zsh &>/dev/null; then 56 | # echo -n $'zsh \t' 57 | # t zsh -c 'echo '"$p"' | wc -w' 58 | # fi 59 | 60 | # echo -n $'glob v7 sync \t' 61 | # t node -e ' 62 | # var glob=require(process.argv[1]) 63 | # console.log(glob.sync(process.argv[2]).length) 64 | # ' "$wd/bench-working-dir/node_modules/glob7" "$p" 65 | 66 | # echo -n $'glob v7 async \t' 67 | # t node -e ' 68 | # var glob=require(process.argv[1]) 69 | # glob(process.argv[2], (er, files) => { 70 | # console.log(files.length) 71 | # })' "$wd/bench-working-dir/node_modules/glob7" "$p" 72 | 73 | echo '~~ sync ~~' 74 | 75 | echo -n $'fast-glob sync \t' 76 | cat > "$wd"/bench-working-dir/fast-glob-sync.cjs < "$wd"/bench-working-dir/globby-sync.mjs < "$wd"/bench-working-dir/node-fs-glob-sync.js < "$wd/bench-working-dir/sync.cjs" < "$wd/bench-working-dir/async.cjs" < console.log(files.length)) 109 | #CJS 110 | # t node "$wd/bench-working-dir/async.cjs" "$p" 111 | 112 | # echo -n $'glob v8 sync \t' 113 | # cat > "$wd/bench-working-dir/glob-8-sync.cjs" < "$wd/bench-working-dir/sync.mjs" < "$wd/bench-working-dir/stream-sync.mjs" < c++) 132 | .on('end', () => console.log(c)) 133 | MJS 134 | t node "$wd/bench-working-dir/stream-sync.mjs" "$p" 135 | 136 | echo '~~ async ~~' 137 | 138 | echo -n $'fast-glob async \t' 139 | cat > "$wd"/bench-working-dir/fast-glob-async.cjs < console.log(r.length)) 142 | CJS 143 | t node "$wd/bench-working-dir/fast-glob-async.cjs" "$p" 144 | 145 | echo -n $'globby async \t' 146 | cat > "$wd"/bench-working-dir/globby-async.mjs < { 149 | console.log(files.length) 150 | }) 151 | MJS 152 | t node "$wd/bench-working-dir/globby-async.mjs" "$p" 153 | 154 | if node -e "require('fs').glob || process.exit(1)"; then 155 | echo -n $'fs.glob \t' 156 | cat > "$wd"/bench-working-dir/node-fs-glob.js < { 159 | console.log(er ? 0 : results.length) 160 | }) 161 | CJS 162 | t node "$wd/bench-working-dir/node-fs-glob.js" "$p" 163 | fi 164 | 165 | # echo -n $'glob v8 async \t' 166 | # cat > "$wd/bench-working-dir/glob-8-async.cjs" < 169 | # console.log(results.length) 170 | # ) 171 | # CJS 172 | # t node "$wd/bench-working-dir/glob-8-async.cjs" "$p" 173 | 174 | echo -n $'current glob async mjs \t' 175 | cat > "$wd/bench-working-dir/async.mjs" < console.log(files.length)) 178 | MJS 179 | t node "$wd/bench-working-dir/async.mjs" "$p" 180 | 181 | echo -n $'current glob stream \t' 182 | cat > "$wd/bench-working-dir/stream.mjs" < c++) 187 | .on('end', () => console.log(c)) 188 | MJS 189 | t node "$wd/bench-working-dir/stream.mjs" "$p" 190 | 191 | # echo -n $'current glob sync cjs -e \t' 192 | # t node -e ' 193 | # console.log(require(process.argv[1]).sync(process.argv[2]).length) 194 | # ' "$wd/dist/cjs/index-cjs.js" "$p" 195 | 196 | # echo -n $'current glob async cjs -e\t' 197 | # t node -e ' 198 | # require(process.argv[1])(process.argv[2]).then((files) => console.log(files.length)) 199 | # ' "$wd/dist/cjs/index-cjs.js" "$p" 200 | 201 | done 202 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # changeglob 2 | 3 | ## 11.0 4 | 5 | - Drop support for node before v20 6 | 7 | ## 10.4 8 | 9 | - Add `includeChildMatches: false` option 10 | - Export the `Ignore` class 11 | 12 | ## 10.3 13 | 14 | - Add `--default -p` flag to provide a default pattern 15 | - exclude symbolic links to directories when `follow` and `nodir` 16 | are both set 17 | 18 | ## 10.2 19 | 20 | - Add glob cli 21 | 22 | ## 10.1 23 | 24 | - Return `'.'` instead of the empty string `''` when the current 25 | working directory is returned as a match. 26 | - Add `posix: true` option to return `/` delimited paths, even on 27 | Windows. 28 | 29 | ## 10.0.0 30 | 31 | - No default exports, only named exports 32 | 33 | ## 9.3.3 34 | 35 | - Upgraded minimatch to v8, adding support for any degree of 36 | nested extglob patterns. 37 | 38 | ## 9.3 39 | 40 | - Add aliases for methods. `glob.sync`, `glob.stream`, 41 | `glob.stream.sync`, etc. 42 | 43 | ## 9.2 44 | 45 | - Support using a custom fs object, which is passed to PathScurry 46 | - add maxDepth option 47 | - add stat option 48 | - add custom Ignore support 49 | 50 | ## 9.1 51 | 52 | - Bring back the `root` option, albeit with slightly different 53 | semantics than in v8 and before. 54 | - Support `{ absolute:false }` option to explicitly always return 55 | relative paths. An unset `absolute` setting will still return 56 | absolute or relative paths based on whether the pattern is 57 | absolute. 58 | - Add `magicalBraces` option to treat brace expansion as "magic" 59 | in the `hasMagic` function. 60 | - Add `dotRelative` option 61 | - Add `escape()` and `unescape()` methods 62 | 63 | ## 9.0 64 | 65 | This is a full rewrite, with significant API and algorithm 66 | changes. 67 | 68 | ### High-Level Feature and API Surface Changes 69 | 70 | - Only support node 16 and higher. 71 | - Promise API instead of callbacks. 72 | - Exported function names have changed, as have the methods on 73 | the Glob class. See API documentation for details. 74 | - Accept pattern as string or array of strings. 75 | - Hybrid module distribution. 76 | - Full TypeScript support. 77 | - Exported `Glob` class is no longer an event emitter. 78 | - Exported `Glob` class has `walk()`, `walkSync()`, `stream()`, 79 | `streamSync()`, `iterate()`, `iterateSync()` methods, and is 80 | both an async and sync Generator. 81 | - First class support for UNC paths and drive letters on Windows. 82 | Note that _glob patterns_ must still use `/` as a path 83 | separator, unless the `windowsPathsNoEscape` option is set, in 84 | which case glob patterns cannot be escaped with `\`. 85 | - Paths are returned in the canonical formatting for the platform 86 | in question. 87 | - The `hasMagic` method will return false for patterns that only 88 | contain brace expansion, but no other "magic" glob characters. 89 | - Patterns ending in `/` will still be restricted to matching 90 | directories, but will not have a `/` appended in the results. 91 | In general, results will be in their default relative or 92 | absolute forms, without any extraneous `/` and `.` characters, 93 | unlike shell matches. (The `mark` option may still be used to 94 | _always_ mark directory matches with a trailing `/` or `\`.) 95 | - An options argument is required for the `Glob` class 96 | constructor. `{}` may be provided to accept all default 97 | options. 98 | 99 | ### Options Changes 100 | 101 | - Removed `root` option and mounting behavior. 102 | - Removed `stat` option. It's slow and pointless. (Could bring 103 | back easily if there's demand, but items are already statted in 104 | cases where it's relevant, such as `nodir:true` or 105 | `mark:true`.) 106 | - Simplified `cwd` behavior so it is far less magical, and relies 107 | less on platform-specific absolute path representations. 108 | - `cwd` can be a File URL or a string path. 109 | - More efficient handling for absolute patterns. (That is, 110 | patterns that start with `/` on any platform, or start with a 111 | drive letter or UNC path on Windows.) 112 | - Removed `silent` and `strict` options. Any readdir errors are 113 | simply treated as "the directory could not be read", and it is 114 | treated as a normal file entry instead, like shells do. 115 | - Removed `fs` option. This module only operates on the real 116 | filesystem. (Could bring back if there's demand for it, but 117 | it'd be an update to PathScurry, not Glob.) 118 | - `nonull:true` is no longer supported. 119 | - `withFileTypes:true` option added, to get `Path` objects. 120 | These are a bit like a Dirent, but can do a lot more. See 121 | 122 | - `nounique:true` is no longer supported. Result sets are always 123 | unique. 124 | - `nosort:true` is no longer supported. Result sets are never 125 | sorted. 126 | - When the `nocase` option is used, the assumption is that it 127 | reflects the case sensitivity of the _filesystem itself_. 128 | Using case-insensitive matching on a case-sensitive filesystem, 129 | or vice versa, may thus result in more or fewer matches than 130 | expected. In general, it should only be used when the 131 | filesystem is known to differ from the platform default. 132 | - `realpath:true` no longer implies `absolute:true`. The 133 | relative path to the realpath will be emitted when `absolute` 134 | is not set. 135 | - `realpath:true` will cause invalid symbolic links to be 136 | omitted, rather than matching the link itself. 137 | 138 | ### Performance and Algorithm Changes 139 | 140 | - Massive performance improvements. 141 | - Removed nearly all stat calls, in favor of using 142 | `withFileTypes:true` with `fs.readdir()`. 143 | - Replaced most of the caching with a 144 | [PathScurry](http://npm.im/path-scurry) based implementation. 145 | - More correct handling of `**` vs `./**`, following Bash 146 | semantics, where a `**` is followed one time only if it is not 147 | the first item in the pattern. 148 | 149 | ## 8.1 150 | 151 | - Add `windowsPathsNoEscape` option 152 | 153 | ## 8.0 154 | 155 | - Only support node v12 and higher 156 | - `\` is now **only** used as an escape character, and never as a 157 | path separator in glob patterns, so that Windows users have a 158 | way to match against filenames containing literal glob pattern 159 | characters. 160 | - Glob pattern paths **must** use forward-slashes as path 161 | separators, since `\` is an escape character to match literal 162 | glob pattern characters. 163 | - (8.0.2) `cwd` and `root` will always be automatically coerced 164 | to use `/` as path separators on Windows, as they cannot 165 | contain glob patterns anyway, and are often supplied by 166 | `path.resolve()` and other methods that will use `\` path 167 | separators by default. 168 | 169 | ## 7.2 170 | 171 | - Add fs option to allow passing virtual filesystem 172 | 173 | ## 7.1 174 | 175 | - Ignore stat errors that are not `ENOENT` to work around Windows issues. 176 | - Support using root and absolute options together 177 | - Bring back lumpy space princess 178 | - force 'en' locale in string sorting 179 | 180 | ## 7.0 181 | 182 | - Raise error if `options.cwd` is specified, and not a directory 183 | 184 | ## 6.0 185 | 186 | - Remove comment and negation pattern support 187 | - Ignore patterns are always in `dot:true` mode 188 | 189 | ## 5.0 190 | 191 | - Deprecate comment and negation patterns 192 | - Fix regression in `mark` and `nodir` options from making all cache 193 | keys absolute path. 194 | - Abort if `fs.readdir` returns an error that's unexpected 195 | - Don't emit `match` events for ignored items 196 | - Treat ENOTSUP like ENOTDIR in readdir 197 | 198 | ## 4.5 199 | 200 | - Add `options.follow` to always follow directory symlinks in globstar 201 | - Add `options.realpath` to call `fs.realpath` on all results 202 | - Always cache based on absolute path 203 | 204 | ## 4.4 205 | 206 | - Add `options.ignore` 207 | - Fix handling of broken symlinks 208 | 209 | ## 4.3 210 | 211 | - Bump minimatch to 2.x 212 | - Pass all tests on Windows 213 | 214 | ## 4.2 215 | 216 | - Add `glob.hasMagic` function 217 | - Add `options.nodir` flag 218 | 219 | ## 4.1 220 | 221 | - Refactor sync and async implementations for performance 222 | - Throw if callback provided to sync glob function 223 | - Treat symbolic links in globstar results the same as Bash 4.3 224 | 225 | ## 4.0 226 | 227 | - Use `^` for dependency versions (bumped major because this breaks 228 | older npm versions) 229 | - Ensure callbacks are only ever called once 230 | - switch to ISC license 231 | 232 | ## 3.x 233 | 234 | - Rewrite in JavaScript 235 | - Add support for setting root, cwd, and windows support 236 | - Cache many fs calls 237 | - Add globstar support 238 | - emit match events 239 | 240 | ## 2.x 241 | 242 | - Use `glob.h` and `fnmatch.h` from NetBSD 243 | 244 | ## 1.x 245 | 246 | - `glob.h` static binding. 247 | -------------------------------------------------------------------------------- /examples/g.js: -------------------------------------------------------------------------------- 1 | var Glob = require('../').Glob 2 | 3 | var pattern = 'test/a/**/[cg]/../[cg]' 4 | console.log(pattern) 5 | 6 | var mg = new Glob(pattern, { mark: true, sync: true }, function ( 7 | er, 8 | matches, 9 | ) { 10 | console.log('matches', matches) 11 | }) 12 | console.log('after') 13 | -------------------------------------------------------------------------------- /examples/usr-local.js: -------------------------------------------------------------------------------- 1 | var Glob = require('../').Glob 2 | 3 | var pattern = '{./*/*,/*,/usr/local/*}' 4 | console.log(pattern) 5 | 6 | var mg = new Glob(pattern, { mark: true }, function (er, matches) { 7 | console.log('matches', matches) 8 | }) 9 | console.log('after') 10 | -------------------------------------------------------------------------------- /fixup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cat >dist-tmp/cjs/package.json <dist-tmp/mjs/package.json < (https://blog.izs.me/)", 3 | "name": "glob", 4 | "description": "the most correct and second fastest glob implementation in JavaScript", 5 | "version": "11.0.2", 6 | "type": "module", 7 | "tshy": { 8 | "main": true, 9 | "exports": { 10 | "./package.json": "./package.json", 11 | ".": "./src/index.ts" 12 | } 13 | }, 14 | "bin": "./dist/esm/bin.mjs", 15 | "main": "./dist/commonjs/index.js", 16 | "types": "./dist/commonjs/index.d.ts", 17 | "exports": { 18 | "./package.json": "./package.json", 19 | ".": { 20 | "import": { 21 | "types": "./dist/esm/index.d.ts", 22 | "default": "./dist/esm/index.js" 23 | }, 24 | "require": { 25 | "types": "./dist/commonjs/index.d.ts", 26 | "default": "./dist/commonjs/index.js" 27 | } 28 | } 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git://github.com/isaacs/node-glob.git" 33 | }, 34 | "files": [ 35 | "dist" 36 | ], 37 | "scripts": { 38 | "preversion": "npm test", 39 | "postversion": "npm publish", 40 | "prepublishOnly": "npm run benchclean; git push origin --follow-tags", 41 | "prepare": "tshy", 42 | "pretest": "npm run prepare", 43 | "presnap": "npm run prepare", 44 | "test": "tap", 45 | "snap": "tap", 46 | "format": "prettier --write . --log-level warn", 47 | "typedoc": "typedoc --tsconfig .tshy/esm.json ./src/*.ts", 48 | "profclean": "rm -f v8.log profile.txt", 49 | "test-regen": "npm run profclean && TEST_REGEN=1 node --no-warnings --loader ts-node/esm test/00-setup.ts", 50 | "prebench": "npm run prepare", 51 | "bench": "bash benchmark.sh", 52 | "preprof": "npm run prepare", 53 | "prof": "bash prof.sh", 54 | "benchclean": "node benchclean.cjs" 55 | }, 56 | "prettier": { 57 | "experimentalTernaries": true, 58 | "semi": false, 59 | "printWidth": 75, 60 | "tabWidth": 2, 61 | "useTabs": false, 62 | "singleQuote": true, 63 | "jsxSingleQuote": false, 64 | "bracketSameLine": true, 65 | "arrowParens": "avoid", 66 | "endOfLine": "lf" 67 | }, 68 | "dependencies": { 69 | "foreground-child": "^3.1.0", 70 | "jackspeak": "^4.0.1", 71 | "minimatch": "^10.0.0", 72 | "minipass": "^7.1.2", 73 | "package-json-from-dist": "^1.0.0", 74 | "path-scurry": "^2.0.0" 75 | }, 76 | "devDependencies": { 77 | "@types/node": "^20.11.30", 78 | "memfs": "^4.9.3", 79 | "mkdirp": "^3.0.1", 80 | "prettier": "^3.2.5", 81 | "rimraf": "^5.0.7", 82 | "sync-content": "^1.0.2", 83 | "tap": "^20.0.3", 84 | "tshy": "^2.0.1", 85 | "typedoc": "^0.26.3" 86 | }, 87 | "tap": { 88 | "before": "test/00-setup.ts" 89 | }, 90 | "license": "ISC", 91 | "funding": { 92 | "url": "https://github.com/sponsors/isaacs" 93 | }, 94 | "engines": { 95 | "node": "20 || >=22" 96 | }, 97 | "module": "./dist/esm/index.js" 98 | } 99 | -------------------------------------------------------------------------------- /patterns.sh: -------------------------------------------------------------------------------- 1 | patterns=( 2 | '{0000,0,1111,1}/{0000,0,1111,1}/{0000,0,1111,1}/**' 3 | 4 | '**' 5 | '**/..' 6 | 7 | # some of these aren't particularly "representative" of real-world 8 | # glob patterns, but they're here to highlight pathological perf 9 | # cases that I found while working on the rewrite of this library. 10 | './**/0/**/0/**/0/**/0/**/*.txt' 11 | './**/[01]/**/[12]/**/[23]/**/[45]/**/*.txt' 12 | './**/0/**/0/**/*.txt' 13 | 14 | '**/*.txt' 15 | '{**/*.txt,**/?/**/*.txt,**/?/**/?/**/*.txt,**/?/**/?/**/?/**/*.txt,**/?/**/?/**/?/**/?/**/*.txt}' 16 | '**/5555/0000/*.txt' 17 | 18 | './**/0/**/../[01]/**/0/../**/0/*.txt' 19 | '**/????/????/????/????/*.txt' 20 | 21 | 22 | './{**/?{/**/?{/**/?{/**/?,,,,},,,,},,,,},,,}/**/*.txt' 23 | 24 | 25 | '**/!(0|9).txt' 26 | 27 | './{*/**/../{*/**/../{*/**/../{*/**/../{*/**,,,,},,,,},,,,},,,,},,,,}/*.txt' 28 | './*/**/../*/**/../*/**/../*/**/../*/**/../*/**/../*/**/../*/**/*.txt' 29 | './*/**/../*/**/../*/**/../*/**/../*/**/*.txt' 30 | './0/**/../1/**/../2/**/../3/**/../4/**/../5/**/../6/**/../7/**/*.txt' 31 | './**/?/**/?/**/?/**/?/**/*.txt' 32 | '**/*/**/*/**/*/**/*/**' 33 | # '5555/0000/**/*.txt' 34 | # '*/*/9/**/**/**/**/*/**/**/*.txt' 35 | './**/*/**/*/**/*/**/*/**/*.txt' 36 | '**/*.txt' 37 | # './**/*.txt' 38 | './**/**/**/**/**/**/**/**/*.txt' 39 | '**/*/*.txt' 40 | '**/*/**/*.txt' 41 | '**/[0-9]/**/*.txt' 42 | # '0/@([5-9]/*.txt|8/**)' 43 | # '[0-9]/[0-9]/[0-9]/[0-9]/[0-9].txt' 44 | # /**/**/**/**//////**/**//*.txt' 45 | # '**/[5-9]/*.txt' 46 | # '[678]/**/2.txt' 47 | # '0/!(1|2)@(4|5)/**/**/**/**/*.txt' 48 | # '0/!(1|2|@(4|5))/**/**/**/**/*.txt' 49 | ) 50 | -------------------------------------------------------------------------------- /prof.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export CDPATH= 3 | set -e 4 | set -x 5 | 6 | . patterns.sh 7 | 8 | bash -x make-benchmark-fixture.sh 9 | wd=$PWD 10 | tmp="$wd/bench-working-dir" 11 | cd "$tmp" 12 | 13 | export __GLOB_PROFILE__=1 14 | 15 | cat > "profscript.mjs" < { 22 | await glob("./fixture/" + p) 23 | })) 24 | MJS 25 | 26 | node --prof profscript.mjs "${patterns[@]}" &> profile.out 27 | mkdir -p profiles 28 | d=./profiles/$(date +%s) 29 | mv isolate*.log ${d}.log 30 | node --prof-process ${d}.log > ${d}.txt 31 | cp ${d}.txt ../profile.txt 32 | #cat ${d}.txt 33 | -------------------------------------------------------------------------------- /scripts/make-big-tree.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const mkdirp = require('mkdirp') 3 | const { readFileSync } = require('fs') 4 | const { writeFile } = require('fs/promises') 5 | const rimraf = require('rimraf') 6 | const filesPerDir = 10 7 | const dirsPerDir = 5 8 | const max = (module === require.main && +process.argv[2]) || 1_000_000 9 | const { now } = performance 10 | let lastReported = now() 11 | 12 | const report = s => { 13 | if (!process.stderr.isTTY) return 14 | process.stderr.write('\r' + s.padEnd(40)) 15 | } 16 | 17 | let made = 0 18 | const makeStep = async dir => { 19 | if (now() - lastReported > 250) report('growing: ' + made) 20 | const promises = [] 21 | for (let i = 0; i < filesPerDir && made < max; i++) { 22 | made++ 23 | promises.push(writeFile(`${dir}/${i}.txt`, '')) 24 | } 25 | await Promise.all(promises) 26 | 27 | const childDirs = [] 28 | for (let i = 0; i < dirsPerDir && made < max; i++) { 29 | made++ 30 | await mkdirp(`${dir}/${i}`) 31 | childDirs.push(makeStep(`${dir}/${i}`)) 32 | } 33 | await Promise.all(childDirs) 34 | } 35 | 36 | const make = async root => { 37 | try { 38 | const already = +readFileSync(`${root}/bigtree.txt`) 39 | if (already === max) { 40 | console.log('already done!') 41 | return 42 | } 43 | } catch (_) {} 44 | report('chop down previous bigtree...') 45 | await rimraf(root + '/bigtree') 46 | report('creating bigtree...') 47 | report('\n') 48 | await mkdirp(root + '/bigtree') 49 | await makeStep(root + '/bigtree') 50 | await writeFile(`${root}/bigtree.txt`, `${max}`) 51 | } 52 | 53 | make(__dirname + '/fixture').then(() => { 54 | if (process.stderr.isTTY) process.stderr.write('\r'.padEnd(40) + '\r') 55 | console.log('done') 56 | }) 57 | -------------------------------------------------------------------------------- /src/bin.mts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { foregroundChild } from 'foreground-child' 3 | import { existsSync } from 'fs' 4 | import { jack } from 'jackspeak' 5 | import { loadPackageJson } from 'package-json-from-dist' 6 | import { join } from 'path' 7 | import { globStream } from './index.js' 8 | 9 | const { version } = loadPackageJson(import.meta.url, '../package.json') 10 | 11 | const j = jack({ 12 | usage: 'glob [options] [ [ ...]]', 13 | }) 14 | .description( 15 | ` 16 | Glob v${version} 17 | 18 | Expand the positional glob expression arguments into any matching file 19 | system paths found. 20 | `, 21 | ) 22 | .opt({ 23 | cmd: { 24 | short: 'c', 25 | hint: 'command', 26 | description: `Run the command provided, passing the glob expression 27 | matches as arguments.`, 28 | }, 29 | }) 30 | .opt({ 31 | default: { 32 | short: 'p', 33 | hint: 'pattern', 34 | description: `If no positional arguments are provided, glob will use 35 | this pattern`, 36 | }, 37 | }) 38 | .flag({ 39 | all: { 40 | short: 'A', 41 | description: `By default, the glob cli command will not expand any 42 | arguments that are an exact match to a file on disk. 43 | 44 | This prevents double-expanding, in case the shell expands 45 | an argument whose filename is a glob expression. 46 | 47 | For example, if 'app/*.ts' would match 'app/[id].ts', then 48 | on Windows powershell or cmd.exe, 'glob app/*.ts' will 49 | expand to 'app/[id].ts', as expected. However, in posix 50 | shells such as bash or zsh, the shell will first expand 51 | 'app/*.ts' to a list of filenames. Then glob will look 52 | for a file matching 'app/[id].ts' (ie, 'app/i.ts' or 53 | 'app/d.ts'), which is unexpected. 54 | 55 | Setting '--all' prevents this behavior, causing glob 56 | to treat ALL patterns as glob expressions to be expanded, 57 | even if they are an exact match to a file on disk. 58 | 59 | When setting this option, be sure to enquote arguments 60 | so that the shell will not expand them prior to passing 61 | them to the glob command process. 62 | `, 63 | }, 64 | absolute: { 65 | short: 'a', 66 | description: 'Expand to absolute paths', 67 | }, 68 | 'dot-relative': { 69 | short: 'd', 70 | description: `Prepend './' on relative matches`, 71 | }, 72 | mark: { 73 | short: 'm', 74 | description: `Append a / on any directories matched`, 75 | }, 76 | posix: { 77 | short: 'x', 78 | description: `Always resolve to posix style paths, using '/' as the 79 | directory separator, even on Windows. Drive letter 80 | absolute matches on Windows will be expanded to their 81 | full resolved UNC maths, eg instead of 'C:\\foo\\bar', 82 | it will expand to '//?/C:/foo/bar'. 83 | `, 84 | }, 85 | 86 | follow: { 87 | short: 'f', 88 | description: `Follow symlinked directories when expanding '**'`, 89 | }, 90 | realpath: { 91 | short: 'R', 92 | description: `Call 'fs.realpath' on all of the results. In the case 93 | of an entry that cannot be resolved, the entry is 94 | omitted. This incurs a slight performance penalty, of 95 | course, because of the added system calls.`, 96 | }, 97 | stat: { 98 | short: 's', 99 | description: `Call 'fs.lstat' on all entries, whether required or not 100 | to determine if it's a valid match.`, 101 | }, 102 | 'match-base': { 103 | short: 'b', 104 | description: `Perform a basename-only match if the pattern does not 105 | contain any slash characters. That is, '*.js' would be 106 | treated as equivalent to '**/*.js', matching js files 107 | in all directories. 108 | `, 109 | }, 110 | 111 | dot: { 112 | description: `Allow patterns to match files/directories that start 113 | with '.', even if the pattern does not start with '.' 114 | `, 115 | }, 116 | nobrace: { 117 | description: 'Do not expand {...} patterns', 118 | }, 119 | nocase: { 120 | description: `Perform a case-insensitive match. This defaults to 121 | 'true' on macOS and Windows platforms, and false on 122 | all others. 123 | 124 | Note: 'nocase' should only be explicitly set when it is 125 | known that the filesystem's case sensitivity differs 126 | from the platform default. If set 'true' on 127 | case-insensitive file systems, then the walk may return 128 | more or less results than expected. 129 | `, 130 | }, 131 | nodir: { 132 | description: `Do not match directories, only files. 133 | 134 | Note: to *only* match directories, append a '/' at the 135 | end of the pattern. 136 | `, 137 | }, 138 | noext: { 139 | description: `Do not expand extglob patterns, such as '+(a|b)'`, 140 | }, 141 | noglobstar: { 142 | description: `Do not expand '**' against multiple path portions. 143 | Ie, treat it as a normal '*' instead.`, 144 | }, 145 | 'windows-path-no-escape': { 146 | description: `Use '\\' as a path separator *only*, and *never* as an 147 | escape character. If set, all '\\' characters are 148 | replaced with '/' in the pattern.`, 149 | }, 150 | }) 151 | .num({ 152 | 'max-depth': { 153 | short: 'D', 154 | description: `Maximum depth to traverse from the current 155 | working directory`, 156 | }, 157 | }) 158 | .opt({ 159 | cwd: { 160 | short: 'C', 161 | description: 'Current working directory to execute/match in', 162 | default: process.cwd(), 163 | }, 164 | root: { 165 | short: 'r', 166 | description: `A string path resolved against the 'cwd', which is 167 | used as the starting point for absolute patterns that 168 | start with '/' (but not drive letters or UNC paths 169 | on Windows). 170 | 171 | Note that this *doesn't* necessarily limit the walk to 172 | the 'root' directory, and doesn't affect the cwd 173 | starting point for non-absolute patterns. A pattern 174 | containing '..' will still be able to traverse out of 175 | the root directory, if it is not an actual root directory 176 | on the filesystem, and any non-absolute patterns will 177 | still be matched in the 'cwd'. 178 | 179 | To start absolute and non-absolute patterns in the same 180 | path, you can use '--root=' to set it to the empty 181 | string. However, be aware that on Windows systems, a 182 | pattern like 'x:/*' or '//host/share/*' will *always* 183 | start in the 'x:/' or '//host/share/' directory, 184 | regardless of the --root setting. 185 | `, 186 | }, 187 | platform: { 188 | description: `Defaults to the value of 'process.platform' if 189 | available, or 'linux' if not. Setting --platform=win32 190 | on non-Windows systems may cause strange behavior!`, 191 | validOptions: [ 192 | 'aix', 193 | 'android', 194 | 'darwin', 195 | 'freebsd', 196 | 'haiku', 197 | 'linux', 198 | 'openbsd', 199 | 'sunos', 200 | 'win32', 201 | 'cygwin', 202 | 'netbsd', 203 | ], 204 | }, 205 | }) 206 | .optList({ 207 | ignore: { 208 | short: 'i', 209 | description: `Glob patterns to ignore`, 210 | }, 211 | }) 212 | .flag({ 213 | debug: { 214 | short: 'v', 215 | description: `Output a huge amount of noisy debug information about 216 | patterns as they are parsed and used to match files.`, 217 | }, 218 | version: { 219 | short: 'V', 220 | description: `Output the version (${version})`, 221 | }, 222 | help: { 223 | short: 'h', 224 | description: 'Show this usage information', 225 | }, 226 | }) 227 | 228 | try { 229 | const { positionals, values } = j.parse() 230 | if (values.version) { 231 | console.log(version) 232 | process.exit(0) 233 | } 234 | if (values.help) { 235 | console.log(j.usage()) 236 | process.exit(0) 237 | } 238 | if (positionals.length === 0 && !values.default) 239 | throw 'No patterns provided' 240 | if (positionals.length === 0 && values.default) 241 | positionals.push(values.default) 242 | const patterns = 243 | values.all ? positionals : positionals.filter(p => !existsSync(p)) 244 | const matches = 245 | values.all ? 246 | [] 247 | : positionals.filter(p => existsSync(p)).map(p => join(p)) 248 | const stream = globStream(patterns, { 249 | absolute: values.absolute, 250 | cwd: values.cwd, 251 | dot: values.dot, 252 | dotRelative: values['dot-relative'], 253 | follow: values.follow, 254 | ignore: values.ignore, 255 | mark: values.mark, 256 | matchBase: values['match-base'], 257 | maxDepth: values['max-depth'], 258 | nobrace: values.nobrace, 259 | nocase: values.nocase, 260 | nodir: values.nodir, 261 | noext: values.noext, 262 | noglobstar: values.noglobstar, 263 | platform: values.platform as undefined | NodeJS.Platform, 264 | realpath: values.realpath, 265 | root: values.root, 266 | stat: values.stat, 267 | debug: values.debug, 268 | posix: values.posix, 269 | }) 270 | 271 | const cmd = values.cmd 272 | if (!cmd) { 273 | matches.forEach(m => console.log(m)) 274 | stream.on('data', f => console.log(f)) 275 | } else { 276 | stream.on('data', f => matches.push(f)) 277 | stream.on('end', () => foregroundChild(cmd, matches, { shell: true })) 278 | } 279 | } catch (e) { 280 | console.error(j.usage()) 281 | console.error(e instanceof Error ? e.message : String(e)) 282 | process.exit(1) 283 | } 284 | -------------------------------------------------------------------------------- /src/glob.ts: -------------------------------------------------------------------------------- 1 | import { Minimatch, MinimatchOptions } from 'minimatch' 2 | import { Minipass } from 'minipass' 3 | import { fileURLToPath } from 'node:url' 4 | import { 5 | FSOption, 6 | Path, 7 | PathScurry, 8 | PathScurryDarwin, 9 | PathScurryPosix, 10 | PathScurryWin32, 11 | } from 'path-scurry' 12 | import { IgnoreLike } from './ignore.js' 13 | import { Pattern } from './pattern.js' 14 | import { GlobStream, GlobWalker } from './walker.js' 15 | 16 | export type MatchSet = Minimatch['set'] 17 | export type GlobParts = Exclude 18 | 19 | // if no process global, just call it linux. 20 | // so we default to case-sensitive, / separators 21 | const defaultPlatform: NodeJS.Platform = 22 | ( 23 | typeof process === 'object' && 24 | process && 25 | typeof process.platform === 'string' 26 | ) ? 27 | process.platform 28 | : 'linux' 29 | 30 | /** 31 | * A `GlobOptions` object may be provided to any of the exported methods, and 32 | * must be provided to the `Glob` constructor. 33 | * 34 | * All options are optional, boolean, and false by default, unless otherwise 35 | * noted. 36 | * 37 | * All resolved options are added to the Glob object as properties. 38 | * 39 | * If you are running many `glob` operations, you can pass a Glob object as the 40 | * `options` argument to a subsequent operation to share the previously loaded 41 | * cache. 42 | */ 43 | export interface GlobOptions { 44 | /** 45 | * Set to `true` to always receive absolute paths for 46 | * matched files. Set to `false` to always return relative paths. 47 | * 48 | * When this option is not set, absolute paths are returned for patterns 49 | * that are absolute, and otherwise paths are returned that are relative 50 | * to the `cwd` setting. 51 | * 52 | * This does _not_ make an extra system call to get 53 | * the realpath, it only does string path resolution. 54 | * 55 | * Conflicts with {@link withFileTypes} 56 | */ 57 | absolute?: boolean 58 | 59 | /** 60 | * Set to false to enable {@link windowsPathsNoEscape} 61 | * 62 | * @deprecated 63 | */ 64 | allowWindowsEscape?: boolean 65 | 66 | /** 67 | * The current working directory in which to search. Defaults to 68 | * `process.cwd()`. 69 | * 70 | * May be eiher a string path or a `file://` URL object or string. 71 | */ 72 | cwd?: string | URL 73 | 74 | /** 75 | * Include `.dot` files in normal matches and `globstar` 76 | * matches. Note that an explicit dot in a portion of the pattern 77 | * will always match dot files. 78 | */ 79 | dot?: boolean 80 | 81 | /** 82 | * Prepend all relative path strings with `./` (or `.\` on Windows). 83 | * 84 | * Without this option, returned relative paths are "bare", so instead of 85 | * returning `'./foo/bar'`, they are returned as `'foo/bar'`. 86 | * 87 | * Relative patterns starting with `'../'` are not prepended with `./`, even 88 | * if this option is set. 89 | */ 90 | dotRelative?: boolean 91 | 92 | /** 93 | * Follow symlinked directories when expanding `**` 94 | * patterns. This can result in a lot of duplicate references in 95 | * the presence of cyclic links, and make performance quite bad. 96 | * 97 | * By default, a `**` in a pattern will follow 1 symbolic link if 98 | * it is not the first item in the pattern, or none if it is the 99 | * first item in the pattern, following the same behavior as Bash. 100 | */ 101 | follow?: boolean 102 | 103 | /** 104 | * string or string[], or an object with `ignored` and `childrenIgnored` 105 | * methods. 106 | * 107 | * If a string or string[] is provided, then this is treated as a glob 108 | * pattern or array of glob patterns to exclude from matches. To ignore all 109 | * children within a directory, as well as the entry itself, append `'/**'` 110 | * to the ignore pattern. 111 | * 112 | * **Note** `ignore` patterns are _always_ in `dot:true` mode, regardless of 113 | * any other settings. 114 | * 115 | * If an object is provided that has `ignored(path)` and/or 116 | * `childrenIgnored(path)` methods, then these methods will be called to 117 | * determine whether any Path is a match or if its children should be 118 | * traversed, respectively. 119 | */ 120 | ignore?: string | string[] | IgnoreLike 121 | 122 | /** 123 | * Treat brace expansion like `{a,b}` as a "magic" pattern. Has no 124 | * effect if {@link nobrace} is set. 125 | * 126 | * Only has effect on the {@link hasMagic} function. 127 | */ 128 | magicalBraces?: boolean 129 | 130 | /** 131 | * Add a `/` character to directory matches. Note that this requires 132 | * additional stat calls in some cases. 133 | */ 134 | mark?: boolean 135 | 136 | /** 137 | * Perform a basename-only match if the pattern does not contain any slash 138 | * characters. That is, `*.js` would be treated as equivalent to 139 | * `**\/*.js`, matching all js files in all directories. 140 | */ 141 | matchBase?: boolean 142 | 143 | /** 144 | * Limit the directory traversal to a given depth below the cwd. 145 | * Note that this does NOT prevent traversal to sibling folders, 146 | * root patterns, and so on. It only limits the maximum folder depth 147 | * that the walk will descend, relative to the cwd. 148 | */ 149 | maxDepth?: number 150 | 151 | /** 152 | * Do not expand `{a,b}` and `{1..3}` brace sets. 153 | */ 154 | nobrace?: boolean 155 | 156 | /** 157 | * Perform a case-insensitive match. This defaults to `true` on macOS and 158 | * Windows systems, and `false` on all others. 159 | * 160 | * **Note** `nocase` should only be explicitly set when it is 161 | * known that the filesystem's case sensitivity differs from the 162 | * platform default. If set `true` on case-sensitive file 163 | * systems, or `false` on case-insensitive file systems, then the 164 | * walk may return more or less results than expected. 165 | */ 166 | nocase?: boolean 167 | 168 | /** 169 | * Do not match directories, only files. (Note: to match 170 | * _only_ directories, put a `/` at the end of the pattern.) 171 | */ 172 | nodir?: boolean 173 | 174 | /** 175 | * Do not match "extglob" patterns such as `+(a|b)`. 176 | */ 177 | noext?: boolean 178 | 179 | /** 180 | * Do not match `**` against multiple filenames. (Ie, treat it as a normal 181 | * `*` instead.) 182 | * 183 | * Conflicts with {@link matchBase} 184 | */ 185 | noglobstar?: boolean 186 | 187 | /** 188 | * Defaults to value of `process.platform` if available, or `'linux'` if 189 | * not. Setting `platform:'win32'` on non-Windows systems may cause strange 190 | * behavior. 191 | */ 192 | platform?: NodeJS.Platform 193 | 194 | /** 195 | * Set to true to call `fs.realpath` on all of the 196 | * results. In the case of an entry that cannot be resolved, the 197 | * entry is omitted. This incurs a slight performance penalty, of 198 | * course, because of the added system calls. 199 | */ 200 | realpath?: boolean 201 | 202 | /** 203 | * 204 | * A string path resolved against the `cwd` option, which 205 | * is used as the starting point for absolute patterns that start 206 | * with `/`, (but not drive letters or UNC paths on Windows). 207 | * 208 | * Note that this _doesn't_ necessarily limit the walk to the 209 | * `root` directory, and doesn't affect the cwd starting point for 210 | * non-absolute patterns. A pattern containing `..` will still be 211 | * able to traverse out of the root directory, if it is not an 212 | * actual root directory on the filesystem, and any non-absolute 213 | * patterns will be matched in the `cwd`. For example, the 214 | * pattern `/../*` with `{root:'/some/path'}` will return all 215 | * files in `/some`, not all files in `/some/path`. The pattern 216 | * `*` with `{root:'/some/path'}` will return all the entries in 217 | * the cwd, not the entries in `/some/path`. 218 | * 219 | * To start absolute and non-absolute patterns in the same 220 | * path, you can use `{root:''}`. However, be aware that on 221 | * Windows systems, a pattern like `x:/*` or `//host/share/*` will 222 | * _always_ start in the `x:/` or `//host/share` directory, 223 | * regardless of the `root` setting. 224 | */ 225 | root?: string 226 | 227 | /** 228 | * A [PathScurry](http://npm.im/path-scurry) object used 229 | * to traverse the file system. If the `nocase` option is set 230 | * explicitly, then any provided `scurry` object must match this 231 | * setting. 232 | */ 233 | scurry?: PathScurry 234 | 235 | /** 236 | * Call `lstat()` on all entries, whether required or not to determine 237 | * if it's a valid match. When used with {@link withFileTypes}, this means 238 | * that matches will include data such as modified time, permissions, and 239 | * so on. Note that this will incur a performance cost due to the added 240 | * system calls. 241 | */ 242 | stat?: boolean 243 | 244 | /** 245 | * An AbortSignal which will cancel the Glob walk when 246 | * triggered. 247 | */ 248 | signal?: AbortSignal 249 | 250 | /** 251 | * Use `\\` as a path separator _only_, and 252 | * _never_ as an escape character. If set, all `\\` characters are 253 | * replaced with `/` in the pattern. 254 | * 255 | * Note that this makes it **impossible** to match against paths 256 | * containing literal glob pattern characters, but allows matching 257 | * with patterns constructed using `path.join()` and 258 | * `path.resolve()` on Windows platforms, mimicking the (buggy!) 259 | * behavior of Glob v7 and before on Windows. Please use with 260 | * caution, and be mindful of [the caveat below about Windows 261 | * paths](#windows). (For legacy reasons, this is also set if 262 | * `allowWindowsEscape` is set to the exact value `false`.) 263 | */ 264 | windowsPathsNoEscape?: boolean 265 | 266 | /** 267 | * Return [PathScurry](http://npm.im/path-scurry) 268 | * `Path` objects instead of strings. These are similar to a 269 | * NodeJS `Dirent` object, but with additional methods and 270 | * properties. 271 | * 272 | * Conflicts with {@link absolute} 273 | */ 274 | withFileTypes?: boolean 275 | 276 | /** 277 | * An fs implementation to override some or all of the defaults. See 278 | * http://npm.im/path-scurry for details about what can be overridden. 279 | */ 280 | fs?: FSOption 281 | 282 | /** 283 | * Just passed along to Minimatch. Note that this makes all pattern 284 | * matching operations slower and *extremely* noisy. 285 | */ 286 | debug?: boolean 287 | 288 | /** 289 | * Return `/` delimited paths, even on Windows. 290 | * 291 | * On posix systems, this has no effect. But, on Windows, it means that 292 | * paths will be `/` delimited, and absolute paths will be their full 293 | * resolved UNC forms, eg instead of `'C:\\foo\\bar'`, it would return 294 | * `'//?/C:/foo/bar'` 295 | */ 296 | posix?: boolean 297 | 298 | /** 299 | * Do not match any children of any matches. For example, the pattern 300 | * `**\/foo` would match `a/foo`, but not `a/foo/b/foo` in this mode. 301 | * 302 | * This is especially useful for cases like "find all `node_modules` 303 | * folders, but not the ones in `node_modules`". 304 | * 305 | * In order to support this, the `Ignore` implementation must support an 306 | * `add(pattern: string)` method. If using the default `Ignore` class, then 307 | * this is fine, but if this is set to `false`, and a custom `Ignore` is 308 | * provided that does not have an `add()` method, then it will throw an 309 | * error. 310 | * 311 | * **Caveat** It *only* ignores matches that would be a descendant of a 312 | * previous match, and only if that descendant is matched *after* the 313 | * ancestor is encountered. Since the file system walk happens in 314 | * indeterminate order, it's possible that a match will already be added 315 | * before its ancestor, if multiple or braced patterns are used. 316 | * 317 | * For example: 318 | * 319 | * ```ts 320 | * const results = await glob([ 321 | * // likely to match first, since it's just a stat 322 | * 'a/b/c/d/e/f', 323 | * 324 | * // this pattern is more complicated! It must to various readdir() 325 | * // calls and test the results against a regular expression, and that 326 | * // is certainly going to take a little bit longer. 327 | * // 328 | * // So, later on, it encounters a match at 'a/b/c/d/e', but it's too 329 | * // late to ignore a/b/c/d/e/f, because it's already been emitted. 330 | * 'a/[bdf]/?/[a-z]/*', 331 | * ], { includeChildMatches: false }) 332 | * ``` 333 | * 334 | * It's best to only set this to `false` if you can be reasonably sure that 335 | * no components of the pattern will potentially match one another's file 336 | * system descendants, or if the occasional included child entry will not 337 | * cause problems. 338 | * 339 | * @default true 340 | */ 341 | includeChildMatches?: boolean 342 | } 343 | 344 | export type GlobOptionsWithFileTypesTrue = GlobOptions & { 345 | withFileTypes: true 346 | // string options not relevant if returning Path objects. 347 | absolute?: undefined 348 | mark?: undefined 349 | posix?: undefined 350 | } 351 | 352 | export type GlobOptionsWithFileTypesFalse = GlobOptions & { 353 | withFileTypes?: false 354 | } 355 | 356 | export type GlobOptionsWithFileTypesUnset = GlobOptions & { 357 | withFileTypes?: undefined 358 | } 359 | 360 | export type Result = 361 | Opts extends GlobOptionsWithFileTypesTrue ? Path 362 | : Opts extends GlobOptionsWithFileTypesFalse ? string 363 | : Opts extends GlobOptionsWithFileTypesUnset ? string 364 | : string | Path 365 | export type Results = Result[] 366 | 367 | export type FileTypes = 368 | Opts extends GlobOptionsWithFileTypesTrue ? true 369 | : Opts extends GlobOptionsWithFileTypesFalse ? false 370 | : Opts extends GlobOptionsWithFileTypesUnset ? false 371 | : boolean 372 | 373 | /** 374 | * An object that can perform glob pattern traversals. 375 | */ 376 | export class Glob implements GlobOptions { 377 | absolute?: boolean 378 | cwd: string 379 | root?: string 380 | dot: boolean 381 | dotRelative: boolean 382 | follow: boolean 383 | ignore?: string | string[] | IgnoreLike 384 | magicalBraces: boolean 385 | mark?: boolean 386 | matchBase: boolean 387 | maxDepth: number 388 | nobrace: boolean 389 | nocase: boolean 390 | nodir: boolean 391 | noext: boolean 392 | noglobstar: boolean 393 | pattern: string[] 394 | platform: NodeJS.Platform 395 | realpath: boolean 396 | scurry: PathScurry 397 | stat: boolean 398 | signal?: AbortSignal 399 | windowsPathsNoEscape: boolean 400 | withFileTypes: FileTypes 401 | includeChildMatches: boolean 402 | 403 | /** 404 | * The options provided to the constructor. 405 | */ 406 | opts: Opts 407 | 408 | /** 409 | * An array of parsed immutable {@link Pattern} objects. 410 | */ 411 | patterns: Pattern[] 412 | 413 | /** 414 | * All options are stored as properties on the `Glob` object. 415 | * 416 | * See {@link GlobOptions} for full options descriptions. 417 | * 418 | * Note that a previous `Glob` object can be passed as the 419 | * `GlobOptions` to another `Glob` instantiation to re-use settings 420 | * and caches with a new pattern. 421 | * 422 | * Traversal functions can be called multiple times to run the walk 423 | * again. 424 | */ 425 | constructor(pattern: string | string[], opts: Opts) { 426 | /* c8 ignore start */ 427 | if (!opts) throw new TypeError('glob options required') 428 | /* c8 ignore stop */ 429 | this.withFileTypes = !!opts.withFileTypes as FileTypes 430 | this.signal = opts.signal 431 | this.follow = !!opts.follow 432 | this.dot = !!opts.dot 433 | this.dotRelative = !!opts.dotRelative 434 | this.nodir = !!opts.nodir 435 | this.mark = !!opts.mark 436 | if (!opts.cwd) { 437 | this.cwd = '' 438 | } else if (opts.cwd instanceof URL || opts.cwd.startsWith('file://')) { 439 | opts.cwd = fileURLToPath(opts.cwd) 440 | } 441 | this.cwd = opts.cwd || '' 442 | this.root = opts.root 443 | this.magicalBraces = !!opts.magicalBraces 444 | this.nobrace = !!opts.nobrace 445 | this.noext = !!opts.noext 446 | this.realpath = !!opts.realpath 447 | this.absolute = opts.absolute 448 | this.includeChildMatches = opts.includeChildMatches !== false 449 | 450 | this.noglobstar = !!opts.noglobstar 451 | this.matchBase = !!opts.matchBase 452 | this.maxDepth = 453 | typeof opts.maxDepth === 'number' ? opts.maxDepth : Infinity 454 | this.stat = !!opts.stat 455 | this.ignore = opts.ignore 456 | 457 | if (this.withFileTypes && this.absolute !== undefined) { 458 | throw new Error('cannot set absolute and withFileTypes:true') 459 | } 460 | 461 | if (typeof pattern === 'string') { 462 | pattern = [pattern] 463 | } 464 | 465 | this.windowsPathsNoEscape = 466 | !!opts.windowsPathsNoEscape || 467 | (opts as { allowWindowsEscape?: boolean }).allowWindowsEscape === 468 | false 469 | 470 | if (this.windowsPathsNoEscape) { 471 | pattern = pattern.map(p => p.replace(/\\/g, '/')) 472 | } 473 | 474 | if (this.matchBase) { 475 | if (opts.noglobstar) { 476 | throw new TypeError('base matching requires globstar') 477 | } 478 | pattern = pattern.map(p => (p.includes('/') ? p : `./**/${p}`)) 479 | } 480 | 481 | this.pattern = pattern 482 | 483 | this.platform = opts.platform || defaultPlatform 484 | this.opts = { ...opts, platform: this.platform } 485 | if (opts.scurry) { 486 | this.scurry = opts.scurry 487 | if ( 488 | opts.nocase !== undefined && 489 | opts.nocase !== opts.scurry.nocase 490 | ) { 491 | throw new Error('nocase option contradicts provided scurry option') 492 | } 493 | } else { 494 | const Scurry = 495 | opts.platform === 'win32' ? PathScurryWin32 496 | : opts.platform === 'darwin' ? PathScurryDarwin 497 | : opts.platform ? PathScurryPosix 498 | : PathScurry 499 | this.scurry = new Scurry(this.cwd, { 500 | nocase: opts.nocase, 501 | fs: opts.fs, 502 | }) 503 | } 504 | this.nocase = this.scurry.nocase 505 | 506 | // If you do nocase:true on a case-sensitive file system, then 507 | // we need to use regexps instead of strings for non-magic 508 | // path portions, because statting `aBc` won't return results 509 | // for the file `AbC` for example. 510 | const nocaseMagicOnly = 511 | this.platform === 'darwin' || this.platform === 'win32' 512 | 513 | const mmo: MinimatchOptions = { 514 | // default nocase based on platform 515 | ...opts, 516 | dot: this.dot, 517 | matchBase: this.matchBase, 518 | nobrace: this.nobrace, 519 | nocase: this.nocase, 520 | nocaseMagicOnly, 521 | nocomment: true, 522 | noext: this.noext, 523 | nonegate: true, 524 | optimizationLevel: 2, 525 | platform: this.platform, 526 | windowsPathsNoEscape: this.windowsPathsNoEscape, 527 | debug: !!this.opts.debug, 528 | } 529 | 530 | const mms = this.pattern.map(p => new Minimatch(p, mmo)) 531 | const [matchSet, globParts] = mms.reduce( 532 | (set: [MatchSet, GlobParts], m) => { 533 | set[0].push(...m.set) 534 | set[1].push(...m.globParts) 535 | return set 536 | }, 537 | [[], []], 538 | ) 539 | this.patterns = matchSet.map((set, i) => { 540 | const g = globParts[i] 541 | /* c8 ignore start */ 542 | if (!g) throw new Error('invalid pattern object') 543 | /* c8 ignore stop */ 544 | return new Pattern(set, g, 0, this.platform) 545 | }) 546 | } 547 | 548 | /** 549 | * Returns a Promise that resolves to the results array. 550 | */ 551 | async walk(): Promise> 552 | async walk(): Promise<(string | Path)[]> { 553 | // Walkers always return array of Path objects, so we just have to 554 | // coerce them into the right shape. It will have already called 555 | // realpath() if the option was set to do so, so we know that's cached. 556 | // start out knowing the cwd, at least 557 | return [ 558 | ...(await new GlobWalker(this.patterns, this.scurry.cwd, { 559 | ...this.opts, 560 | maxDepth: 561 | this.maxDepth !== Infinity ? 562 | this.maxDepth + this.scurry.cwd.depth() 563 | : Infinity, 564 | platform: this.platform, 565 | nocase: this.nocase, 566 | includeChildMatches: this.includeChildMatches, 567 | }).walk()), 568 | ] 569 | } 570 | 571 | /** 572 | * synchronous {@link Glob.walk} 573 | */ 574 | walkSync(): Results 575 | walkSync(): (string | Path)[] { 576 | return [ 577 | ...new GlobWalker(this.patterns, this.scurry.cwd, { 578 | ...this.opts, 579 | maxDepth: 580 | this.maxDepth !== Infinity ? 581 | this.maxDepth + this.scurry.cwd.depth() 582 | : Infinity, 583 | platform: this.platform, 584 | nocase: this.nocase, 585 | includeChildMatches: this.includeChildMatches, 586 | }).walkSync(), 587 | ] 588 | } 589 | 590 | /** 591 | * Stream results asynchronously. 592 | */ 593 | stream(): Minipass, Result> 594 | stream(): Minipass { 595 | return new GlobStream(this.patterns, this.scurry.cwd, { 596 | ...this.opts, 597 | maxDepth: 598 | this.maxDepth !== Infinity ? 599 | this.maxDepth + this.scurry.cwd.depth() 600 | : Infinity, 601 | platform: this.platform, 602 | nocase: this.nocase, 603 | includeChildMatches: this.includeChildMatches, 604 | }).stream() 605 | } 606 | 607 | /** 608 | * Stream results synchronously. 609 | */ 610 | streamSync(): Minipass, Result> 611 | streamSync(): Minipass { 612 | return new GlobStream(this.patterns, this.scurry.cwd, { 613 | ...this.opts, 614 | maxDepth: 615 | this.maxDepth !== Infinity ? 616 | this.maxDepth + this.scurry.cwd.depth() 617 | : Infinity, 618 | platform: this.platform, 619 | nocase: this.nocase, 620 | includeChildMatches: this.includeChildMatches, 621 | }).streamSync() 622 | } 623 | 624 | /** 625 | * Default sync iteration function. Returns a Generator that 626 | * iterates over the results. 627 | */ 628 | iterateSync(): Generator, void, void> { 629 | return this.streamSync()[Symbol.iterator]() 630 | } 631 | [Symbol.iterator]() { 632 | return this.iterateSync() 633 | } 634 | 635 | /** 636 | * Default async iteration function. Returns an AsyncGenerator that 637 | * iterates over the results. 638 | */ 639 | iterate(): AsyncGenerator, void, void> { 640 | return this.stream()[Symbol.asyncIterator]() 641 | } 642 | [Symbol.asyncIterator]() { 643 | return this.iterate() 644 | } 645 | } 646 | -------------------------------------------------------------------------------- /src/has-magic.ts: -------------------------------------------------------------------------------- 1 | import { Minimatch } from 'minimatch' 2 | import { GlobOptions } from './glob.js' 3 | 4 | /** 5 | * Return true if the patterns provided contain any magic glob characters, 6 | * given the options provided. 7 | * 8 | * Brace expansion is not considered "magic" unless the `magicalBraces` option 9 | * is set, as brace expansion just turns one string into an array of strings. 10 | * So a pattern like `'x{a,b}y'` would return `false`, because `'xay'` and 11 | * `'xby'` both do not contain any magic glob characters, and it's treated the 12 | * same as if you had called it on `['xay', 'xby']`. When `magicalBraces:true` 13 | * is in the options, brace expansion _is_ treated as a pattern having magic. 14 | */ 15 | export const hasMagic = ( 16 | pattern: string | string[], 17 | options: GlobOptions = {}, 18 | ): boolean => { 19 | if (!Array.isArray(pattern)) { 20 | pattern = [pattern] 21 | } 22 | for (const p of pattern) { 23 | if (new Minimatch(p, options).hasMagic()) return true 24 | } 25 | return false 26 | } 27 | -------------------------------------------------------------------------------- /src/ignore.ts: -------------------------------------------------------------------------------- 1 | // give it a pattern, and it'll be able to tell you if 2 | // a given path should be ignored. 3 | // Ignoring a path ignores its children if the pattern ends in /** 4 | // Ignores are always parsed in dot:true mode 5 | 6 | import { Minimatch, MinimatchOptions } from 'minimatch' 7 | import { Path } from 'path-scurry' 8 | import { Pattern } from './pattern.js' 9 | import { GlobWalkerOpts } from './walker.js' 10 | 11 | export interface IgnoreLike { 12 | ignored?: (p: Path) => boolean 13 | childrenIgnored?: (p: Path) => boolean 14 | add?: (ignore: string) => void 15 | } 16 | 17 | const defaultPlatform: NodeJS.Platform = 18 | ( 19 | typeof process === 'object' && 20 | process && 21 | typeof process.platform === 'string' 22 | ) ? 23 | process.platform 24 | : 'linux' 25 | 26 | /** 27 | * Class used to process ignored patterns 28 | */ 29 | export class Ignore implements IgnoreLike { 30 | relative: Minimatch[] 31 | relativeChildren: Minimatch[] 32 | absolute: Minimatch[] 33 | absoluteChildren: Minimatch[] 34 | platform: NodeJS.Platform 35 | mmopts: MinimatchOptions 36 | 37 | constructor( 38 | ignored: string[], 39 | { 40 | nobrace, 41 | nocase, 42 | noext, 43 | noglobstar, 44 | platform = defaultPlatform, 45 | }: GlobWalkerOpts, 46 | ) { 47 | this.relative = [] 48 | this.absolute = [] 49 | this.relativeChildren = [] 50 | this.absoluteChildren = [] 51 | this.platform = platform 52 | this.mmopts = { 53 | dot: true, 54 | nobrace, 55 | nocase, 56 | noext, 57 | noglobstar, 58 | optimizationLevel: 2, 59 | platform, 60 | nocomment: true, 61 | nonegate: true, 62 | } 63 | for (const ign of ignored) this.add(ign) 64 | } 65 | 66 | add(ign: string) { 67 | // this is a little weird, but it gives us a clean set of optimized 68 | // minimatch matchers, without getting tripped up if one of them 69 | // ends in /** inside a brace section, and it's only inefficient at 70 | // the start of the walk, not along it. 71 | // It'd be nice if the Pattern class just had a .test() method, but 72 | // handling globstars is a bit of a pita, and that code already lives 73 | // in minimatch anyway. 74 | // Another way would be if maybe Minimatch could take its set/globParts 75 | // as an option, and then we could at least just use Pattern to test 76 | // for absolute-ness. 77 | // Yet another way, Minimatch could take an array of glob strings, and 78 | // a cwd option, and do the right thing. 79 | const mm = new Minimatch(ign, this.mmopts) 80 | for (let i = 0; i < mm.set.length; i++) { 81 | const parsed = mm.set[i] 82 | const globParts = mm.globParts[i] 83 | /* c8 ignore start */ 84 | if (!parsed || !globParts) { 85 | throw new Error('invalid pattern object') 86 | } 87 | // strip off leading ./ portions 88 | // https://github.com/isaacs/node-glob/issues/570 89 | while (parsed[0] === '.' && globParts[0] === '.') { 90 | parsed.shift() 91 | globParts.shift() 92 | } 93 | /* c8 ignore stop */ 94 | const p = new Pattern(parsed, globParts, 0, this.platform) 95 | const m = new Minimatch(p.globString(), this.mmopts) 96 | const children = globParts[globParts.length - 1] === '**' 97 | const absolute = p.isAbsolute() 98 | if (absolute) this.absolute.push(m) 99 | else this.relative.push(m) 100 | if (children) { 101 | if (absolute) this.absoluteChildren.push(m) 102 | else this.relativeChildren.push(m) 103 | } 104 | } 105 | } 106 | 107 | ignored(p: Path): boolean { 108 | const fullpath = p.fullpath() 109 | const fullpaths = `${fullpath}/` 110 | const relative = p.relative() || '.' 111 | const relatives = `${relative}/` 112 | for (const m of this.relative) { 113 | if (m.match(relative) || m.match(relatives)) return true 114 | } 115 | for (const m of this.absolute) { 116 | if (m.match(fullpath) || m.match(fullpaths)) return true 117 | } 118 | return false 119 | } 120 | 121 | childrenIgnored(p: Path): boolean { 122 | const fullpath = p.fullpath() + '/' 123 | const relative = (p.relative() || '.') + '/' 124 | for (const m of this.relativeChildren) { 125 | if (m.match(relative)) return true 126 | } 127 | for (const m of this.absoluteChildren) { 128 | if (m.match(fullpath)) return true 129 | } 130 | return false 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { escape, unescape } from 'minimatch' 2 | import { Minipass } from 'minipass' 3 | import { Path } from 'path-scurry' 4 | import type { 5 | GlobOptions, 6 | GlobOptionsWithFileTypesFalse, 7 | GlobOptionsWithFileTypesTrue, 8 | GlobOptionsWithFileTypesUnset, 9 | } from './glob.js' 10 | import { Glob } from './glob.js' 11 | import { hasMagic } from './has-magic.js' 12 | 13 | export { escape, unescape } from 'minimatch' 14 | export type { 15 | FSOption, 16 | Path, 17 | WalkOptions, 18 | WalkOptionsWithFileTypesTrue, 19 | WalkOptionsWithFileTypesUnset, 20 | } from 'path-scurry' 21 | export { Glob } from './glob.js' 22 | export type { 23 | GlobOptions, 24 | GlobOptionsWithFileTypesFalse, 25 | GlobOptionsWithFileTypesTrue, 26 | GlobOptionsWithFileTypesUnset, 27 | } from './glob.js' 28 | export { hasMagic } from './has-magic.js' 29 | export { Ignore } from './ignore.js' 30 | export type { IgnoreLike } from './ignore.js' 31 | export type { MatchStream } from './walker.js' 32 | 33 | /** 34 | * Syncronous form of {@link globStream}. Will read all the matches as fast as 35 | * you consume them, even all in a single tick if you consume them immediately, 36 | * but will still respond to backpressure if they're not consumed immediately. 37 | */ 38 | export function globStreamSync( 39 | pattern: string | string[], 40 | options: GlobOptionsWithFileTypesTrue, 41 | ): Minipass 42 | export function globStreamSync( 43 | pattern: string | string[], 44 | options: GlobOptionsWithFileTypesFalse, 45 | ): Minipass 46 | export function globStreamSync( 47 | pattern: string | string[], 48 | options: GlobOptionsWithFileTypesUnset, 49 | ): Minipass 50 | export function globStreamSync( 51 | pattern: string | string[], 52 | options: GlobOptions, 53 | ): Minipass | Minipass 54 | export function globStreamSync( 55 | pattern: string | string[], 56 | options: GlobOptions = {}, 57 | ) { 58 | return new Glob(pattern, options).streamSync() 59 | } 60 | 61 | /** 62 | * Return a stream that emits all the strings or `Path` objects and 63 | * then emits `end` when completed. 64 | */ 65 | export function globStream( 66 | pattern: string | string[], 67 | options: GlobOptionsWithFileTypesFalse, 68 | ): Minipass 69 | export function globStream( 70 | pattern: string | string[], 71 | options: GlobOptionsWithFileTypesTrue, 72 | ): Minipass 73 | export function globStream( 74 | pattern: string | string[], 75 | options?: GlobOptionsWithFileTypesUnset | undefined, 76 | ): Minipass 77 | export function globStream( 78 | pattern: string | string[], 79 | options: GlobOptions, 80 | ): Minipass | Minipass 81 | export function globStream( 82 | pattern: string | string[], 83 | options: GlobOptions = {}, 84 | ) { 85 | return new Glob(pattern, options).stream() 86 | } 87 | 88 | /** 89 | * Synchronous form of {@link glob} 90 | */ 91 | export function globSync( 92 | pattern: string | string[], 93 | options: GlobOptionsWithFileTypesFalse, 94 | ): string[] 95 | export function globSync( 96 | pattern: string | string[], 97 | options: GlobOptionsWithFileTypesTrue, 98 | ): Path[] 99 | export function globSync( 100 | pattern: string | string[], 101 | options?: GlobOptionsWithFileTypesUnset | undefined, 102 | ): string[] 103 | export function globSync( 104 | pattern: string | string[], 105 | options: GlobOptions, 106 | ): Path[] | string[] 107 | export function globSync( 108 | pattern: string | string[], 109 | options: GlobOptions = {}, 110 | ) { 111 | return new Glob(pattern, options).walkSync() 112 | } 113 | 114 | /** 115 | * Perform an asynchronous glob search for the pattern(s) specified. Returns 116 | * [Path](https://isaacs.github.io/path-scurry/classes/PathBase) objects if the 117 | * {@link withFileTypes} option is set to `true`. See {@link GlobOptions} for 118 | * full option descriptions. 119 | */ 120 | async function glob_( 121 | pattern: string | string[], 122 | options?: GlobOptionsWithFileTypesUnset | undefined, 123 | ): Promise 124 | async function glob_( 125 | pattern: string | string[], 126 | options: GlobOptionsWithFileTypesTrue, 127 | ): Promise 128 | async function glob_( 129 | pattern: string | string[], 130 | options: GlobOptionsWithFileTypesFalse, 131 | ): Promise 132 | async function glob_( 133 | pattern: string | string[], 134 | options: GlobOptions, 135 | ): Promise 136 | async function glob_( 137 | pattern: string | string[], 138 | options: GlobOptions = {}, 139 | ) { 140 | return new Glob(pattern, options).walk() 141 | } 142 | 143 | /** 144 | * Return a sync iterator for walking glob pattern matches. 145 | */ 146 | export function globIterateSync( 147 | pattern: string | string[], 148 | options?: GlobOptionsWithFileTypesUnset | undefined, 149 | ): Generator 150 | export function globIterateSync( 151 | pattern: string | string[], 152 | options: GlobOptionsWithFileTypesTrue, 153 | ): Generator 154 | export function globIterateSync( 155 | pattern: string | string[], 156 | options: GlobOptionsWithFileTypesFalse, 157 | ): Generator 158 | export function globIterateSync( 159 | pattern: string | string[], 160 | options: GlobOptions, 161 | ): Generator | Generator 162 | export function globIterateSync( 163 | pattern: string | string[], 164 | options: GlobOptions = {}, 165 | ) { 166 | return new Glob(pattern, options).iterateSync() 167 | } 168 | 169 | /** 170 | * Return an async iterator for walking glob pattern matches. 171 | */ 172 | export function globIterate( 173 | pattern: string | string[], 174 | options?: GlobOptionsWithFileTypesUnset | undefined, 175 | ): AsyncGenerator 176 | export function globIterate( 177 | pattern: string | string[], 178 | options: GlobOptionsWithFileTypesTrue, 179 | ): AsyncGenerator 180 | export function globIterate( 181 | pattern: string | string[], 182 | options: GlobOptionsWithFileTypesFalse, 183 | ): AsyncGenerator 184 | export function globIterate( 185 | pattern: string | string[], 186 | options: GlobOptions, 187 | ): AsyncGenerator | AsyncGenerator 188 | export function globIterate( 189 | pattern: string | string[], 190 | options: GlobOptions = {}, 191 | ) { 192 | return new Glob(pattern, options).iterate() 193 | } 194 | 195 | // aliases: glob.sync.stream() glob.stream.sync() glob.sync() etc 196 | export const streamSync = globStreamSync 197 | export const stream = Object.assign(globStream, { sync: globStreamSync }) 198 | export const iterateSync = globIterateSync 199 | export const iterate = Object.assign(globIterate, { 200 | sync: globIterateSync, 201 | }) 202 | export const sync = Object.assign(globSync, { 203 | stream: globStreamSync, 204 | iterate: globIterateSync, 205 | }) 206 | 207 | export const glob = Object.assign(glob_, { 208 | glob: glob_, 209 | globSync, 210 | sync, 211 | globStream, 212 | stream, 213 | globStreamSync, 214 | streamSync, 215 | globIterate, 216 | iterate, 217 | globIterateSync, 218 | iterateSync, 219 | Glob, 220 | hasMagic, 221 | escape, 222 | unescape, 223 | }) 224 | glob.glob = glob 225 | -------------------------------------------------------------------------------- /src/pattern.ts: -------------------------------------------------------------------------------- 1 | // this is just a very light wrapper around 2 arrays with an offset index 2 | 3 | import { GLOBSTAR } from 'minimatch' 4 | export type MMPattern = string | RegExp | typeof GLOBSTAR 5 | 6 | // an array of length >= 1 7 | export type PatternList = [p: MMPattern, ...rest: MMPattern[]] 8 | export type UNCPatternList = [ 9 | p0: '', 10 | p1: '', 11 | p2: string, 12 | p3: string, 13 | ...rest: MMPattern[], 14 | ] 15 | export type DrivePatternList = [p0: string, ...rest: MMPattern[]] 16 | export type AbsolutePatternList = [p0: '', ...rest: MMPattern[]] 17 | export type GlobList = [p: string, ...rest: string[]] 18 | 19 | const isPatternList = (pl: MMPattern[]): pl is PatternList => 20 | pl.length >= 1 21 | const isGlobList = (gl: string[]): gl is GlobList => gl.length >= 1 22 | 23 | /** 24 | * An immutable-ish view on an array of glob parts and their parsed 25 | * results 26 | */ 27 | export class Pattern { 28 | readonly #patternList: PatternList 29 | readonly #globList: GlobList 30 | readonly #index: number 31 | readonly length: number 32 | readonly #platform: NodeJS.Platform 33 | #rest?: Pattern | null 34 | #globString?: string 35 | #isDrive?: boolean 36 | #isUNC?: boolean 37 | #isAbsolute?: boolean 38 | #followGlobstar: boolean = true 39 | 40 | constructor( 41 | patternList: MMPattern[], 42 | globList: string[], 43 | index: number, 44 | platform: NodeJS.Platform, 45 | ) { 46 | if (!isPatternList(patternList)) { 47 | throw new TypeError('empty pattern list') 48 | } 49 | if (!isGlobList(globList)) { 50 | throw new TypeError('empty glob list') 51 | } 52 | if (globList.length !== patternList.length) { 53 | throw new TypeError('mismatched pattern list and glob list lengths') 54 | } 55 | this.length = patternList.length 56 | if (index < 0 || index >= this.length) { 57 | throw new TypeError('index out of range') 58 | } 59 | this.#patternList = patternList 60 | this.#globList = globList 61 | this.#index = index 62 | this.#platform = platform 63 | 64 | // normalize root entries of absolute patterns on initial creation. 65 | if (this.#index === 0) { 66 | // c: => ['c:/'] 67 | // C:/ => ['C:/'] 68 | // C:/x => ['C:/', 'x'] 69 | // //host/share => ['//host/share/'] 70 | // //host/share/ => ['//host/share/'] 71 | // //host/share/x => ['//host/share/', 'x'] 72 | // /etc => ['/', 'etc'] 73 | // / => ['/'] 74 | if (this.isUNC()) { 75 | // '' / '' / 'host' / 'share' 76 | const [p0, p1, p2, p3, ...prest] = this.#patternList 77 | const [g0, g1, g2, g3, ...grest] = this.#globList 78 | if (prest[0] === '') { 79 | // ends in / 80 | prest.shift() 81 | grest.shift() 82 | } 83 | const p = [p0, p1, p2, p3, ''].join('/') 84 | const g = [g0, g1, g2, g3, ''].join('/') 85 | this.#patternList = [p, ...prest] 86 | this.#globList = [g, ...grest] 87 | this.length = this.#patternList.length 88 | } else if (this.isDrive() || this.isAbsolute()) { 89 | const [p1, ...prest] = this.#patternList 90 | const [g1, ...grest] = this.#globList 91 | if (prest[0] === '') { 92 | // ends in / 93 | prest.shift() 94 | grest.shift() 95 | } 96 | const p = (p1 as string) + '/' 97 | const g = g1 + '/' 98 | this.#patternList = [p, ...prest] 99 | this.#globList = [g, ...grest] 100 | this.length = this.#patternList.length 101 | } 102 | } 103 | } 104 | 105 | /** 106 | * The first entry in the parsed list of patterns 107 | */ 108 | pattern(): MMPattern { 109 | return this.#patternList[this.#index] as MMPattern 110 | } 111 | 112 | /** 113 | * true of if pattern() returns a string 114 | */ 115 | isString(): boolean { 116 | return typeof this.#patternList[this.#index] === 'string' 117 | } 118 | /** 119 | * true of if pattern() returns GLOBSTAR 120 | */ 121 | isGlobstar(): boolean { 122 | return this.#patternList[this.#index] === GLOBSTAR 123 | } 124 | /** 125 | * true if pattern() returns a regexp 126 | */ 127 | isRegExp(): boolean { 128 | return this.#patternList[this.#index] instanceof RegExp 129 | } 130 | 131 | /** 132 | * The /-joined set of glob parts that make up this pattern 133 | */ 134 | globString(): string { 135 | return (this.#globString = 136 | this.#globString || 137 | (this.#index === 0 ? 138 | this.isAbsolute() ? 139 | this.#globList[0] + this.#globList.slice(1).join('/') 140 | : this.#globList.join('/') 141 | : this.#globList.slice(this.#index).join('/'))) 142 | } 143 | 144 | /** 145 | * true if there are more pattern parts after this one 146 | */ 147 | hasMore(): boolean { 148 | return this.length > this.#index + 1 149 | } 150 | 151 | /** 152 | * The rest of the pattern after this part, or null if this is the end 153 | */ 154 | rest(): Pattern | null { 155 | if (this.#rest !== undefined) return this.#rest 156 | if (!this.hasMore()) return (this.#rest = null) 157 | this.#rest = new Pattern( 158 | this.#patternList, 159 | this.#globList, 160 | this.#index + 1, 161 | this.#platform, 162 | ) 163 | this.#rest.#isAbsolute = this.#isAbsolute 164 | this.#rest.#isUNC = this.#isUNC 165 | this.#rest.#isDrive = this.#isDrive 166 | return this.#rest 167 | } 168 | 169 | /** 170 | * true if the pattern represents a //unc/path/ on windows 171 | */ 172 | isUNC(): boolean { 173 | const pl = this.#patternList 174 | return this.#isUNC !== undefined ? 175 | this.#isUNC 176 | : (this.#isUNC = 177 | this.#platform === 'win32' && 178 | this.#index === 0 && 179 | pl[0] === '' && 180 | pl[1] === '' && 181 | typeof pl[2] === 'string' && 182 | !!pl[2] && 183 | typeof pl[3] === 'string' && 184 | !!pl[3]) 185 | } 186 | 187 | // pattern like C:/... 188 | // split = ['C:', ...] 189 | // XXX: would be nice to handle patterns like `c:*` to test the cwd 190 | // in c: for *, but I don't know of a way to even figure out what that 191 | // cwd is without actually chdir'ing into it? 192 | /** 193 | * True if the pattern starts with a drive letter on Windows 194 | */ 195 | isDrive(): boolean { 196 | const pl = this.#patternList 197 | return this.#isDrive !== undefined ? 198 | this.#isDrive 199 | : (this.#isDrive = 200 | this.#platform === 'win32' && 201 | this.#index === 0 && 202 | this.length > 1 && 203 | typeof pl[0] === 'string' && 204 | /^[a-z]:$/i.test(pl[0])) 205 | } 206 | 207 | // pattern = '/' or '/...' or '/x/...' 208 | // split = ['', ''] or ['', ...] or ['', 'x', ...] 209 | // Drive and UNC both considered absolute on windows 210 | /** 211 | * True if the pattern is rooted on an absolute path 212 | */ 213 | isAbsolute(): boolean { 214 | const pl = this.#patternList 215 | return this.#isAbsolute !== undefined ? 216 | this.#isAbsolute 217 | : (this.#isAbsolute = 218 | (pl[0] === '' && pl.length > 1) || 219 | this.isDrive() || 220 | this.isUNC()) 221 | } 222 | 223 | /** 224 | * consume the root of the pattern, and return it 225 | */ 226 | root(): string { 227 | const p = this.#patternList[0] 228 | return ( 229 | typeof p === 'string' && this.isAbsolute() && this.#index === 0 230 | ) ? 231 | p 232 | : '' 233 | } 234 | 235 | /** 236 | * Check to see if the current globstar pattern is allowed to follow 237 | * a symbolic link. 238 | */ 239 | checkFollowGlobstar(): boolean { 240 | return !( 241 | this.#index === 0 || 242 | !this.isGlobstar() || 243 | !this.#followGlobstar 244 | ) 245 | } 246 | 247 | /** 248 | * Mark that the current globstar pattern is following a symbolic link 249 | */ 250 | markFollowGlobstar(): boolean { 251 | if (this.#index === 0 || !this.isGlobstar() || !this.#followGlobstar) 252 | return false 253 | this.#followGlobstar = false 254 | return true 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/processor.ts: -------------------------------------------------------------------------------- 1 | // synchronous utility for filtering entries and calculating subwalks 2 | 3 | import { GLOBSTAR, MMRegExp } from 'minimatch' 4 | import { Path } from 'path-scurry' 5 | import { MMPattern, Pattern } from './pattern.js' 6 | import { GlobWalkerOpts } from './walker.js' 7 | 8 | /** 9 | * A cache of which patterns have been processed for a given Path 10 | */ 11 | export class HasWalkedCache { 12 | store: Map> 13 | constructor(store: Map> = new Map()) { 14 | this.store = store 15 | } 16 | copy() { 17 | return new HasWalkedCache(new Map(this.store)) 18 | } 19 | hasWalked(target: Path, pattern: Pattern) { 20 | return this.store.get(target.fullpath())?.has(pattern.globString()) 21 | } 22 | storeWalked(target: Path, pattern: Pattern) { 23 | const fullpath = target.fullpath() 24 | const cached = this.store.get(fullpath) 25 | if (cached) cached.add(pattern.globString()) 26 | else this.store.set(fullpath, new Set([pattern.globString()])) 27 | } 28 | } 29 | 30 | /** 31 | * A record of which paths have been matched in a given walk step, 32 | * and whether they only are considered a match if they are a directory, 33 | * and whether their absolute or relative path should be returned. 34 | */ 35 | export class MatchRecord { 36 | store: Map = new Map() 37 | add(target: Path, absolute: boolean, ifDir: boolean) { 38 | const n = (absolute ? 2 : 0) | (ifDir ? 1 : 0) 39 | const current = this.store.get(target) 40 | this.store.set(target, current === undefined ? n : n & current) 41 | } 42 | // match, absolute, ifdir 43 | entries(): [Path, boolean, boolean][] { 44 | return [...this.store.entries()].map(([path, n]) => [ 45 | path, 46 | !!(n & 2), 47 | !!(n & 1), 48 | ]) 49 | } 50 | } 51 | 52 | /** 53 | * A collection of patterns that must be processed in a subsequent step 54 | * for a given path. 55 | */ 56 | export class SubWalks { 57 | store: Map = new Map() 58 | add(target: Path, pattern: Pattern) { 59 | if (!target.canReaddir()) { 60 | return 61 | } 62 | const subs = this.store.get(target) 63 | if (subs) { 64 | if (!subs.find(p => p.globString() === pattern.globString())) { 65 | subs.push(pattern) 66 | } 67 | } else this.store.set(target, [pattern]) 68 | } 69 | get(target: Path): Pattern[] { 70 | const subs = this.store.get(target) 71 | /* c8 ignore start */ 72 | if (!subs) { 73 | throw new Error('attempting to walk unknown path') 74 | } 75 | /* c8 ignore stop */ 76 | return subs 77 | } 78 | entries(): [Path, Pattern[]][] { 79 | return this.keys().map(k => [k, this.store.get(k) as Pattern[]]) 80 | } 81 | keys(): Path[] { 82 | return [...this.store.keys()].filter(t => t.canReaddir()) 83 | } 84 | } 85 | 86 | /** 87 | * The class that processes patterns for a given path. 88 | * 89 | * Handles child entry filtering, and determining whether a path's 90 | * directory contents must be read. 91 | */ 92 | export class Processor { 93 | hasWalkedCache: HasWalkedCache 94 | matches = new MatchRecord() 95 | subwalks = new SubWalks() 96 | patterns?: Pattern[] 97 | follow: boolean 98 | dot: boolean 99 | opts: GlobWalkerOpts 100 | 101 | constructor(opts: GlobWalkerOpts, hasWalkedCache?: HasWalkedCache) { 102 | this.opts = opts 103 | this.follow = !!opts.follow 104 | this.dot = !!opts.dot 105 | this.hasWalkedCache = 106 | hasWalkedCache ? hasWalkedCache.copy() : new HasWalkedCache() 107 | } 108 | 109 | processPatterns(target: Path, patterns: Pattern[]) { 110 | this.patterns = patterns 111 | const processingSet: [Path, Pattern][] = patterns.map(p => [target, p]) 112 | 113 | // map of paths to the magic-starting subwalks they need to walk 114 | // first item in patterns is the filter 115 | 116 | for (let [t, pattern] of processingSet) { 117 | this.hasWalkedCache.storeWalked(t, pattern) 118 | 119 | const root = pattern.root() 120 | const absolute = pattern.isAbsolute() && this.opts.absolute !== false 121 | 122 | // start absolute patterns at root 123 | if (root) { 124 | t = t.resolve( 125 | root === '/' && this.opts.root !== undefined ? 126 | this.opts.root 127 | : root, 128 | ) 129 | const rest = pattern.rest() 130 | if (!rest) { 131 | this.matches.add(t, true, false) 132 | continue 133 | } else { 134 | pattern = rest 135 | } 136 | } 137 | 138 | if (t.isENOENT()) continue 139 | 140 | let p: MMPattern 141 | let rest: Pattern | null 142 | let changed = false 143 | while ( 144 | typeof (p = pattern.pattern()) === 'string' && 145 | (rest = pattern.rest()) 146 | ) { 147 | const c = t.resolve(p) 148 | t = c 149 | pattern = rest 150 | changed = true 151 | } 152 | p = pattern.pattern() 153 | rest = pattern.rest() 154 | if (changed) { 155 | if (this.hasWalkedCache.hasWalked(t, pattern)) continue 156 | this.hasWalkedCache.storeWalked(t, pattern) 157 | } 158 | 159 | // now we have either a final string for a known entry, 160 | // more strings for an unknown entry, 161 | // or a pattern starting with magic, mounted on t. 162 | if (typeof p === 'string') { 163 | // must not be final entry, otherwise we would have 164 | // concatenated it earlier. 165 | const ifDir = p === '..' || p === '' || p === '.' 166 | this.matches.add(t.resolve(p), absolute, ifDir) 167 | continue 168 | } else if (p === GLOBSTAR) { 169 | // if no rest, match and subwalk pattern 170 | // if rest, process rest and subwalk pattern 171 | // if it's a symlink, but we didn't get here by way of a 172 | // globstar match (meaning it's the first time THIS globstar 173 | // has traversed a symlink), then we follow it. Otherwise, stop. 174 | if ( 175 | !t.isSymbolicLink() || 176 | this.follow || 177 | pattern.checkFollowGlobstar() 178 | ) { 179 | this.subwalks.add(t, pattern) 180 | } 181 | const rp = rest?.pattern() 182 | const rrest = rest?.rest() 183 | if (!rest || ((rp === '' || rp === '.') && !rrest)) { 184 | // only HAS to be a dir if it ends in **/ or **/. 185 | // but ending in ** will match files as well. 186 | this.matches.add(t, absolute, rp === '' || rp === '.') 187 | } else { 188 | if (rp === '..') { 189 | // this would mean you're matching **/.. at the fs root, 190 | // and no thanks, I'm not gonna test that specific case. 191 | /* c8 ignore start */ 192 | const tp = t.parent || t 193 | /* c8 ignore stop */ 194 | if (!rrest) this.matches.add(tp, absolute, true) 195 | else if (!this.hasWalkedCache.hasWalked(tp, rrest)) { 196 | this.subwalks.add(tp, rrest) 197 | } 198 | } 199 | } 200 | } else if (p instanceof RegExp) { 201 | this.subwalks.add(t, pattern) 202 | } 203 | } 204 | 205 | return this 206 | } 207 | 208 | subwalkTargets(): Path[] { 209 | return this.subwalks.keys() 210 | } 211 | 212 | child() { 213 | return new Processor(this.opts, this.hasWalkedCache) 214 | } 215 | 216 | // return a new Processor containing the subwalks for each 217 | // child entry, and a set of matches, and 218 | // a hasWalkedCache that's a copy of this one 219 | // then we're going to call 220 | filterEntries(parent: Path, entries: Path[]): Processor { 221 | const patterns = this.subwalks.get(parent) 222 | // put matches and entry walks into the results processor 223 | const results = this.child() 224 | for (const e of entries) { 225 | for (const pattern of patterns) { 226 | const absolute = pattern.isAbsolute() 227 | const p = pattern.pattern() 228 | const rest = pattern.rest() 229 | if (p === GLOBSTAR) { 230 | results.testGlobstar(e, pattern, rest, absolute) 231 | } else if (p instanceof RegExp) { 232 | results.testRegExp(e, p, rest, absolute) 233 | } else { 234 | results.testString(e, p, rest, absolute) 235 | } 236 | } 237 | } 238 | return results 239 | } 240 | 241 | testGlobstar( 242 | e: Path, 243 | pattern: Pattern, 244 | rest: Pattern | null, 245 | absolute: boolean, 246 | ) { 247 | if (this.dot || !e.name.startsWith('.')) { 248 | if (!pattern.hasMore()) { 249 | this.matches.add(e, absolute, false) 250 | } 251 | if (e.canReaddir()) { 252 | // if we're in follow mode or it's not a symlink, just keep 253 | // testing the same pattern. If there's more after the globstar, 254 | // then this symlink consumes the globstar. If not, then we can 255 | // follow at most ONE symlink along the way, so we mark it, which 256 | // also checks to ensure that it wasn't already marked. 257 | if (this.follow || !e.isSymbolicLink()) { 258 | this.subwalks.add(e, pattern) 259 | } else if (e.isSymbolicLink()) { 260 | if (rest && pattern.checkFollowGlobstar()) { 261 | this.subwalks.add(e, rest) 262 | } else if (pattern.markFollowGlobstar()) { 263 | this.subwalks.add(e, pattern) 264 | } 265 | } 266 | } 267 | } 268 | // if the NEXT thing matches this entry, then also add 269 | // the rest. 270 | if (rest) { 271 | const rp = rest.pattern() 272 | if ( 273 | typeof rp === 'string' && 274 | // dots and empty were handled already 275 | rp !== '..' && 276 | rp !== '' && 277 | rp !== '.' 278 | ) { 279 | this.testString(e, rp, rest.rest(), absolute) 280 | } else if (rp === '..') { 281 | /* c8 ignore start */ 282 | const ep = e.parent || e 283 | /* c8 ignore stop */ 284 | this.subwalks.add(ep, rest) 285 | } else if (rp instanceof RegExp) { 286 | this.testRegExp(e, rp, rest.rest(), absolute) 287 | } 288 | } 289 | } 290 | 291 | testRegExp( 292 | e: Path, 293 | p: MMRegExp, 294 | rest: Pattern | null, 295 | absolute: boolean, 296 | ) { 297 | if (!p.test(e.name)) return 298 | if (!rest) { 299 | this.matches.add(e, absolute, false) 300 | } else { 301 | this.subwalks.add(e, rest) 302 | } 303 | } 304 | 305 | testString(e: Path, p: string, rest: Pattern | null, absolute: boolean) { 306 | // should never happen? 307 | if (!e.isNamed(p)) return 308 | if (!rest) { 309 | this.matches.add(e, absolute, false) 310 | } else { 311 | this.subwalks.add(e, rest) 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/walker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Single-use utility classes to provide functionality to the {@link Glob} 3 | * methods. 4 | * 5 | * @module 6 | */ 7 | import { Minipass } from 'minipass' 8 | import { Path } from 'path-scurry' 9 | import { Ignore, IgnoreLike } from './ignore.js' 10 | 11 | // XXX can we somehow make it so that it NEVER processes a given path more than 12 | // once, enough that the match set tracking is no longer needed? that'd speed 13 | // things up a lot. Or maybe bring back nounique, and skip it in that case? 14 | 15 | // a single minimatch set entry with 1 or more parts 16 | import { Pattern } from './pattern.js' 17 | import { Processor } from './processor.js' 18 | 19 | export interface GlobWalkerOpts { 20 | absolute?: boolean 21 | allowWindowsEscape?: boolean 22 | cwd?: string | URL 23 | dot?: boolean 24 | dotRelative?: boolean 25 | follow?: boolean 26 | ignore?: string | string[] | IgnoreLike 27 | mark?: boolean 28 | matchBase?: boolean 29 | // Note: maxDepth here means "maximum actual Path.depth()", 30 | // not "maximum depth beyond cwd" 31 | maxDepth?: number 32 | nobrace?: boolean 33 | nocase?: boolean 34 | nodir?: boolean 35 | noext?: boolean 36 | noglobstar?: boolean 37 | platform?: NodeJS.Platform 38 | posix?: boolean 39 | realpath?: boolean 40 | root?: string 41 | stat?: boolean 42 | signal?: AbortSignal 43 | windowsPathsNoEscape?: boolean 44 | withFileTypes?: boolean 45 | includeChildMatches?: boolean 46 | } 47 | 48 | export type GWOFileTypesTrue = GlobWalkerOpts & { 49 | withFileTypes: true 50 | } 51 | export type GWOFileTypesFalse = GlobWalkerOpts & { 52 | withFileTypes: false 53 | } 54 | export type GWOFileTypesUnset = GlobWalkerOpts & { 55 | withFileTypes?: undefined 56 | } 57 | 58 | export type Result = 59 | O extends GWOFileTypesTrue ? Path 60 | : O extends GWOFileTypesFalse ? string 61 | : O extends GWOFileTypesUnset ? string 62 | : Path | string 63 | 64 | export type Matches = 65 | O extends GWOFileTypesTrue ? Set 66 | : O extends GWOFileTypesFalse ? Set 67 | : O extends GWOFileTypesUnset ? Set 68 | : Set 69 | 70 | export type MatchStream = Minipass< 71 | Result, 72 | Result 73 | > 74 | 75 | const makeIgnore = ( 76 | ignore: string | string[] | IgnoreLike, 77 | opts: GlobWalkerOpts, 78 | ): IgnoreLike => 79 | typeof ignore === 'string' ? new Ignore([ignore], opts) 80 | : Array.isArray(ignore) ? new Ignore(ignore, opts) 81 | : ignore 82 | 83 | /** 84 | * basic walking utilities that all the glob walker types use 85 | */ 86 | export abstract class GlobUtil { 87 | path: Path 88 | patterns: Pattern[] 89 | opts: O 90 | seen: Set = new Set() 91 | paused: boolean = false 92 | aborted: boolean = false 93 | #onResume: (() => any)[] = [] 94 | #ignore?: IgnoreLike 95 | #sep: '\\' | '/' 96 | signal?: AbortSignal 97 | maxDepth: number 98 | includeChildMatches: boolean 99 | 100 | constructor(patterns: Pattern[], path: Path, opts: O) 101 | constructor(patterns: Pattern[], path: Path, opts: O) { 102 | this.patterns = patterns 103 | this.path = path 104 | this.opts = opts 105 | this.#sep = !opts.posix && opts.platform === 'win32' ? '\\' : '/' 106 | this.includeChildMatches = opts.includeChildMatches !== false 107 | if (opts.ignore || !this.includeChildMatches) { 108 | this.#ignore = makeIgnore(opts.ignore ?? [], opts) 109 | if ( 110 | !this.includeChildMatches && 111 | typeof this.#ignore.add !== 'function' 112 | ) { 113 | const m = 'cannot ignore child matches, ignore lacks add() method.' 114 | throw new Error(m) 115 | } 116 | } 117 | // ignore, always set with maxDepth, but it's optional on the 118 | // GlobOptions type 119 | /* c8 ignore start */ 120 | this.maxDepth = opts.maxDepth || Infinity 121 | /* c8 ignore stop */ 122 | if (opts.signal) { 123 | this.signal = opts.signal 124 | this.signal.addEventListener('abort', () => { 125 | this.#onResume.length = 0 126 | }) 127 | } 128 | } 129 | 130 | #ignored(path: Path): boolean { 131 | return this.seen.has(path) || !!this.#ignore?.ignored?.(path) 132 | } 133 | #childrenIgnored(path: Path): boolean { 134 | return !!this.#ignore?.childrenIgnored?.(path) 135 | } 136 | 137 | // backpressure mechanism 138 | pause() { 139 | this.paused = true 140 | } 141 | resume() { 142 | /* c8 ignore start */ 143 | if (this.signal?.aborted) return 144 | /* c8 ignore stop */ 145 | this.paused = false 146 | let fn: (() => any) | undefined = undefined 147 | while (!this.paused && (fn = this.#onResume.shift())) { 148 | fn() 149 | } 150 | } 151 | onResume(fn: () => any) { 152 | if (this.signal?.aborted) return 153 | /* c8 ignore start */ 154 | if (!this.paused) { 155 | fn() 156 | } else { 157 | /* c8 ignore stop */ 158 | this.#onResume.push(fn) 159 | } 160 | } 161 | 162 | // do the requisite realpath/stat checking, and return the path 163 | // to add or undefined to filter it out. 164 | async matchCheck(e: Path, ifDir: boolean): Promise { 165 | if (ifDir && this.opts.nodir) return undefined 166 | let rpc: Path | undefined 167 | if (this.opts.realpath) { 168 | rpc = e.realpathCached() || (await e.realpath()) 169 | if (!rpc) return undefined 170 | e = rpc 171 | } 172 | const needStat = e.isUnknown() || this.opts.stat 173 | const s = needStat ? await e.lstat() : e 174 | if (this.opts.follow && this.opts.nodir && s?.isSymbolicLink()) { 175 | const target = await s.realpath() 176 | /* c8 ignore start */ 177 | if (target && (target.isUnknown() || this.opts.stat)) { 178 | await target.lstat() 179 | } 180 | /* c8 ignore stop */ 181 | } 182 | return this.matchCheckTest(s, ifDir) 183 | } 184 | 185 | matchCheckTest(e: Path | undefined, ifDir: boolean): Path | undefined { 186 | return ( 187 | e && 188 | (this.maxDepth === Infinity || e.depth() <= this.maxDepth) && 189 | (!ifDir || e.canReaddir()) && 190 | (!this.opts.nodir || !e.isDirectory()) && 191 | (!this.opts.nodir || 192 | !this.opts.follow || 193 | !e.isSymbolicLink() || 194 | !e.realpathCached()?.isDirectory()) && 195 | !this.#ignored(e) 196 | ) ? 197 | e 198 | : undefined 199 | } 200 | 201 | matchCheckSync(e: Path, ifDir: boolean): Path | undefined { 202 | if (ifDir && this.opts.nodir) return undefined 203 | let rpc: Path | undefined 204 | if (this.opts.realpath) { 205 | rpc = e.realpathCached() || e.realpathSync() 206 | if (!rpc) return undefined 207 | e = rpc 208 | } 209 | const needStat = e.isUnknown() || this.opts.stat 210 | const s = needStat ? e.lstatSync() : e 211 | if (this.opts.follow && this.opts.nodir && s?.isSymbolicLink()) { 212 | const target = s.realpathSync() 213 | if (target && (target?.isUnknown() || this.opts.stat)) { 214 | target.lstatSync() 215 | } 216 | } 217 | return this.matchCheckTest(s, ifDir) 218 | } 219 | 220 | abstract matchEmit(p: Result): void 221 | abstract matchEmit(p: string | Path): void 222 | 223 | matchFinish(e: Path, absolute: boolean) { 224 | if (this.#ignored(e)) return 225 | // we know we have an ignore if this is false, but TS doesn't 226 | if (!this.includeChildMatches && this.#ignore?.add) { 227 | const ign = `${e.relativePosix()}/**` 228 | this.#ignore.add(ign) 229 | } 230 | const abs = 231 | this.opts.absolute === undefined ? absolute : this.opts.absolute 232 | this.seen.add(e) 233 | const mark = this.opts.mark && e.isDirectory() ? this.#sep : '' 234 | // ok, we have what we need! 235 | if (this.opts.withFileTypes) { 236 | this.matchEmit(e) 237 | } else if (abs) { 238 | const abs = this.opts.posix ? e.fullpathPosix() : e.fullpath() 239 | this.matchEmit(abs + mark) 240 | } else { 241 | const rel = this.opts.posix ? e.relativePosix() : e.relative() 242 | const pre = 243 | this.opts.dotRelative && !rel.startsWith('..' + this.#sep) ? 244 | '.' + this.#sep 245 | : '' 246 | this.matchEmit(!rel ? '.' + mark : pre + rel + mark) 247 | } 248 | } 249 | 250 | async match(e: Path, absolute: boolean, ifDir: boolean): Promise { 251 | const p = await this.matchCheck(e, ifDir) 252 | if (p) this.matchFinish(p, absolute) 253 | } 254 | 255 | matchSync(e: Path, absolute: boolean, ifDir: boolean): void { 256 | const p = this.matchCheckSync(e, ifDir) 257 | if (p) this.matchFinish(p, absolute) 258 | } 259 | 260 | walkCB(target: Path, patterns: Pattern[], cb: () => any) { 261 | /* c8 ignore start */ 262 | if (this.signal?.aborted) cb() 263 | /* c8 ignore stop */ 264 | this.walkCB2(target, patterns, new Processor(this.opts), cb) 265 | } 266 | 267 | walkCB2( 268 | target: Path, 269 | patterns: Pattern[], 270 | processor: Processor, 271 | cb: () => any, 272 | ) { 273 | if (this.#childrenIgnored(target)) return cb() 274 | if (this.signal?.aborted) cb() 275 | if (this.paused) { 276 | this.onResume(() => this.walkCB2(target, patterns, processor, cb)) 277 | return 278 | } 279 | processor.processPatterns(target, patterns) 280 | 281 | // done processing. all of the above is sync, can be abstracted out. 282 | // subwalks is a map of paths to the entry filters they need 283 | // matches is a map of paths to [absolute, ifDir] tuples. 284 | let tasks = 1 285 | const next = () => { 286 | if (--tasks === 0) cb() 287 | } 288 | 289 | for (const [m, absolute, ifDir] of processor.matches.entries()) { 290 | if (this.#ignored(m)) continue 291 | tasks++ 292 | this.match(m, absolute, ifDir).then(() => next()) 293 | } 294 | 295 | for (const t of processor.subwalkTargets()) { 296 | if (this.maxDepth !== Infinity && t.depth() >= this.maxDepth) { 297 | continue 298 | } 299 | tasks++ 300 | const childrenCached = t.readdirCached() 301 | if (t.calledReaddir()) 302 | this.walkCB3(t, childrenCached, processor, next) 303 | else { 304 | t.readdirCB( 305 | (_, entries) => this.walkCB3(t, entries, processor, next), 306 | true, 307 | ) 308 | } 309 | } 310 | 311 | next() 312 | } 313 | 314 | walkCB3( 315 | target: Path, 316 | entries: Path[], 317 | processor: Processor, 318 | cb: () => any, 319 | ) { 320 | processor = processor.filterEntries(target, entries) 321 | 322 | let tasks = 1 323 | const next = () => { 324 | if (--tasks === 0) cb() 325 | } 326 | 327 | for (const [m, absolute, ifDir] of processor.matches.entries()) { 328 | if (this.#ignored(m)) continue 329 | tasks++ 330 | this.match(m, absolute, ifDir).then(() => next()) 331 | } 332 | for (const [target, patterns] of processor.subwalks.entries()) { 333 | tasks++ 334 | this.walkCB2(target, patterns, processor.child(), next) 335 | } 336 | 337 | next() 338 | } 339 | 340 | walkCBSync(target: Path, patterns: Pattern[], cb: () => any) { 341 | /* c8 ignore start */ 342 | if (this.signal?.aborted) cb() 343 | /* c8 ignore stop */ 344 | this.walkCB2Sync(target, patterns, new Processor(this.opts), cb) 345 | } 346 | 347 | walkCB2Sync( 348 | target: Path, 349 | patterns: Pattern[], 350 | processor: Processor, 351 | cb: () => any, 352 | ) { 353 | if (this.#childrenIgnored(target)) return cb() 354 | if (this.signal?.aborted) cb() 355 | if (this.paused) { 356 | this.onResume(() => 357 | this.walkCB2Sync(target, patterns, processor, cb), 358 | ) 359 | return 360 | } 361 | processor.processPatterns(target, patterns) 362 | 363 | // done processing. all of the above is sync, can be abstracted out. 364 | // subwalks is a map of paths to the entry filters they need 365 | // matches is a map of paths to [absolute, ifDir] tuples. 366 | let tasks = 1 367 | const next = () => { 368 | if (--tasks === 0) cb() 369 | } 370 | 371 | for (const [m, absolute, ifDir] of processor.matches.entries()) { 372 | if (this.#ignored(m)) continue 373 | this.matchSync(m, absolute, ifDir) 374 | } 375 | 376 | for (const t of processor.subwalkTargets()) { 377 | if (this.maxDepth !== Infinity && t.depth() >= this.maxDepth) { 378 | continue 379 | } 380 | tasks++ 381 | const children = t.readdirSync() 382 | this.walkCB3Sync(t, children, processor, next) 383 | } 384 | 385 | next() 386 | } 387 | 388 | walkCB3Sync( 389 | target: Path, 390 | entries: Path[], 391 | processor: Processor, 392 | cb: () => any, 393 | ) { 394 | processor = processor.filterEntries(target, entries) 395 | 396 | let tasks = 1 397 | const next = () => { 398 | if (--tasks === 0) cb() 399 | } 400 | 401 | for (const [m, absolute, ifDir] of processor.matches.entries()) { 402 | if (this.#ignored(m)) continue 403 | this.matchSync(m, absolute, ifDir) 404 | } 405 | for (const [target, patterns] of processor.subwalks.entries()) { 406 | tasks++ 407 | this.walkCB2Sync(target, patterns, processor.child(), next) 408 | } 409 | 410 | next() 411 | } 412 | } 413 | 414 | export class GlobWalker< 415 | O extends GlobWalkerOpts = GlobWalkerOpts, 416 | > extends GlobUtil { 417 | matches = new Set>() 418 | 419 | constructor(patterns: Pattern[], path: Path, opts: O) { 420 | super(patterns, path, opts) 421 | } 422 | 423 | matchEmit(e: Result): void { 424 | this.matches.add(e) 425 | } 426 | 427 | async walk(): Promise>> { 428 | if (this.signal?.aborted) throw this.signal.reason 429 | if (this.path.isUnknown()) { 430 | await this.path.lstat() 431 | } 432 | await new Promise((res, rej) => { 433 | this.walkCB(this.path, this.patterns, () => { 434 | if (this.signal?.aborted) { 435 | rej(this.signal.reason) 436 | } else { 437 | res(this.matches) 438 | } 439 | }) 440 | }) 441 | return this.matches 442 | } 443 | 444 | walkSync(): Set> { 445 | if (this.signal?.aborted) throw this.signal.reason 446 | if (this.path.isUnknown()) { 447 | this.path.lstatSync() 448 | } 449 | // nothing for the callback to do, because this never pauses 450 | this.walkCBSync(this.path, this.patterns, () => { 451 | if (this.signal?.aborted) throw this.signal.reason 452 | }) 453 | return this.matches 454 | } 455 | } 456 | 457 | export class GlobStream< 458 | O extends GlobWalkerOpts = GlobWalkerOpts, 459 | > extends GlobUtil { 460 | results: Minipass, Result> 461 | 462 | constructor(patterns: Pattern[], path: Path, opts: O) { 463 | super(patterns, path, opts) 464 | this.results = new Minipass, Result>({ 465 | signal: this.signal, 466 | objectMode: true, 467 | }) 468 | this.results.on('drain', () => this.resume()) 469 | this.results.on('resume', () => this.resume()) 470 | } 471 | 472 | matchEmit(e: Result): void { 473 | this.results.write(e) 474 | if (!this.results.flowing) this.pause() 475 | } 476 | 477 | stream(): MatchStream { 478 | const target = this.path 479 | if (target.isUnknown()) { 480 | target.lstat().then(() => { 481 | this.walkCB(target, this.patterns, () => this.results.end()) 482 | }) 483 | } else { 484 | this.walkCB(target, this.patterns, () => this.results.end()) 485 | } 486 | return this.results 487 | } 488 | 489 | streamSync(): MatchStream { 490 | if (this.path.isUnknown()) { 491 | this.path.lstatSync() 492 | } 493 | this.walkCBSync(this.path, this.patterns, () => this.results.end()) 494 | return this.results 495 | } 496 | } 497 | -------------------------------------------------------------------------------- /tap-snapshots/test/bin.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/bin.ts > TAP > usage > -h shows usage 1`] = ` 9 | Object { 10 | "args": Array [ 11 | "-h", 12 | ], 13 | "code": 0, 14 | "options": Object {}, 15 | "signal": null, 16 | "stderr": "", 17 | "stdout": String( 18 | Usage: 19 | glob [options] [ [ ...]] 20 | 21 | Glob v{VERSION} 22 | 23 | Expand the positional glob expression arguments into any matching file system 24 | paths found. 25 | 26 | -c --cmd= 27 | Run the command provided, passing the glob expression 28 | matches as arguments. 29 | 30 | -p --default= 31 | If no positional arguments are provided, glob will use 32 | this pattern 33 | 34 | -A --all By default, the glob cli command will not expand any 35 | arguments that are an exact match to a file on disk. 36 | 37 | This prevents double-expanding, in case the shell 38 | expands an argument whose filename is a glob 39 | expression. 40 | 41 | For example, if 'app/*.ts' would match 'app/[id].ts', 42 | then on Windows powershell or cmd.exe, 'glob app/*.ts' 43 | will expand to 'app/[id].ts', as expected. However, in 44 | posix shells such as bash or zsh, the shell will first 45 | expand 'app/*.ts' to a list of filenames. Then glob 46 | will look for a file matching 'app/[id].ts' (ie, 47 | 'app/i.ts' or 'app/d.ts'), which is unexpected. 48 | 49 | Setting '--all' prevents this behavior, causing glob to 50 | treat ALL patterns as glob expressions to be expanded, 51 | even if they are an exact match to a file on disk. 52 | 53 | When setting this option, be sure to enquote arguments 54 | so that the shell will not expand them prior to passing 55 | them to the glob command process. 56 | 57 | -a --absolute Expand to absolute paths 58 | -d --dot-relative Prepend './' on relative matches 59 | -m --mark Append a / on any directories matched 60 | -x --posix Always resolve to posix style paths, using '/' as the 61 | directory separator, even on Windows. Drive letter 62 | absolute matches on Windows will be expanded to their 63 | full resolved UNC maths, eg instead of 'C:\\\\foo\\\\bar', it 64 | will expand to '//?/C:/foo/bar'. 65 | 66 | -f --follow Follow symlinked directories when expanding '**' 67 | -R --realpath Call 'fs.realpath' on all of the results. In the case 68 | of an entry that cannot be resolved, the entry is 69 | omitted. This incurs a slight performance penalty, of 70 | course, because of the added system calls. 71 | 72 | -s --stat Call 'fs.lstat' on all entries, whether required or not 73 | to determine if it's a valid match. 74 | 75 | -b --match-base Perform a basename-only match if the pattern does not 76 | contain any slash characters. That is, '*.js' would be 77 | treated as equivalent to '**/*.js', matching js files 78 | in all directories. 79 | 80 | --dot Allow patterns to match files/directories that start 81 | with '.', even if the pattern does not start with '.' 82 | 83 | --nobrace Do not expand {...} patterns 84 | --nocase Perform a case-insensitive match. This defaults to 85 | 'true' on macOS and Windows platforms, and false on all 86 | others. 87 | 88 | Note: 'nocase' should only be explicitly set when it is 89 | known that the filesystem's case sensitivity differs 90 | from the platform default. If set 'true' on 91 | case-insensitive file systems, then the walk may return 92 | more or less results than expected. 93 | 94 | --nodir Do not match directories, only files. 95 | 96 | Note: to *only* match directories, append a '/' at the 97 | end of the pattern. 98 | 99 | --noext Do not expand extglob patterns, such as '+(a|b)' 100 | --noglobstar Do not expand '**' against multiple path portions. Ie, 101 | treat it as a normal '*' instead. 102 | 103 | --windows-path-no-escape 104 | Use '\\\\' as a path separator *only*, and *never* as an 105 | escape character. If set, all '\\\\' characters are 106 | replaced with '/' in the pattern. 107 | 108 | -D --max-depth= Maximum depth to traverse from the current working 109 | directory 110 | 111 | -C --cwd= Current working directory to execute/match in 112 | -r --root= A string path resolved against the 'cwd', which is used 113 | as the starting point for absolute patterns that start 114 | with '/' (but not drive letters or UNC paths on 115 | Windows). 116 | 117 | Note that this *doesn't* necessarily limit the walk to 118 | the 'root' directory, and doesn't affect the cwd 119 | starting point for non-absolute patterns. A pattern 120 | containing '..' will still be able to traverse out of 121 | the root directory, if it is not an actual root 122 | directory on the filesystem, and any non-absolute 123 | patterns will still be matched in the 'cwd'. 124 | 125 | To start absolute and non-absolute patterns in the same 126 | path, you can use '--root=' to set it to the empty 127 | string. However, be aware that on Windows systems, a 128 | pattern like 'x:/*' or '//host/share/*' will *always* 129 | start in the 'x:/' or '//host/share/' directory, 130 | regardless of the --root setting. 131 | 132 | --platform= Defaults to the value of 'process.platform' if 133 | available, or 'linux' if not. Setting --platform=win32 134 | on non-Windows systems may cause strange behavior! 135 | 136 | Valid options: "aix", "android", "darwin", "freebsd", 137 | "haiku", "linux", "openbsd", "sunos", "win32", 138 | "cygwin", "netbsd" 139 | 140 | -i --ignore= 141 | Glob patterns to ignore 142 | Can be set multiple times 143 | -v --debug Output a huge amount of noisy debug information about 144 | patterns as they are parsed and used to match files. 145 | 146 | -V --version Output the version ({VERSION}) 147 | -h --help Show this usage information 148 | 149 | ), 150 | } 151 | ` 152 | 153 | exports[`test/bin.ts > TAP > version > --version shows version 1`] = ` 154 | Object { 155 | "args": Array [ 156 | "--version", 157 | ], 158 | "code": 0, 159 | "options": Object {}, 160 | "signal": null, 161 | "stderr": "", 162 | "stdout": "{VERSION}\\n", 163 | } 164 | ` 165 | 166 | exports[`test/bin.ts > TAP > version > -V shows version 1`] = ` 167 | Object { 168 | "args": Array [ 169 | "-V", 170 | ], 171 | "code": 0, 172 | "options": Object {}, 173 | "signal": null, 174 | "stderr": "", 175 | "stdout": "{VERSION}\\n", 176 | } 177 | ` 178 | -------------------------------------------------------------------------------- /tap-snapshots/test/root.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/root.ts > TAP > set root option > absolute=false > async 1`] = ` 9 | Array [ 10 | "x/x/a", 11 | "x/x/x/a", 12 | "x/x/x/y", 13 | "x/x/x/y/r", 14 | "x/x/y", 15 | "x/x/y/r", 16 | "x/y", 17 | "x/y/r", 18 | "y/r", 19 | ] 20 | ` 21 | 22 | exports[`test/root.ts > TAP > set root option > absolute=false > sync 1`] = ` 23 | Array [ 24 | "x/x/a", 25 | "x/x/x/a", 26 | "x/x/x/y", 27 | "x/x/x/y/r", 28 | "x/x/y", 29 | "x/x/y/r", 30 | "x/y", 31 | "x/y/r", 32 | "y/r", 33 | ] 34 | ` 35 | 36 | exports[`test/root.ts > TAP > set root option > absolute=true > async 1`] = ` 37 | Array [ 38 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/a", 39 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/x/a", 40 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/x/y", 41 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/x/y/r", 42 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/y", 43 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/y/r", 44 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/y", 45 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/y/r", 46 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/y/r", 47 | ] 48 | ` 49 | 50 | exports[`test/root.ts > TAP > set root option > absolute=true > sync 1`] = ` 51 | Array [ 52 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/a", 53 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/x/a", 54 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/x/y", 55 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/x/y/r", 56 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/y", 57 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/y/r", 58 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/y", 59 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/y/r", 60 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/y/r", 61 | ] 62 | ` 63 | 64 | exports[`test/root.ts > TAP > set root option > absolute=undefined > async 1`] = ` 65 | Array [ 66 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/a", 67 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/x/a", 68 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/x/y", 69 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/y", 70 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/y", 71 | "x/x/x/y/r", 72 | "x/x/y/r", 73 | "x/y/r", 74 | "y/r", 75 | ] 76 | ` 77 | 78 | exports[`test/root.ts > TAP > set root option > absolute=undefined > sync 1`] = ` 79 | Array [ 80 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/a", 81 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/x/a", 82 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/x/y", 83 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/x/y", 84 | "{CWD}/.tap/fixtures/test-root.ts-set-root-option/x/y", 85 | "x/x/x/y/r", 86 | "x/x/y/r", 87 | "x/y/r", 88 | "y/r", 89 | ] 90 | ` 91 | -------------------------------------------------------------------------------- /test/00-setup.ts: -------------------------------------------------------------------------------- 1 | // just a little pre-run script to set up the fixtures. 2 | // zz-finish cleans it up 3 | 4 | import { spawn } from 'child_process' 5 | import { createWriteStream, promises } from 'fs' 6 | import { mkdirp } from 'mkdirp' 7 | import { join, dirname, resolve } from 'path' 8 | import t from 'tap' 9 | import { fileURLToPath } from 'url' 10 | 11 | const { writeFile, symlink } = promises 12 | //@ts-ignore 13 | t.pipe(createWriteStream('00-setup.tap')) 14 | process.env.TAP_BAIL = '1' 15 | 16 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 17 | const fixtureDir = resolve(__dirname, 'fixtures') 18 | 19 | const filesUnresolved = [ 20 | 'a/.abcdef/x/y/z/a', 21 | 'a/abcdef/g/h', 22 | 'a/abcfed/g/h', 23 | 'a/b/c/d', 24 | 'a/bc/e/f', 25 | 'a/c/d/c/b', 26 | 'a/cb/e/f', 27 | 'a/x/.y/b', 28 | 'a/z/.y/b', 29 | ] 30 | 31 | const symlinkTo = resolve(fixtureDir, 'a/symlink/a/b/c') 32 | const symlinkFrom = '../..' 33 | 34 | const files = filesUnresolved.map(f => resolve(fixtureDir, f)) 35 | 36 | for (const file of files) { 37 | t.test(file, { bail: true }, async () => { 38 | const f = resolve(fixtureDir, file) 39 | const d = dirname(f) 40 | await mkdirp(d) 41 | await writeFile(f, 'i like tests') 42 | }) 43 | } 44 | 45 | if (process.platform !== 'win32') { 46 | t.test('symlinky', async () => { 47 | const d = dirname(symlinkTo) 48 | await mkdirp(d) 49 | await symlink(symlinkFrom, symlinkTo, 'dir') 50 | }) 51 | } 52 | 53 | ;['foo', 'bar', 'baz', 'asdf', 'quux', 'qwer', 'rewq'].forEach( 54 | function (w) { 55 | w = '/tmp/glob-test/' + w 56 | t.test('create ' + w, async t => { 57 | await mkdirp(w) 58 | t.pass(w) 59 | }) 60 | }, 61 | ) 62 | 63 | // generate the bash pattern test-fixtures if possible 64 | if (process.platform === 'win32' || !process.env.TEST_REGEN) { 65 | console.error('Windows, or TEST_REGEN unset. Using cached fixtures.') 66 | } else { 67 | const globs = 68 | // put more patterns here. 69 | // anything that would be directly in / should be in /tmp/glob-test 70 | [ 71 | 'a/c/d/*/b', 72 | 'a//c//d//*//b', 73 | 'a/*/d/*/b', 74 | 'a/*/+(c|g)/./d', 75 | 'a/**/[cg]/../[cg]', 76 | 'a/{b,c,d,e,f}/**/g', 77 | 'a/b/**', 78 | './**/g', 79 | 'a/abc{fed,def}/g/h', 80 | 'a/abc{fed/g,def}/**/', 81 | 'a/abc{fed/g,def}/**///**/', 82 | // When a ** is the FIRST item in a pattern, it has 83 | // more restrictive symbolic link handling behavior. 84 | '**/a', 85 | '**/a/**', 86 | './**/a', 87 | './**/a/**/', 88 | './**/a/**', 89 | './**/a/**/a/**/', 90 | '+(a|b|c)/a{/,bc*}/**', 91 | '*/*/*/f', 92 | './**/f', 93 | 'a/symlink/a/b/c/a/b/c/a/b/c//a/b/c////a/b/c/**/b/c/**', 94 | '{./*/*,/tmp/glob-test/*}', 95 | '{/tmp/glob-test/*,*}', // evil owl face! how you taunt me! 96 | 'a/!(symlink)/**', 97 | 'a/symlink/a/**/*', 98 | // this one we don't quite match bash, because when bash 99 | // applies the .. to the symlink walked by **, it effectively 100 | // resets the symlink walk limit, and that is just a step too 101 | // far for an edge case no one knows or cares about, even for 102 | // an obsessive perfectionist like me. 103 | // './a/**/../*/**', 104 | 'a/!(symlink)/**/..', 105 | 'a/!(symlink)/**/../', 106 | 'a/!(symlink)/**/../*', 107 | 'a/!(symlink)/**/../*/*', 108 | ] 109 | 110 | const bashOutput: { [k: string]: string[] } = {} 111 | 112 | for (const pattern of globs) { 113 | t.test('generate fixture ' + pattern, t => { 114 | const opts = [ 115 | '-O', 116 | 'globstar', 117 | '-O', 118 | 'extglob', 119 | '-O', 120 | 'nullglob', 121 | '-c', 122 | 'for i in ' + pattern + '; do echo $i; done', 123 | ] 124 | const cp = spawn('bash', opts, { cwd: fixtureDir }) 125 | const out: Buffer[] = [] 126 | cp.stdout.on('data', c => out.push(c)) 127 | cp.stderr.pipe(process.stderr) 128 | cp.on('close', function (code) { 129 | const o = flatten(out) 130 | bashOutput[pattern] = !o ? [] : cleanResults(o.split(/\r*\n/)) 131 | t.notOk(code, 'bash test should finish nicely') 132 | t.end() 133 | }) 134 | }) 135 | } 136 | 137 | t.test('save fixtures', async () => { 138 | const fname = resolve(__dirname, 'bash-results.ts') 139 | const data = `// generated via 'npm run test-regen' 140 | import { fileURLToPath } from 'url' 141 | 142 | if (process.argv[1] === fileURLToPath(import.meta.url)) { 143 | console.log('TAP version 14\\n1..1\\nok\\n') 144 | } 145 | 146 | export const bashResults:{ [path: string]: string[] } = ${ 147 | JSON.stringify(bashOutput, null, 2) + '\n' 148 | } 149 | ` 150 | await writeFile(fname, data) 151 | }) 152 | 153 | t.test('formatting', t => { 154 | const c = spawn( 155 | 'prettier', 156 | ['--write', resolve(__dirname, 'bash-results.ts')], 157 | { stdio: ['ignore', 2, 2] }, 158 | ) 159 | c.on('close', (code, signal) => { 160 | t.equal(code, 0, 'code') 161 | t.equal(signal, null, 'signal') 162 | t.end() 163 | }) 164 | }) 165 | 166 | function cleanResults(m: string[]) { 167 | // normalize discrepancies in ordering, duplication, 168 | // and ending slashes. 169 | return m 170 | .map(m => join(m.replace(/\/$/, '').replace(/\/+/g, '/'))) 171 | .sort(alphasort) 172 | .reduce(function (set: string[], f) { 173 | if (f !== set[set.length - 1]) set.push(f) 174 | return set 175 | }, []) 176 | .sort(alphasort) 177 | .map(function (f) { 178 | // de-windows 179 | return process.platform !== 'win32' ? 180 | f 181 | : f.replace(/^[a-zA-Z]:\\\\/, '/').replace(/\\/g, '/') 182 | }) 183 | } 184 | 185 | const flatten = (chunks: Buffer[]) => 186 | Buffer.concat(chunks).toString().trim() 187 | 188 | const alphasort = (a: string, b: string) => 189 | a.toLowerCase().localeCompare(b.toLowerCase(), 'en') 190 | } 191 | -------------------------------------------------------------------------------- /test/absolute-must-be-strings.ts: -------------------------------------------------------------------------------- 1 | import { Glob } from '../dist/esm/index.js' 2 | import t from 'tap' 3 | t.throws(() => { 4 | new Glob('.', { 5 | withFileTypes: true, 6 | absolute: true, 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /test/absolute.ts: -------------------------------------------------------------------------------- 1 | import { isAbsolute } from 'path' 2 | import t, { Test } from 'tap' 3 | import { fileURLToPath } from 'url' 4 | import { Glob } from '../dist/esm/index.js' 5 | import { bashResults } from './bash-results.js' 6 | 7 | const pattern = 'a/b/**' 8 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 9 | process.chdir(__dirname + '/fixtures') 10 | 11 | const ok = (t: Test, file: string) => 12 | t.ok(isAbsolute(file), 'must be absolute', { found: file }) 13 | 14 | var marks = [true, false] 15 | for (const mark of marks) { 16 | t.test('mark=' + mark, t => { 17 | t.plan(2) 18 | 19 | t.test('Emits absolute matches if option set', async t => { 20 | var g = new Glob(pattern, { absolute: true, posix: true }) 21 | const results = await g.walk() 22 | 23 | t.equal( 24 | results.length, 25 | bashResults[pattern]?.length, 26 | 'must match all files', 27 | ) 28 | for (const m of results) { 29 | t.ok(m.startsWith('/'), 'starts with / ' + m) 30 | } 31 | }) 32 | 33 | t.test('returns absolute results synchronously', async t => { 34 | var g = new Glob(pattern, { absolute: true }) 35 | const results = g.walkSync() 36 | 37 | t.equal( 38 | results.length, 39 | bashResults[pattern]?.length, 40 | 'must match all files', 41 | ) 42 | for (const m of results) { 43 | ok(t, m) 44 | } 45 | }) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /test/bash-comparison.ts: -------------------------------------------------------------------------------- 1 | // basic test 2 | // show that it does the same thing by default as the shell. 3 | import { resolve } from 'path' 4 | import t from 'tap' 5 | import { fileURLToPath } from 'url' 6 | import { glob } from '../dist/esm/index.js' 7 | import { bashResults } from './bash-results.js' 8 | const globs = Object.keys(bashResults) 9 | 10 | // run from the root of the project 11 | // this is usually where you're at anyway, but be sure. 12 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 13 | const fixtures = resolve(__dirname, 'fixtures') 14 | process.chdir(fixtures) 15 | 16 | const alphasort = (a: string, b: string) => 17 | a.toLowerCase().localeCompare(b.toLowerCase(), 'en') 18 | 19 | const cleanResults = (m: string[]) => { 20 | // normalize discrepancies in ordering, duplication, 21 | // and ending slashes. 22 | return m 23 | .map(m => m.replace(/\/$/, '')) 24 | .sort(alphasort) 25 | .reduce((set: string[], f) => { 26 | if (f !== set[set.length - 1]) set.push(f) 27 | return set 28 | }, []) 29 | .map(f => { 30 | // de-windows 31 | return process.platform !== 'win32' ? 32 | f 33 | : f.replace(/^[a-zA-Z]:[\/\\]+/, '/').replace(/[\\\/]+/g, '/') 34 | }) 35 | .sort(alphasort) 36 | } 37 | 38 | globs.forEach(function (pattern) { 39 | var expect = bashResults[pattern] 40 | // anything regarding the symlink thing will fail on windows, so just skip it 41 | if ( 42 | process.platform === 'win32' && 43 | expect?.some((m: string) => /\bsymlink\b/.test(m)) 44 | ) { 45 | return 46 | } 47 | 48 | t.test(pattern, async t => { 49 | // sort and unmark, just to match the shell results 50 | const matches = cleanResults(await glob(pattern)) 51 | t.same(matches, expect, pattern) 52 | }) 53 | 54 | t.test(pattern + ' sync', async t => { 55 | const matches = cleanResults(glob.globSync(pattern)) 56 | t.same(matches, expect, 'should match shell (sync)') 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /test/bash-results.ts: -------------------------------------------------------------------------------- 1 | // generated via 'npm run test-regen' 2 | import { fileURLToPath } from 'url' 3 | 4 | if (process.argv[1] === fileURLToPath(import.meta.url)) { 5 | console.log('TAP version 14\n1..1\nok\n') 6 | } 7 | 8 | export const bashResults: { [path: string]: string[] } = { 9 | 'a/c/d/*/b': ['a/c/d/c/b'], 10 | 'a//c//d//*//b': ['a/c/d/c/b'], 11 | 'a/*/d/*/b': ['a/c/d/c/b'], 12 | 'a/*/+(c|g)/./d': ['a/b/c/d'], 13 | 'a/**/[cg]/../[cg]': [ 14 | 'a/abcdef/g', 15 | 'a/abcfed/g', 16 | 'a/b/c', 17 | 'a/c', 18 | 'a/c/d/c', 19 | 'a/symlink/a/b/c', 20 | ], 21 | 'a/{b,c,d,e,f}/**/g': [], 22 | 'a/b/**': ['a/b', 'a/b/c', 'a/b/c/d'], 23 | './**/g': ['a/abcdef/g', 'a/abcfed/g'], 24 | 'a/abc{fed,def}/g/h': ['a/abcdef/g/h', 'a/abcfed/g/h'], 25 | 'a/abc{fed/g,def}/**/': ['a/abcdef', 'a/abcdef/g', 'a/abcfed/g'], 26 | 'a/abc{fed/g,def}/**///**/': ['a/abcdef', 'a/abcdef/g', 'a/abcfed/g'], 27 | '**/a': ['a', 'a/symlink/a'], 28 | '**/a/**': [ 29 | 'a', 30 | 'a/abcdef', 31 | 'a/abcdef/g', 32 | 'a/abcdef/g/h', 33 | 'a/abcfed', 34 | 'a/abcfed/g', 35 | 'a/abcfed/g/h', 36 | 'a/b', 37 | 'a/b/c', 38 | 'a/b/c/d', 39 | 'a/bc', 40 | 'a/bc/e', 41 | 'a/bc/e/f', 42 | 'a/c', 43 | 'a/c/d', 44 | 'a/c/d/c', 45 | 'a/c/d/c/b', 46 | 'a/cb', 47 | 'a/cb/e', 48 | 'a/cb/e/f', 49 | 'a/symlink', 50 | 'a/symlink/a', 51 | 'a/symlink/a/b', 52 | 'a/symlink/a/b/c', 53 | 'a/x', 54 | 'a/z', 55 | ], 56 | './**/a': ['a', 'a/symlink/a', 'a/symlink/a/b/c/a'], 57 | './**/a/**/': [ 58 | 'a', 59 | 'a/abcdef', 60 | 'a/abcdef/g', 61 | 'a/abcfed', 62 | 'a/abcfed/g', 63 | 'a/b', 64 | 'a/b/c', 65 | 'a/bc', 66 | 'a/bc/e', 67 | 'a/c', 68 | 'a/c/d', 69 | 'a/c/d/c', 70 | 'a/cb', 71 | 'a/cb/e', 72 | 'a/symlink', 73 | 'a/symlink/a', 74 | 'a/symlink/a/b', 75 | 'a/symlink/a/b/c', 76 | 'a/symlink/a/b/c/a', 77 | 'a/symlink/a/b/c/a/b', 78 | 'a/symlink/a/b/c/a/b/c', 79 | 'a/x', 80 | 'a/z', 81 | ], 82 | './**/a/**': [ 83 | 'a', 84 | 'a/abcdef', 85 | 'a/abcdef/g', 86 | 'a/abcdef/g/h', 87 | 'a/abcfed', 88 | 'a/abcfed/g', 89 | 'a/abcfed/g/h', 90 | 'a/b', 91 | 'a/b/c', 92 | 'a/b/c/d', 93 | 'a/bc', 94 | 'a/bc/e', 95 | 'a/bc/e/f', 96 | 'a/c', 97 | 'a/c/d', 98 | 'a/c/d/c', 99 | 'a/c/d/c/b', 100 | 'a/cb', 101 | 'a/cb/e', 102 | 'a/cb/e/f', 103 | 'a/symlink', 104 | 'a/symlink/a', 105 | 'a/symlink/a/b', 106 | 'a/symlink/a/b/c', 107 | 'a/symlink/a/b/c/a', 108 | 'a/symlink/a/b/c/a/b', 109 | 'a/symlink/a/b/c/a/b/c', 110 | 'a/x', 111 | 'a/z', 112 | ], 113 | './**/a/**/a/**/': [ 114 | 'a/symlink/a', 115 | 'a/symlink/a/b', 116 | 'a/symlink/a/b/c', 117 | 'a/symlink/a/b/c/a', 118 | 'a/symlink/a/b/c/a/b', 119 | 'a/symlink/a/b/c/a/b/c', 120 | 'a/symlink/a/b/c/a/b/c/a', 121 | 'a/symlink/a/b/c/a/b/c/a/b', 122 | 'a/symlink/a/b/c/a/b/c/a/b/c', 123 | ], 124 | '+(a|b|c)/a{/,bc*}/**': [ 125 | 'a/abcdef', 126 | 'a/abcdef/g', 127 | 'a/abcdef/g/h', 128 | 'a/abcfed', 129 | 'a/abcfed/g', 130 | 'a/abcfed/g/h', 131 | ], 132 | '*/*/*/f': ['a/bc/e/f', 'a/cb/e/f'], 133 | './**/f': ['a/bc/e/f', 'a/cb/e/f'], 134 | 'a/symlink/a/b/c/a/b/c/a/b/c//a/b/c////a/b/c/**/b/c/**': [ 135 | 'a/symlink/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c', 136 | 'a/symlink/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a', 137 | 'a/symlink/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b', 138 | 'a/symlink/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c', 139 | ], 140 | '{./*/*,/tmp/glob-test/*}': [ 141 | '/tmp/glob-test/asdf', 142 | '/tmp/glob-test/bar', 143 | '/tmp/glob-test/baz', 144 | '/tmp/glob-test/foo', 145 | '/tmp/glob-test/quux', 146 | '/tmp/glob-test/qwer', 147 | '/tmp/glob-test/rewq', 148 | 'a/abcdef', 149 | 'a/abcfed', 150 | 'a/b', 151 | 'a/bc', 152 | 'a/c', 153 | 'a/cb', 154 | 'a/symlink', 155 | 'a/x', 156 | 'a/z', 157 | ], 158 | '{/tmp/glob-test/*,*}': [ 159 | '/tmp/glob-test/asdf', 160 | '/tmp/glob-test/bar', 161 | '/tmp/glob-test/baz', 162 | '/tmp/glob-test/foo', 163 | '/tmp/glob-test/quux', 164 | '/tmp/glob-test/qwer', 165 | '/tmp/glob-test/rewq', 166 | 'a', 167 | ], 168 | 'a/!(symlink)/**': [ 169 | 'a/abcdef', 170 | 'a/abcdef/g', 171 | 'a/abcdef/g/h', 172 | 'a/abcfed', 173 | 'a/abcfed/g', 174 | 'a/abcfed/g/h', 175 | 'a/b', 176 | 'a/b/c', 177 | 'a/b/c/d', 178 | 'a/bc', 179 | 'a/bc/e', 180 | 'a/bc/e/f', 181 | 'a/c', 182 | 'a/c/d', 183 | 'a/c/d/c', 184 | 'a/c/d/c/b', 185 | 'a/cb', 186 | 'a/cb/e', 187 | 'a/cb/e/f', 188 | 'a/x', 189 | 'a/z', 190 | ], 191 | 'a/symlink/a/**/*': [ 192 | 'a/symlink/a/b', 193 | 'a/symlink/a/b/c', 194 | 'a/symlink/a/b/c/a', 195 | ], 196 | 'a/!(symlink)/**/..': [ 197 | 'a', 198 | 'a/abcdef', 199 | 'a/abcfed', 200 | 'a/b', 201 | 'a/bc', 202 | 'a/c', 203 | 'a/c/d', 204 | 'a/cb', 205 | ], 206 | 'a/!(symlink)/**/../': [ 207 | 'a', 208 | 'a/abcdef', 209 | 'a/abcfed', 210 | 'a/b', 211 | 'a/bc', 212 | 'a/c', 213 | 'a/c/d', 214 | 'a/cb', 215 | ], 216 | 'a/!(symlink)/**/../*': [ 217 | 'a/abcdef', 218 | 'a/abcdef/g', 219 | 'a/abcfed', 220 | 'a/abcfed/g', 221 | 'a/b', 222 | 'a/b/c', 223 | 'a/bc', 224 | 'a/bc/e', 225 | 'a/c', 226 | 'a/c/d', 227 | 'a/c/d/c', 228 | 'a/cb', 229 | 'a/cb/e', 230 | 'a/symlink', 231 | 'a/x', 232 | 'a/z', 233 | ], 234 | 'a/!(symlink)/**/../*/*': [ 235 | 'a/abcdef/g', 236 | 'a/abcdef/g/h', 237 | 'a/abcfed/g', 238 | 'a/abcfed/g/h', 239 | 'a/b/c', 240 | 'a/b/c/d', 241 | 'a/bc/e', 242 | 'a/bc/e/f', 243 | 'a/c/d', 244 | 'a/c/d/c', 245 | 'a/c/d/c/b', 246 | 'a/cb/e', 247 | 'a/cb/e/f', 248 | 'a/symlink/a', 249 | ], 250 | } 251 | -------------------------------------------------------------------------------- /test/bin.ts: -------------------------------------------------------------------------------- 1 | import { spawn, SpawnOptions } from 'child_process' 2 | import { readFileSync } from 'fs' 3 | import { sep } from 'path' 4 | import t from 'tap' 5 | import { fileURLToPath } from 'url' 6 | const { version } = JSON.parse( 7 | readFileSync( 8 | fileURLToPath(new URL('../package.json', import.meta.url)), 9 | 'utf8', 10 | ), 11 | ) 12 | const bin = fileURLToPath(new URL('../dist/esm/bin.mjs', import.meta.url)) 13 | 14 | t.cleanSnapshot = s => s.split(version).join('{VERSION}') 15 | 16 | interface Result { 17 | args: string[] 18 | options: SpawnOptions 19 | stdout: string 20 | stderr: string 21 | code: number | null 22 | signal: NodeJS.Signals | null 23 | } 24 | const run = async (args: string[], options = {}) => { 25 | const proc = spawn( 26 | process.execPath, 27 | ['--enable-source-maps', bin, ...args], 28 | options, 29 | ) 30 | const out: Buffer[] = [] 31 | const err: Buffer[] = [] 32 | proc.stdout.on('data', c => out.push(c)) 33 | proc.stderr.on('data', c => err.push(c)) 34 | return new Promise(res => { 35 | proc.on('close', (code, signal) => { 36 | res({ 37 | args, 38 | options, 39 | stdout: Buffer.concat(out).toString(), 40 | stderr: Buffer.concat(err).toString(), 41 | code, 42 | signal, 43 | }) 44 | }) 45 | }) 46 | } 47 | 48 | t.test('usage', async t => { 49 | t.matchSnapshot(await run(['-h']), '-h shows usage') 50 | const res = await run([]) 51 | t.equal(res.code, 1, 'exit with code 1 when no args') 52 | t.match(res.stderr, 'No patterns provided') 53 | t.match(res.stderr, /-h --help +Show this usage information$/m) 54 | const badp = await run(['--platform=glorb']) 55 | t.equal(badp.code, 1, 'exit with code 1 on bad platform arg') 56 | t.match(badp.stderr, 'Invalid value provided for --platform: "glorb"\n') 57 | }) 58 | 59 | t.test('version', async t => { 60 | t.matchSnapshot(await run(['-V']), '-V shows version') 61 | t.matchSnapshot(await run(['--version']), '--version shows version') 62 | }) 63 | 64 | t.test('finds matches for a pattern', async t => { 65 | const cwd = t.testdir({ 66 | a: { 67 | 'x.y': '', 68 | 'x.a': '', 69 | b: { 70 | 'z.y': '', 71 | 'z.a': '', 72 | }, 73 | }, 74 | }) 75 | const res = await run(['**/*.y'], { cwd }) 76 | t.match(res.stdout, `a${sep}x.y\n`) 77 | t.match(res.stdout, `a${sep}b${sep}z.y\n`) 78 | 79 | const c = `node -p "process.argv.map(s=>s.toUpperCase())"` 80 | const cmd = await run(['**/*.y', '-c', c], { cwd }) 81 | t.match(cmd.stdout, `'a${sep.replace(/\\/g, '\\\\')}x.y'`.toUpperCase()) 82 | t.match( 83 | cmd.stdout, 84 | `'a${sep.replace(/\\/g, '\\\\')}b${sep.replace( 85 | /\\/g, 86 | '\\\\', 87 | )}z.y'`.toUpperCase(), 88 | ) 89 | }) 90 | 91 | t.test('prioritizes exact match if exists, unless --all', async t => { 92 | const cwd = t.testdir({ 93 | routes: { 94 | '[id].tsx': '', 95 | 'i.tsx': '', 96 | 'd.tsx': '', 97 | }, 98 | }) 99 | const res = await run(['routes/[id].tsx'], { cwd }) 100 | t.equal(res.stdout, `routes${sep}[id].tsx\n`) 101 | 102 | const all = await run(['routes/[id].tsx', '--all'], { cwd }) 103 | t.match(all.stdout, `routes${sep}i.tsx\n`) 104 | t.match(all.stdout, `routes${sep}d.tsx\n`) 105 | }) 106 | 107 | t.test('uses default pattern if none provided', async t => { 108 | const cwd = t.testdir({ 109 | a: { 110 | 'x.y': '', 111 | 'x.a': '', 112 | b: { 113 | 'z.y': '', 114 | 'z.a': '', 115 | }, 116 | }, 117 | }) 118 | 119 | const def = await run(['-p', '**/*.y'], { cwd }) 120 | t.match(def.stdout, `a${sep}x.y\n`) 121 | t.match(def.stdout, `a${sep}b${sep}z.y\n`) 122 | 123 | const exp = await run(['-p', '**/*.y', '**/*.a'], { cwd }) 124 | t.match(exp.stdout, `a${sep}x.a\n`) 125 | t.match(exp.stdout, `a${sep}b${sep}z.a\n`) 126 | }) 127 | -------------------------------------------------------------------------------- /test/broken-symlink.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path' 2 | import t from 'tap' 3 | import { glob } from '../dist/esm/index.js' 4 | import { GlobOptionsWithFileTypesUnset } from '../dist/esm/glob.js' 5 | 6 | if (process.platform === 'win32') { 7 | t.plan(0, 'skip on windows') 8 | process.exit(0) 9 | } 10 | 11 | const dir = relative( 12 | process.cwd(), 13 | t.testdir({ 14 | a: { 15 | 'broken-link': { 16 | link: t.fixture('symlink', 'this-does-not-exist'), 17 | }, 18 | }, 19 | }), 20 | ) 21 | 22 | const link = `${dir}/a/broken-link/link` 23 | 24 | const patterns = [ 25 | `${dir}/a/broken-link/*`, 26 | `${dir}/a/broken-link/**`, 27 | `${dir}/a/broken-link/**/link`, 28 | `${dir}/a/broken-link/**/*`, 29 | `${dir}/a/broken-link/link`, 30 | `${dir}/a/broken-link/{link,asdf}`, 31 | `${dir}/a/broken-link/+(link|asdf)`, 32 | `${dir}/a/broken-link/!(asdf)`, 33 | ] 34 | 35 | const opts: (GlobOptionsWithFileTypesUnset | undefined)[] = [ 36 | undefined, 37 | { mark: true }, 38 | { follow: true }, 39 | ] 40 | 41 | t.test('async test', t => { 42 | t.plan(patterns.length) 43 | for (const pattern of patterns) { 44 | t.test(pattern, async t => { 45 | t.plan(opts.length) 46 | for (const opt of opts) { 47 | const res = await glob(pattern, opt) 48 | const msg = pattern + ' ' + JSON.stringify(opt) 49 | t.not(res.indexOf(link), -1, msg) 50 | } 51 | }) 52 | } 53 | }) 54 | 55 | t.test('sync test', t => { 56 | t.plan(patterns.length) 57 | for (const pattern of patterns) { 58 | t.test(pattern, t => { 59 | t.plan(opts.length) 60 | for (const opt of opts) { 61 | const res = glob.globSync(pattern, opt) 62 | t.not(res.indexOf(link), -1, 'opt=' + JSON.stringify(opt)) 63 | } 64 | }) 65 | } 66 | }) 67 | -------------------------------------------------------------------------------- /test/custom-fs.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { globSync } from '../dist/esm/index.js' 3 | 4 | // just a rudimentary test, since PathScurry tests it more anyway 5 | import { readdirSync } from 'fs' 6 | let readdirCalled = 0 7 | const myReaddirSync = (path: string, options: { withFileTypes: true }) => { 8 | readdirCalled++ 9 | return readdirSync(path, options) 10 | } 11 | 12 | const cwd = t.testdir({ 13 | a: '', 14 | b: '', 15 | c: {}, 16 | }) 17 | 18 | t.same( 19 | new Set(['a', 'b', 'c', '.']), 20 | new Set( 21 | globSync('**', { 22 | fs: { 23 | readdirSync: myReaddirSync, 24 | }, 25 | cwd, 26 | }), 27 | ), 28 | ) 29 | 30 | t.equal(readdirCalled, 2) 31 | -------------------------------------------------------------------------------- /test/custom-ignore.ts: -------------------------------------------------------------------------------- 1 | import { basename } from 'path' 2 | import { Path } from 'path-scurry' 3 | import t from 'tap' 4 | import { fileURLToPath } from 'url' 5 | import { glob, globSync, IgnoreLike } from '../dist/esm/index.js' 6 | 7 | const cwd = fileURLToPath(new URL('./fixtures', import.meta.url)) 8 | 9 | const j = (a: string[]) => 10 | a 11 | .map(s => s.replace(/\\/g, '/')) 12 | .sort((a, b) => a.localeCompare(b, 'en')) 13 | 14 | t.test('ignore files with long names', async t => { 15 | const ignore: IgnoreLike = { 16 | ignored: (p: Path) => p.name.length > 1, 17 | } 18 | const syncRes = globSync('**', { cwd, ignore }) 19 | const asyncRes = await glob('**', { cwd, ignore }) 20 | const expect = j( 21 | globSync('**', { cwd }).filter(p => { 22 | return basename(p).length === 1 && basename(p) !== '.' 23 | }), 24 | ) 25 | t.same(j(syncRes), expect) 26 | t.same(j(asyncRes), expect) 27 | for (const r of syncRes) { 28 | if (basename(r).length > 1) t.fail(r) 29 | } 30 | }) 31 | 32 | t.test('ignore symlink and abcdef directories', async t => { 33 | const ignore: IgnoreLike = { 34 | childrenIgnored: (p: Path) => { 35 | return p.isNamed('symlink') || p.isNamed('abcdef') 36 | }, 37 | } 38 | const syncRes = globSync('**', { cwd, ignore, nodir: true }) 39 | const asyncRes = await glob('**', { cwd, ignore, nodir: true }) 40 | const expect = j( 41 | globSync('**', { nodir: true, cwd }).filter(p => { 42 | return !/\bsymlink\b|\babcdef\b/.test(p) 43 | }), 44 | ) 45 | t.same(j(syncRes), expect) 46 | t.same(j(asyncRes), expect) 47 | for (const r of syncRes) { 48 | if (r === 'symlink' || r === 'basename') t.fail(r) 49 | } 50 | }) 51 | -------------------------------------------------------------------------------- /test/cwd-noent.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { fileURLToPath } from 'url' 3 | import { Glob } from '../dist/esm/index.js' 4 | const cwd = fileURLToPath( 5 | new URL('./fixtures/does-not-exist', import.meta.url), 6 | ) 7 | 8 | t.test('walk', async t => { 9 | const g = new Glob('**', { cwd }) 10 | t.same(await g.walk(), []) 11 | }) 12 | 13 | t.test('walkSync', t => { 14 | const g = new Glob('**', { cwd }) 15 | t.same(g.walkSync(), []) 16 | t.end() 17 | }) 18 | 19 | t.test('stream', async t => { 20 | const g = new Glob('**', { cwd }) 21 | const s = g.stream() 22 | s.on('data', () => t.fail('should not get entries')) 23 | t.same(await s.collect(), []) 24 | }) 25 | 26 | t.test('streamSync', t => { 27 | const g = new Glob('**', { cwd }) 28 | const s = g.streamSync() 29 | const c: string[] = [] 30 | s.on('data', p => { 31 | t.fail('should not get entries') 32 | c.push(p) 33 | }) 34 | s.on('end', () => { 35 | t.same(c, []) 36 | t.end() 37 | }) 38 | }) 39 | 40 | t.test('iterate', async t => { 41 | const g = new Glob('**', { cwd }) 42 | const s = g.iterate() 43 | const c: string[] = [] 44 | for await (const p of s) { 45 | c.push(p) 46 | t.fail('should not get entries') 47 | } 48 | t.same(c, []) 49 | }) 50 | 51 | t.test('iterateSync', async t => { 52 | const g = new Glob('**', { cwd }) 53 | const s = g.iterateSync() 54 | const c: string[] = [] 55 | for (const p of s) { 56 | c.push(p) 57 | t.fail('should not get entries') 58 | } 59 | t.same(c, []) 60 | t.end() 61 | }) 62 | 63 | t.test('for await', async t => { 64 | const g = new Glob('**', { cwd }) 65 | const c: string[] = [] 66 | for await (const p of g) { 67 | c.push(p) 68 | t.fail('should not get entries') 69 | } 70 | t.same(c, []) 71 | }) 72 | 73 | t.test('iterateSync', async t => { 74 | const g = new Glob('**', { cwd }) 75 | const c: string[] = [] 76 | for (const p of g) { 77 | c.push(p) 78 | t.fail('should not get entries') 79 | } 80 | t.same(c, []) 81 | t.end() 82 | }) 83 | -------------------------------------------------------------------------------- /test/cwd-test.ts: -------------------------------------------------------------------------------- 1 | import { resolve, sep } from 'path' 2 | import t from 'tap' 3 | import { fileURLToPath } from 'url' 4 | import { glob } from '../dist/esm/index.js' 5 | const j = (a: string[]) => a.map(s => s.split('/').join(sep)) 6 | 7 | const origCwd = process.cwd() 8 | process.chdir(fileURLToPath(new URL('./fixtures', import.meta.url))) 9 | t.teardown(() => process.chdir(origCwd)) 10 | 11 | t.test('changing cwd and searching for **/d', t => { 12 | const expect = Object.entries({ 13 | a: new Set(j(['c/d', 'b/c/d'])), 14 | 'a/b': new Set(j(['c/d'])), 15 | '': new Set(j(['a/b/c/d', 'a/c/d'])), 16 | }) 17 | t.plan(expect.length) 18 | for (const [cwd, matches] of expect) { 19 | t.test(cwd || '(empty string)', async t => { 20 | t.same(new Set(await glob('**/d', { cwd })), matches) 21 | if (cwd) { 22 | t.same(new Set(await glob('**/d', { cwd: cwd + '/' })), matches) 23 | t.same(new Set(await glob('**/d', { cwd: cwd + '/.' })), matches) 24 | t.same(new Set(await glob('**/d', { cwd: cwd + '/./' })), matches) 25 | } else { 26 | t.same(new Set(await glob('**/d', { cwd: '.' })), matches) 27 | t.same(new Set(await glob('**/d', { cwd: './' })), matches) 28 | } 29 | t.same(new Set(await glob('**/d', { cwd: resolve(cwd) })), matches) 30 | t.same( 31 | new Set(await glob('**/d', { cwd: resolve(cwd) + '/' })), 32 | matches, 33 | ) 34 | t.same( 35 | new Set(await glob('**/d', { cwd: resolve(cwd) + '/.' })), 36 | matches, 37 | ) 38 | t.same( 39 | new Set(await glob('**/d', { cwd: resolve(cwd) + '/./' })), 40 | matches, 41 | ) 42 | }) 43 | } 44 | }) 45 | -------------------------------------------------------------------------------- /test/dot-relative.ts: -------------------------------------------------------------------------------- 1 | import { resolve, sep } from 'path' 2 | import t from 'tap' 3 | import { fileURLToPath } from 'url' 4 | import { Glob } from '../dist/esm/index.js' 5 | import { bashResults } from './bash-results.js' 6 | 7 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 8 | const pattern = 'a/b/**' 9 | process.chdir(fileURLToPath(new URL('./fixtures', import.meta.url))) 10 | 11 | const marks = [true, false] 12 | for (const mark of marks) { 13 | t.test('mark=' + mark, t => { 14 | t.plan(3) 15 | 16 | t.test('Emits relative matches prefixed with ./', async t => { 17 | const g = new Glob(pattern, { dotRelative: true }) 18 | const results = await g.walk() 19 | 20 | t.equal( 21 | results.length, 22 | bashResults[pattern]?.length, 23 | 'must match all files', 24 | ) 25 | for (const m of results) { 26 | t.ok(m.startsWith('.' + sep)) 27 | } 28 | }) 29 | 30 | t.test('returns ./ prefixed matches synchronously', async t => { 31 | const g = new Glob(pattern, { dotRelative: true }) 32 | const results = g.walkSync() 33 | 34 | t.equal( 35 | results.length, 36 | bashResults[pattern]?.length, 37 | 'must match all files', 38 | ) 39 | for (const m of results) { 40 | t.ok(m.startsWith('.' + sep)) 41 | } 42 | }) 43 | 44 | t.test( 45 | 'does not prefix with ./ unless dotRelative is true', 46 | async t => { 47 | const g = new Glob(pattern, {}) 48 | const results = await g.walk() 49 | 50 | t.equal( 51 | results.length, 52 | bashResults[pattern]?.length, 53 | 'must match all files', 54 | ) 55 | for (const m of results) { 56 | t.ok((mark && m === '.' + sep) || !m.startsWith('.' + sep)) 57 | } 58 | }, 59 | ) 60 | }) 61 | } 62 | 63 | t.test('does not add ./ for patterns starting in ../', async t => { 64 | t.plan(2) 65 | const pattern = '../a/b/**' 66 | const cwd = resolve(__dirname, 'fixtures/a') 67 | t.test('async', async t => { 68 | const g = new Glob(pattern, { dotRelative: true, cwd }) 69 | for await (const m of g) { 70 | t.ok(!m.startsWith('.' + sep + '..' + sep)) 71 | } 72 | }) 73 | t.test('sync', async t => { 74 | const g = new Glob(pattern, { dotRelative: true, cwd }) 75 | for (const m of g) { 76 | t.ok(!m.startsWith('.' + sep + '..' + sep)) 77 | } 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /test/empty-set.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { glob } from '../dist/esm/index.js' 3 | 4 | // Patterns that cannot match anything 5 | const patterns = [ 6 | '# comment', 7 | ' ', 8 | '\n', 9 | 'just doesnt happen to match anything so this is a control', 10 | ] 11 | 12 | t.plan(patterns.length) 13 | for (const p of patterns) { 14 | t.test(JSON.stringify(p), async t => { 15 | const f = await glob(p) 16 | t.same(f, [], 'no returned values') 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /test/escape.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { unescape, escape, hasMagic } from '../dist/esm/index.js' 3 | import { bashResults } from './bash-results.js' 4 | 5 | for (const pattern of Object.keys(bashResults)) { 6 | t.notOk(hasMagic(escape(pattern)), `escape(${pattern})`) 7 | const pp = escape(pattern) 8 | const pw = escape(pattern, { 9 | windowsPathsNoEscape: true, 10 | }) 11 | t.notOk( 12 | hasMagic(pp, { platform: 'linux' }), 13 | 'no magic after posix escape', 14 | ) 15 | t.notOk( 16 | hasMagic(pw, { platform: 'win32', windowsPathsNoEscape: true }), 17 | 'no magic after windows escape', 18 | ) 19 | const up = unescape(pp) 20 | const uw = unescape(pw, { windowsPathsNoEscape: true }) 21 | t.equal(up, pattern, 'unescaped posix pattern returned') 22 | t.equal(uw, pattern, 'unescaped windows pattern returned') 23 | } 24 | -------------------------------------------------------------------------------- /test/follow.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { fileURLToPath } from 'url' 3 | import { glob } from '../dist/esm/index.js' 4 | 5 | if (process.platform === 'win32') { 6 | t.plan(0, 'skip on windows') 7 | process.exit(0) 8 | } 9 | 10 | process.chdir(fileURLToPath(new URL('./fixtures', import.meta.url))) 11 | 12 | t.test('follow symlinks', async t => { 13 | const pattern = 'a/symlink/**' 14 | const syncNoFollow = glob.globSync(pattern) 15 | const syncFollow = glob.globSync(pattern, { follow: true }) 16 | const [noFollow, follow] = await Promise.all([ 17 | glob(pattern), 18 | glob(pattern, { follow: true }), 19 | ]) 20 | t.same( 21 | new Set(follow), 22 | new Set(syncFollow), 23 | 'sync and async follow should match', 24 | ) 25 | t.same( 26 | new Set(noFollow), 27 | new Set(syncNoFollow), 28 | 'sync and async noFollow should match', 29 | ) 30 | var long = 'a/symlink/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c' 31 | t.ok(follow.includes(long), 'follow should have long entry') 32 | t.ok(syncFollow.includes(long), 'syncFollow should have long entry') 33 | t.end() 34 | }) 35 | 36 | t.test('follow + nodir means no symlinks to dirs in results', async t => { 37 | const pattern = 'dir_baz/**' 38 | const cwd = t.testdir({ 39 | dir_baz: { 40 | bar: t.fixture('symlink', '../dir_bar'), 41 | }, 42 | dir_bar: { 43 | foo: t.fixture('symlink', '../dir_foo'), 44 | }, 45 | dir_foo: { 46 | 'foo.txt': 'hello', 47 | }, 48 | }) 49 | const follow = true 50 | const nodir = true 51 | const posix = true 52 | const syncResult = glob 53 | .globSync(pattern, { follow, nodir, cwd, posix }) 54 | .sort((a, b) => a.localeCompare(b)) 55 | const asyncResult = ( 56 | await glob(pattern, { follow, nodir, cwd, posix }) 57 | ).sort((a, b) => a.localeCompare(b)) 58 | t.strictSame(syncResult, asyncResult) 59 | t.strictSame(syncResult, ['dir_baz/bar/foo/foo.txt']) 60 | }) 61 | -------------------------------------------------------------------------------- /test/has-magic.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { fileURLToPath } from 'url' 3 | import { glob } from '../dist/esm/index.js' 4 | 5 | process.chdir(fileURLToPath(new URL('.', import.meta.url))) 6 | 7 | t.test('non-string pattern is evil magic', async t => { 8 | const patterns = [0, null, 12, { x: 1 }, undefined, /x/, NaN] 9 | patterns.forEach(function (p) { 10 | t.throws(function () { 11 | // @ts-expect-error 12 | glob.hasMagic(p) 13 | }) 14 | }) 15 | }) 16 | 17 | t.test('detect magic in glob patterns', async t => { 18 | t.notOk(glob.hasMagic(''), "no magic in ''") 19 | t.notOk(glob.hasMagic('a/b/c/'), 'no magic a/b/c/') 20 | t.ok(glob.hasMagic('a/b/**/'), 'magic in a/b/**/') 21 | t.ok(glob.hasMagic('a/b/?/'), 'magic in a/b/?/') 22 | t.ok(glob.hasMagic('a/b/+(x|y)'), 'magic in a/b/+(x|y)') 23 | t.notOk( 24 | glob.hasMagic('a/b/+(x|y)', { noext: true }), 25 | 'no magic in a/b/+(x|y) noext', 26 | ) 27 | t.notOk(glob.hasMagic('{a,b}'), 'no magic in {a,b}') 28 | t.ok( 29 | glob.hasMagic('{a,b}', { magicalBraces: true }), 30 | 'magical braces are magic in {a,b}', 31 | ) 32 | t.notOk( 33 | glob.hasMagic('{a,b}', { nobrace: true }), 34 | 'no magic in {a,b} nobrace:true', 35 | ) 36 | t.notOk( 37 | glob.hasMagic('{a,b}', { nobrace: true, magicalBraces: true }), 38 | 'magical braces not magic in {a,b} nobrace:true', 39 | ) 40 | }) 41 | -------------------------------------------------------------------------------- /test/ignore.ts: -------------------------------------------------------------------------------- 1 | // Ignore option test 2 | // Show that glob ignores results matching pattern on ignore option 3 | 4 | import { sep } from 'path' 5 | import t from 'tap' 6 | import { fileURLToPath } from 'url' 7 | import type { GlobOptions } from '../dist/esm/index.js' 8 | import { glob } from '../dist/esm/index.js' 9 | 10 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 11 | const alphasort = (a: string, b: string) => a.localeCompare(b, 'en') 12 | const j = (a: string[]) => 13 | a.map(s => s.split('/').join(sep)).sort(alphasort) 14 | 15 | process.chdir(fileURLToPath(new URL('./fixtures', import.meta.url))) 16 | 17 | // [pattern, ignore, expect, opt (object) or cwd (string)] 18 | type Case = [ 19 | pattern: string, 20 | ignore: null | string | string[], 21 | expect: string[], 22 | optOrCwd?: GlobOptions | string | undefined, 23 | ] 24 | 25 | const cases: Case[] = [ 26 | [ 27 | '*', 28 | null, 29 | j(['abcdef', 'abcfed', 'b', 'bc', 'c', 'cb', 'symlink', 'x', 'z']), 30 | 'a', 31 | ], 32 | [ 33 | '*', 34 | ['b'], 35 | j(['abcdef', 'abcfed', 'bc', 'c', 'cb', 'symlink', 'x', 'z']), 36 | 'a', 37 | ], 38 | [ 39 | '*', 40 | 'b*', 41 | j(['abcdef', 'abcfed', 'c', 'cb', 'symlink', 'x', 'z']), 42 | 'a', 43 | ], 44 | ['b/**', 'b/c/d', j(['b', 'b/c']), 'a'], 45 | ['b/**', 'd', j(['b', 'b/c', 'b/c/d']), 'a'], 46 | ['b/**', 'b/c/**', ['b'], 'a'], 47 | ['b/**', (process.cwd() + '/a/b/c/**').split(sep).join('/'), ['b'], 'a'], 48 | ['**/d', 'b/c/d', j(['c/d']), 'a'], 49 | [ 50 | 'a/**/[gh]', 51 | ['a/abcfed/g/h'], 52 | j(['a/abcdef/g', 'a/abcdef/g/h', 'a/abcfed/g']), 53 | ], 54 | [ 55 | '*', 56 | ['c', 'bc', 'symlink', 'abcdef'], 57 | ['abcfed', 'b', 'cb', 'x', 'z'], 58 | 'a', 59 | ], 60 | [ 61 | '**', 62 | ['c/**', 'bc/**', 'symlink/**', 'abcdef/**'], 63 | j([ 64 | '.', 65 | 'abcfed', 66 | 'abcfed/g', 67 | 'abcfed/g/h', 68 | 'b', 69 | 'b/c', 70 | 'b/c/d', 71 | 'cb', 72 | 'cb/e', 73 | 'cb/e/f', 74 | 'x', 75 | 'z', 76 | ]), 77 | 'a', 78 | ], 79 | ['a/**', ['a/**'], []], 80 | ['a/**', ['a/**/**'], []], 81 | ['a/b/**', ['a/b'], j(['a/b/c', 'a/b/c/d'])], 82 | [ 83 | '**', 84 | ['b'], 85 | j([ 86 | '.', 87 | 'abcdef', 88 | 'abcdef/g', 89 | 'abcdef/g/h', 90 | 'abcfed', 91 | 'abcfed/g', 92 | 'abcfed/g/h', 93 | 'b/c', 94 | 'b/c/d', 95 | 'bc', 96 | 'bc/e', 97 | 'bc/e/f', 98 | 'c', 99 | 'c/d', 100 | 'c/d/c', 101 | 'c/d/c/b', 102 | 'cb', 103 | 'cb/e', 104 | 'cb/e/f', 105 | 'symlink', 106 | 'symlink/a', 107 | 'symlink/a/b', 108 | 'symlink/a/b/c', 109 | 'x', 110 | 'z', 111 | ]), 112 | 'a', 113 | ], 114 | [ 115 | '**', 116 | ['b', 'c'], 117 | j([ 118 | '.', 119 | 'abcdef', 120 | 'abcdef/g', 121 | 'abcdef/g/h', 122 | 'abcfed', 123 | 'abcfed/g', 124 | 'abcfed/g/h', 125 | 'b/c', 126 | 'b/c/d', 127 | 'bc', 128 | 'bc/e', 129 | 'bc/e/f', 130 | 'c/d', 131 | 'c/d/c', 132 | 'c/d/c/b', 133 | 'cb', 134 | 'cb/e', 135 | 'cb/e/f', 136 | 'symlink', 137 | 'symlink/a', 138 | 'symlink/a/b', 139 | 'symlink/a/b/c', 140 | 'x', 141 | 'z', 142 | ]), 143 | 'a', 144 | ], 145 | [ 146 | '**', 147 | ['b**'], 148 | j([ 149 | '.', 150 | 'abcdef', 151 | 'abcdef/g', 152 | 'abcdef/g/h', 153 | 'abcfed', 154 | 'abcfed/g', 155 | 'abcfed/g/h', 156 | 'b/c', 157 | 'b/c/d', 158 | 'bc/e', 159 | 'bc/e/f', 160 | 'c', 161 | 'c/d', 162 | 'c/d/c', 163 | 'c/d/c/b', 164 | 'cb', 165 | 'cb/e', 166 | 'cb/e/f', 167 | 'symlink', 168 | 'symlink/a', 169 | 'symlink/a/b', 170 | 'symlink/a/b/c', 171 | 'x', 172 | 'z', 173 | ]), 174 | 'a', 175 | ], 176 | [ 177 | '**', 178 | ['b/**'], 179 | j([ 180 | '.', 181 | 'abcdef', 182 | 'abcdef/g', 183 | 'abcdef/g/h', 184 | 'abcfed', 185 | 'abcfed/g', 186 | 'abcfed/g/h', 187 | 'bc', 188 | 'bc/e', 189 | 'bc/e/f', 190 | 'c', 191 | 'c/d', 192 | 'c/d/c', 193 | 'c/d/c/b', 194 | 'cb', 195 | 'cb/e', 196 | 'cb/e/f', 197 | 'symlink', 198 | 'symlink/a', 199 | 'symlink/a/b', 200 | 'symlink/a/b/c', 201 | 'x', 202 | 'z', 203 | ]), 204 | 'a', 205 | ], 206 | [ 207 | '**', 208 | ['b**/**'], 209 | j([ 210 | '.', 211 | 'abcdef', 212 | 'abcdef/g', 213 | 'abcdef/g/h', 214 | 'abcfed', 215 | 'abcfed/g', 216 | 'abcfed/g/h', 217 | 'c', 218 | 'c/d', 219 | 'c/d/c', 220 | 'c/d/c/b', 221 | 'cb', 222 | 'cb/e', 223 | 'cb/e/f', 224 | 'symlink', 225 | 'symlink/a', 226 | 'symlink/a/b', 227 | 'symlink/a/b/c', 228 | 'x', 229 | 'z', 230 | ]), 231 | 'a', 232 | ], 233 | [ 234 | '**', 235 | ['ab**ef/**'], 236 | j([ 237 | '.', 238 | 'abcfed', 239 | 'abcfed/g', 240 | 'abcfed/g/h', 241 | 'b', 242 | 'b/c', 243 | 'b/c/d', 244 | 'bc', 245 | 'bc/e', 246 | 'bc/e/f', 247 | 'c', 248 | 'c/d', 249 | 'c/d/c', 250 | 'c/d/c/b', 251 | 'cb', 252 | 'cb/e', 253 | 'cb/e/f', 254 | 'symlink', 255 | 'symlink/a', 256 | 'symlink/a/b', 257 | 'symlink/a/b/c', 258 | 'x', 259 | 'z', 260 | ]), 261 | 'a', 262 | ], 263 | [ 264 | '**', 265 | ['abc{def,fed}/**'], 266 | j([ 267 | '.', 268 | 'b', 269 | 'b/c', 270 | 'b/c/d', 271 | 'bc', 272 | 'bc/e', 273 | 'bc/e/f', 274 | 'c', 275 | 'c/d', 276 | 'c/d/c', 277 | 'c/d/c/b', 278 | 'cb', 279 | 'cb/e', 280 | 'cb/e/f', 281 | 'symlink', 282 | 'symlink/a', 283 | 'symlink/a/b', 284 | 'symlink/a/b/c', 285 | 'x', 286 | 'z', 287 | ]), 288 | 'a', 289 | ], 290 | [ 291 | '**', 292 | ['abc{def,fed}/*'], 293 | j([ 294 | '.', 295 | 'abcdef', 296 | 'abcdef/g/h', 297 | 'abcfed', 298 | 'abcfed/g/h', 299 | 'b', 300 | 'b/c', 301 | 'b/c/d', 302 | 'bc', 303 | 'bc/e', 304 | 'bc/e/f', 305 | 'c', 306 | 'c/d', 307 | 'c/d/c', 308 | 'c/d/c/b', 309 | 'cb', 310 | 'cb/e', 311 | 'cb/e/f', 312 | 'symlink', 313 | 'symlink/a', 314 | 'symlink/a/b', 315 | 'symlink/a/b/c', 316 | 'x', 317 | 'z', 318 | ]), 319 | 'a', 320 | ], 321 | ['c/**', ['c/*'], j(['c', 'c/d/c', 'c/d/c/b']), 'a'], 322 | ['a/c/**', ['a/c/*'], j(['a/c', 'a/c/d/c', 'a/c/d/c/b'])], 323 | ['a/c/**', ['a/c/**', 'a/c/*', 'a/c/*/c'], []], 324 | ['a/**/.y', ['a/x/**'], j(['a/z/.y'])], 325 | ['a/**/.y', ['a/x/**'], j(['a/z/.y']), { dot: true }], 326 | ['a/**/b', ['a/x/**'], j(['a/b', 'a/c/d/c/b', 'a/symlink/a/b'])], 327 | [ 328 | 'a/**/b', 329 | ['a/x/**'], 330 | j(['a/b', 'a/c/d/c/b', 'a/symlink/a/b', 'a/z/.y/b']), 331 | { dot: true }, 332 | ], 333 | ['*/.abcdef', 'a/**', []], 334 | ['a/*/.y/b', 'a/x/**', j(['a/z/.y/b'])], 335 | [ 336 | 'a/*/.y/b', 337 | (process.cwd() + '/a/x/**').split(sep).join('/'), 338 | j(['a/z/.y/b']), 339 | ], 340 | [ 341 | './*', 342 | '{./,c}b', 343 | j(['abcdef', 'abcfed', 'bc', 'c', 'symlink', 'x', 'z']), 344 | 'a', 345 | ], 346 | [ 347 | './*', 348 | './c/../b', 349 | j(['abcdef', 'abcfed', 'bc', 'c', 'cb', 'symlink', 'x', 'z']), 350 | 'a', 351 | ], 352 | ] 353 | 354 | for (const c of cases) { 355 | const [pattern, ignore, ex, optCwd] = c 356 | const expect = ( 357 | process.platform === 'win32' ? 358 | ex.filter(e => !/\bsymlink\b/.test(e)) 359 | : ex).sort() 360 | expect.sort() 361 | const opt: GlobOptions = 362 | (typeof optCwd === 'string' ? { cwd: optCwd } : optCwd) || {} 363 | const name = `p=${pattern} i=${JSON.stringify(ignore)} ${JSON.stringify( 364 | opt, 365 | )}` 366 | 367 | if (ignore) { 368 | opt.ignore = ignore 369 | } 370 | 371 | t.test(name, async t => { 372 | const res = await glob(pattern, opt) 373 | t.same(res.sort(), expect, 'async') 374 | const resSync = glob.globSync(pattern, opt) 375 | t.same(resSync.sort(), expect, 'sync') 376 | }) 377 | } 378 | 379 | t.test('race condition', async t => { 380 | process.chdir(__dirname) 381 | var pattern = 'fixtures/*' 382 | t.jobs = 64 383 | for (const dot of [true, false]) { 384 | for (const ignore of ['fixtures/**', undefined]) { 385 | for (const cwd of [undefined, process.cwd(), '.']) { 386 | const opt: GlobOptions = { 387 | dot, 388 | ignore, 389 | } 390 | if (cwd) opt.cwd = cwd 391 | const expect = ignore ? [] : j(['fixtures/a']) 392 | t.test(JSON.stringify(opt), async t => { 393 | t.plan(2) 394 | t.same(glob.globSync(pattern, opt).sort(), expect) 395 | t.same((await glob(pattern, opt)).sort(), expect) 396 | }) 397 | } 398 | } 399 | } 400 | }) 401 | -------------------------------------------------------------------------------- /test/include-child-matches.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { 3 | glob, 4 | GlobOptionsWithFileTypesUnset, 5 | globSync, 6 | } from '../src/index.js' 7 | 8 | t.test('no include child matches', async t => { 9 | const cwd = t.testdir({ a: { b: { c: { d: { e: { f: '' } } } } } }) 10 | const pattern = 'a/**/[cde]/**' 11 | const o: GlobOptionsWithFileTypesUnset = { 12 | cwd, 13 | posix: true, 14 | includeChildMatches: false, 15 | } 16 | const a = await glob(pattern, o) 17 | const s = globSync(pattern, o) 18 | t.strictSame(a, ['a/b/c']) 19 | t.strictSame(s, ['a/b/c']) 20 | }) 21 | 22 | t.test('test the caveat', async t => { 23 | const cwd = t.testdir({ a: { b: { c: { d: { e: { f: '' } } } } } }) 24 | const pattern = ['a/b/c/d/e/f', 'a/[bdf]/?/[a-z]/*'] 25 | const o: GlobOptionsWithFileTypesUnset = { 26 | cwd, 27 | posix: true, 28 | includeChildMatches: false, 29 | } 30 | const a = await glob(pattern, o) 31 | const s = globSync(pattern, o) 32 | t.strictSame(a, ['a/b/c/d/e/f', 'a/b/c/d/e']) 33 | t.strictSame(s, ['a/b/c/d/e/f', 'a/b/c/d/e']) 34 | }) 35 | 36 | t.test('ignore impl must have an add() method', t => { 37 | t.throws(() => 38 | globSync('', { 39 | ignore: { 40 | ignored: () => true, 41 | childrenIgnored: () => true, 42 | }, 43 | includeChildMatches: false, 44 | }), 45 | ) 46 | t.end() 47 | }) 48 | -------------------------------------------------------------------------------- /test/mark.ts: -------------------------------------------------------------------------------- 1 | import { sep } from 'path' 2 | import t from 'tap' 3 | import { fileURLToPath } from 'url' 4 | import { glob } from '../dist/esm/index.js' 5 | 6 | process.chdir(fileURLToPath(new URL('./fixtures', import.meta.url))) 7 | 8 | const alphasort = (a: string, b: string) => a.localeCompare(b, 'en') 9 | const j = (a: string[]) => 10 | a.map(s => s.split('/').join(sep)).sort(alphasort) 11 | 12 | t.test('mark with cwd', async t => { 13 | const pattern = '*/*' 14 | const opt = { mark: true, cwd: 'a' } 15 | const expect = [ 16 | 'abcdef/g/', 17 | 'abcfed/g/', 18 | 'b/c/', 19 | 'bc/e/', 20 | 'c/d/', 21 | 'cb/e/', 22 | ] 23 | 24 | const res = await glob(pattern, opt) 25 | if (process.platform !== 'win32') { 26 | expect.push('symlink/a/') 27 | } 28 | 29 | t.same(res.sort(alphasort), j(expect)) 30 | t.same(glob.globSync(pattern, opt).sort(alphasort), j(expect)) 31 | }) 32 | 33 | t.test('mark, with **', async t => { 34 | const pattern = 'a/*b*/**' 35 | const opt = { mark: true } 36 | const expect = [ 37 | 'a/abcdef/', 38 | 'a/abcdef/g/', 39 | 'a/abcdef/g/h', 40 | 'a/abcfed/', 41 | 'a/abcfed/g/', 42 | 'a/abcfed/g/h', 43 | 'a/b/', 44 | 'a/b/c/', 45 | 'a/b/c/d', 46 | 'a/bc/', 47 | 'a/bc/e/', 48 | 'a/bc/e/f', 49 | 'a/cb/', 50 | 'a/cb/e/', 51 | 'a/cb/e/f', 52 | ].sort(alphasort) 53 | 54 | t.same((await glob(pattern, opt)).sort(alphasort), j(expect), 'async') 55 | t.same(glob.globSync(pattern, opt).sort(alphasort), j(expect), 'sync') 56 | }) 57 | 58 | t.test('mark, no / on pattern', async t => { 59 | const pattern = 'a/*' 60 | const opt = { mark: true } 61 | const expect = [ 62 | 'a/abcdef/', 63 | 'a/abcfed/', 64 | 'a/b/', 65 | 'a/bc/', 66 | 'a/c/', 67 | 'a/cb/', 68 | 'a/x/', 69 | 'a/z/', 70 | ] 71 | if (process.platform !== 'win32') { 72 | expect.push('a/symlink/') 73 | } 74 | const results = (await glob(pattern, opt)).sort(alphasort) 75 | t.same(results, j(expect)) 76 | t.same(glob.globSync(pattern, opt).sort(alphasort), j(expect)) 77 | }) 78 | 79 | t.test('mark=false, no / on pattern', async t => { 80 | const pattern = 'a/*' 81 | const expect = [ 82 | 'a/abcdef', 83 | 'a/abcfed', 84 | 'a/b', 85 | 'a/bc', 86 | 'a/c', 87 | 'a/cb', 88 | 'a/x', 89 | 'a/z', 90 | ] 91 | if (process.platform !== 'win32') { 92 | expect.push('a/symlink') 93 | } 94 | const results = (await glob(pattern)).sort(alphasort) 95 | 96 | t.same(results, j(expect)) 97 | t.same(glob.globSync(pattern).sort(alphasort), j(expect)) 98 | }) 99 | 100 | t.test('mark=true, / on pattern', async t => { 101 | const pattern = 'a/*/' 102 | const opt = { mark: true } 103 | const expect = [ 104 | 'a/abcdef/', 105 | 'a/abcfed/', 106 | 'a/b/', 107 | 'a/bc/', 108 | 'a/c/', 109 | 'a/cb/', 110 | 'a/x/', 111 | 'a/z/', 112 | ] 113 | 114 | if (process.platform !== 'win32') { 115 | expect.push('a/symlink/') 116 | } 117 | const results = (await glob(pattern, opt)).sort(alphasort) 118 | t.same(results, j(expect)) 119 | t.same(glob.globSync(pattern, opt).sort(alphasort), j(expect)) 120 | }) 121 | 122 | t.test('mark=false, / on pattern', async t => { 123 | const pattern = 'a/*/' 124 | const expect = [ 125 | 'a/abcdef', 126 | 'a/abcfed', 127 | 'a/b', 128 | 'a/bc', 129 | 'a/c', 130 | 'a/cb', 131 | 'a/x', 132 | 'a/z', 133 | ] 134 | if (process.platform !== 'win32') { 135 | expect.push('a/symlink') 136 | } 137 | 138 | const results = (await glob(pattern)).sort(alphasort) 139 | t.same(results, j(expect)) 140 | t.same(glob.globSync(pattern).sort(alphasort), j(expect)) 141 | }) 142 | 143 | const cwd = process 144 | .cwd() 145 | .replace(/[\/\\]+$/, '') 146 | .replace(/\\/g, '/') 147 | for (const mark of [true, false]) { 148 | for (const slash of [true, false]) { 149 | t.test('cwd mark:' + mark + ' slash:' + slash, async t => { 150 | const pattern = cwd + (slash ? '/' : '') 151 | const results = await glob(pattern, { mark }) 152 | t.equal(results.length, 1) 153 | const res = results[0]?.replace(/\\/g, '/') 154 | const syncResults = glob.globSync(pattern, { mark: mark }) 155 | const syncRes = syncResults[0]?.replace(/\\/g, '/') 156 | if (mark) { 157 | t.equal(res, cwd + '/') 158 | } else { 159 | t.equal(res?.indexOf(cwd), 0) 160 | } 161 | t.equal(syncRes, res, 'sync should match async') 162 | }) 163 | } 164 | } 165 | 166 | for (const mark of [true, false]) { 167 | for (const slash of [true, false]) { 168 | t.test('. mark:' + mark + ' slash:' + slash, async t => { 169 | const pattern = '.' + (slash ? '/' : '') 170 | const results = await glob(pattern, { mark }) 171 | t.equal(results.length, 1) 172 | const res = results[0]?.replace(/\\/g, '/') 173 | const syncResults = glob.globSync(pattern, { mark: mark }) 174 | const syncRes = syncResults[0]?.replace(/\\/g, '/') 175 | if (mark) { 176 | t.equal(res, './') 177 | } else { 178 | t.equal(res, '.') 179 | } 180 | t.equal(syncRes, res, 'sync should match async') 181 | }) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /test/match-base.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { glob } from '../dist/esm/index.js' 3 | import { sep } from 'path' 4 | import { fileURLToPath } from 'url' 5 | 6 | const alphasort = (a: string, b: string) => a.localeCompare(b, 'en') 7 | const j = (a: string[]) => 8 | a.map(s => s.split('/').join(sep)).sort(alphasort) 9 | 10 | const fixtureDir = fileURLToPath(new URL('./fixtures', import.meta.url)) 11 | 12 | const pattern = 'a*' 13 | const expect = ['a', 'a/abcdef', 'a/abcfed'] 14 | 15 | if (process.platform !== 'win32') { 16 | expect.push('a/symlink/a', 'a/symlink/a/b/c/a') 17 | } 18 | 19 | t.test('chdir', async t => { 20 | const origCwd = process.cwd() 21 | process.chdir(fixtureDir) 22 | t.teardown(() => process.chdir(origCwd)) 23 | t.same( 24 | glob.globSync(pattern, { matchBase: true }).sort(alphasort), 25 | j(expect), 26 | ) 27 | t.same( 28 | (await glob(pattern, { matchBase: true })).sort(alphasort), 29 | j(expect), 30 | ) 31 | }) 32 | 33 | t.test('cwd', async t => { 34 | t.same( 35 | glob 36 | .globSync(pattern, { matchBase: true, cwd: fixtureDir }) 37 | .sort(alphasort), 38 | j(expect), 39 | ) 40 | t.same( 41 | (await glob(pattern, { matchBase: true, cwd: fixtureDir })).sort( 42 | alphasort, 43 | ), 44 | j(expect), 45 | ) 46 | }) 47 | 48 | t.test('noglobstar', async t => { 49 | t.rejects(glob(pattern, { matchBase: true, noglobstar: true })) 50 | t.throws(() => 51 | glob.globSync(pattern, { matchBase: true, noglobstar: true }), 52 | ) 53 | t.end() 54 | }) 55 | 56 | t.test('pattern includes /', async t => { 57 | const pattern = 'a/b*' 58 | const expect = ['a/b', 'a/bc'] 59 | t.same( 60 | glob 61 | .globSync(pattern, { matchBase: true, cwd: fixtureDir }) 62 | .sort(alphasort), 63 | j(expect), 64 | ) 65 | t.same( 66 | (await glob(pattern, { matchBase: true, cwd: fixtureDir })).sort( 67 | alphasort, 68 | ), 69 | j(expect), 70 | ) 71 | }) 72 | 73 | t.test('one brace section of pattern includes /', async t => { 74 | const pattern = 'a{*,/b*}' 75 | const exp = ['a', 'a/b', 'a/bc'] 76 | t.same( 77 | glob 78 | .globSync(pattern, { matchBase: true, cwd: fixtureDir }) 79 | .sort(alphasort), 80 | j(exp), 81 | ) 82 | t.same( 83 | (await glob(pattern, { matchBase: true, cwd: fixtureDir })).sort( 84 | alphasort, 85 | ), 86 | j(exp), 87 | ) 88 | }) 89 | 90 | t.test('one array member of pattern includes /', async t => { 91 | const pattern = ['a*', 'a/b*'] 92 | const exp = expect.concat(['a/b', 'a/bc']).sort() 93 | t.same( 94 | glob 95 | .globSync(pattern, { matchBase: true, cwd: fixtureDir }) 96 | .sort(alphasort), 97 | j(exp), 98 | ) 99 | t.same( 100 | (await glob(pattern, { matchBase: true, cwd: fixtureDir })).sort( 101 | alphasort, 102 | ), 103 | j(exp), 104 | ) 105 | }) 106 | -------------------------------------------------------------------------------- /test/match-parent.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { PathScurry } from 'path-scurry' 3 | import { Glob } from '../dist/esm/index.js' 4 | 5 | const scurry = new PathScurry() 6 | t.test('/', t => { 7 | const g = new Glob('/', { withFileTypes: true, scurry }) 8 | const m = g.walkSync() 9 | t.equal(m.length, 1) 10 | t.equal(m[0], scurry.cwd.resolve('/')) 11 | t.end() 12 | }) 13 | t.test('/..', t => { 14 | const g = new Glob('/..', { withFileTypes: true, scurry }) 15 | const m = g.walkSync() 16 | t.equal(m.length, 1) 17 | t.equal(m[0], scurry.cwd.resolve('/')) 18 | t.end() 19 | }) 20 | t.test('/../../../../../', t => { 21 | const g = new Glob('/../../../../../', { withFileTypes: true, scurry }) 22 | const m = g.walkSync() 23 | t.equal(m.length, 1) 24 | t.equal(m[0], scurry.cwd.resolve('/')) 25 | t.end() 26 | }) 27 | -------------------------------------------------------------------------------- /test/match-root.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { PathScurry } from 'path-scurry' 3 | import { Glob } from '../dist/esm/index.js' 4 | 5 | const scurry = new PathScurry() 6 | const g = new Glob('/', { withFileTypes: true, scurry }) 7 | const m = g.walkSync() 8 | t.equal(m.length, 1) 9 | t.equal(m[0], scurry.cwd.resolve('/')) 10 | -------------------------------------------------------------------------------- /test/max-depth.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { PathScurry } from 'path-scurry' 3 | import t from 'tap' 4 | import { fileURLToPath } from 'url' 5 | import { 6 | Glob, 7 | glob, 8 | globStream, 9 | globStreamSync, 10 | globSync, 11 | } from '../dist/esm/index.js' 12 | 13 | const j = (a: string[]) => 14 | a 15 | .map(s => s.replace(/\\/g, '/')) 16 | .sort((a, b) => a.localeCompare(b, 'en')) 17 | t.test('set maxDepth', async t => { 18 | const maxDepth = 2 19 | const cwd = resolve( 20 | fileURLToPath(new URL('./fixtures', import.meta.url)), 21 | ) 22 | const startDepth = new PathScurry(cwd).cwd.depth() 23 | const pattern = '{*/*/*/**,*/*/**,**}' 24 | const asyncRes = await glob(pattern, { 25 | cwd, 26 | maxDepth, 27 | follow: true, 28 | withFileTypes: true, 29 | }) 30 | const syncRes = globSync(pattern, { 31 | cwd, 32 | maxDepth, 33 | follow: true, 34 | withFileTypes: true, 35 | }) 36 | const noMaxDepth = globSync(pattern, { 37 | cwd, 38 | follow: true, 39 | withFileTypes: true, 40 | }) 41 | const expect = j( 42 | noMaxDepth 43 | .filter(p => p.depth() <= startDepth + maxDepth) 44 | .map(p => p.relative() || '.'), 45 | ) 46 | 47 | const ssync = j(syncRes.map(p => p.relative() || '.')) 48 | const sasync = j(asyncRes.map(p => p.relative() || '.')) 49 | t.same(ssync, expect, 'got all results sync') 50 | t.same(sasync, expect, 'got all results async') 51 | for (const p of syncRes) { 52 | t.ok(p.depth() <= startDepth + maxDepth, 'does not exceed maxDepth', { 53 | max: startDepth + maxDepth, 54 | actual: p.depth(), 55 | file: p.relative(), 56 | results: 'sync', 57 | }) 58 | } 59 | for (const p of asyncRes) { 60 | t.ok(p.depth() <= startDepth + maxDepth, 'does not exceed maxDepth', { 61 | max: startDepth + maxDepth, 62 | actual: p.depth(), 63 | file: p.relative(), 64 | results: 'async', 65 | }) 66 | } 67 | 68 | t.same( 69 | j( 70 | await globStream(pattern, { cwd, maxDepth, follow: true }).collect(), 71 | ), 72 | expect, 73 | 'maxDepth with stream', 74 | ) 75 | t.same( 76 | j( 77 | await globStreamSync(pattern, { 78 | cwd, 79 | maxDepth, 80 | follow: true, 81 | }).collect(), 82 | ), 83 | expect, 84 | 'maxDepth with streamSync', 85 | ) 86 | 87 | t.same( 88 | await glob(pattern, { cwd, maxDepth: -1, follow: true }), 89 | [], 90 | 'async maxDepth -1', 91 | ) 92 | t.same( 93 | globSync(pattern, { cwd, maxDepth: -1, follow: true }), 94 | [], 95 | 'sync maxDepth -1', 96 | ) 97 | 98 | t.same( 99 | await glob(pattern, { cwd, maxDepth: 0, follow: true }), 100 | ['.'], 101 | 'async maxDepth 0', 102 | ) 103 | t.same( 104 | globSync(pattern, { cwd, maxDepth: 0, follow: true }), 105 | ['.'], 106 | 'async maxDepth 0', 107 | ) 108 | 109 | const g = new Glob(pattern, { cwd, follow: true, maxDepth }) 110 | t.same(j([...g]), expect, 'maxDepth with iteration') 111 | const ai = new Glob(pattern, g) 112 | const aires: string[] = [] 113 | for await (const res of ai) { 114 | aires.push(res) 115 | } 116 | t.same(j(aires), expect, 'maxDepth with async iteration') 117 | }) 118 | -------------------------------------------------------------------------------- /test/memfs.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | 3 | if (process.platform === 'win32') { 4 | t.plan(0, 'this test does not work on windows') 5 | process.exit(0) 6 | } 7 | 8 | import { fs as memfs, vol } from 'memfs' 9 | import { glob } from '../dist/esm/index.js' 10 | 11 | t.beforeEach(() => vol.fromJSON({ '/x': 'abc' })) 12 | 13 | const fs = memfs as unknown as typeof import('fs') 14 | 15 | const mock = { 16 | fs: memfs, 17 | 'fs/promises': memfs.promises, 18 | } 19 | 20 | const patterns = ['/**/*', '/*', '/x'] 21 | const cwds = ['/', undefined] 22 | for (const pattern of patterns) { 23 | t.test(pattern, async t => { 24 | for (const cwd of cwds) { 25 | t.test(`cwd=${cwd}`, async t => { 26 | t.test('mocking the fs', async t => { 27 | const { glob } = (await t.mockImport( 28 | '../dist/esm/index.js', 29 | mock, 30 | )) as typeof import('../dist/esm/index.js') 31 | t.strictSame(await glob(pattern, { nodir: true, cwd }), ['/x']) 32 | }) 33 | t.test('passing in fs argument', async t => { 34 | t.strictSame(await glob(pattern, { nodir: true, cwd, fs }), [ 35 | '/x', 36 | ]) 37 | }) 38 | }) 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /test/nocase-magic-only.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { Glob } from '../dist/esm/index.js' 3 | 4 | const darwin = new Glob('x', { nocase: true, platform: 'darwin' }) 5 | const linux = new Glob('x', { nocase: true, platform: 'linux' }) 6 | 7 | t.type(darwin.patterns[0]?.pattern(), 'string') 8 | t.type(linux.patterns[0]?.pattern(), RegExp) 9 | -------------------------------------------------------------------------------- /test/nodir.ts: -------------------------------------------------------------------------------- 1 | import { resolve, sep } from 'path' 2 | import t from 'tap' 3 | import { fileURLToPath } from 'url' 4 | import type { GlobOptions } from '../dist/esm/index.js' 5 | import { glob } from '../dist/esm/index.js' 6 | 7 | process.chdir(fileURLToPath(new URL('./fixtures', import.meta.url))) 8 | 9 | const alphasort = (a: string, b: string) => a.localeCompare(b, 'en') 10 | const j = (a: string[]) => 11 | a.map(s => s.split('/').join(sep)).sort(alphasort) 12 | 13 | // [pattern, options, expect] 14 | const root = resolve('a') 15 | const cases: [string, GlobOptions, string[]][] = [ 16 | [ 17 | '*/**', 18 | { cwd: 'a' }, 19 | j([ 20 | 'abcdef/g/h', 21 | 'abcfed/g/h', 22 | 'b/c/d', 23 | 'bc/e/f', 24 | 'c/d/c/b', 25 | 'cb/e/f', 26 | 'symlink/a/b/c', 27 | ]), 28 | ], 29 | [ 30 | 'a/*b*/**', 31 | {}, 32 | j(['a/abcdef/g/h', 'a/abcfed/g/h', 'a/b/c/d', 'a/bc/e/f', 'a/cb/e/f']), 33 | ], 34 | ['a/*b*/**/', {}, []], 35 | ['*/*', { cwd: 'a' }, []], 36 | ['*/*', { cwd: root }, []], 37 | ] 38 | 39 | for (const [pattern, options, expectRaw] of cases) { 40 | options.nodir = true 41 | const expect = 42 | process.platform === 'win32' ? 43 | expectRaw.filter(e => !/\bsymlink\b/.test(e)) 44 | : expectRaw 45 | expect.sort() 46 | if (process.platform !== 'win32') { 47 | } 48 | t.test(pattern + ' ' + JSON.stringify(options), async t => { 49 | t.same(glob.globSync(pattern, options).sort(), expect, 'sync results') 50 | t.same((await glob(pattern, options)).sort(), expect, 'async results') 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /test/pattern.ts: -------------------------------------------------------------------------------- 1 | import { GLOBSTAR } from 'minimatch' 2 | import t from 'tap' 3 | import { MMPattern, Pattern } from '../dist/esm/pattern.js' 4 | import { Glob } from '../dist/esm/index.js' 5 | 6 | t.same( 7 | new Glob( 8 | [ 9 | '//host/share///x/*', 10 | '//host/share/', 11 | '//host/share', 12 | '//?/z:/x/*', 13 | '//?/z:/', 14 | '//?/z:', 15 | 'c:/x/*', 16 | 'c:/', 17 | ], 18 | { platform: 'win32' }, 19 | ).patterns.map(p => [p.globString(), p.root()]), 20 | [ 21 | ['//host/share/x/*', '//host/share/'], 22 | ['//host/share/', '//host/share/'], 23 | ['//host/share/', '//host/share/'], 24 | ['//?/z:/x/*', '//?/z:/'], 25 | ['//?/z:/', '//?/z:/'], 26 | ['//?/z:/', '//?/z:/'], 27 | ['c:/x/*', 'c:/'], 28 | ['c:/', 'c:/'], 29 | ], 30 | ) 31 | t.throws(() => { 32 | new Pattern([], ['x'], 0, process.platform) 33 | }) 34 | 35 | t.throws(() => { 36 | new Pattern(['x'], [], 0, process.platform) 37 | }) 38 | 39 | t.throws(() => { 40 | new Pattern(['x'], ['x'], 2, process.platform) 41 | }) 42 | 43 | t.throws(() => { 44 | new Pattern(['x'], ['x'], -1, process.platform) 45 | }) 46 | 47 | t.throws(() => { 48 | new Pattern(['x', 'x'], ['x', 'x', 'x'], 0, process.platform) 49 | }) 50 | 51 | const s = new Pattern(['x'], ['x'], 0, process.platform) 52 | const g = new Pattern( 53 | [GLOBSTAR as unknown as MMPattern], 54 | ['**'], 55 | 0, 56 | process.platform, 57 | ) 58 | const r = new Pattern([/./], ['?'], 0, process.platform) 59 | t.equal(s.isString(), true) 60 | t.equal(g.isString(), false) 61 | t.equal(r.isString(), false) 62 | 63 | t.equal(s.isGlobstar(), false) 64 | t.equal(g.isGlobstar(), true) 65 | t.equal(r.isGlobstar(), false) 66 | 67 | t.equal(s.isRegExp(), false) 68 | t.equal(g.isRegExp(), false) 69 | t.equal(r.isRegExp(), true) 70 | -------------------------------------------------------------------------------- /test/platform.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import t from 'tap' 3 | 4 | import { 5 | PathScurry, 6 | PathScurryDarwin, 7 | PathScurryPosix, 8 | PathScurryWin32, 9 | } from 'path-scurry' 10 | import { fileURLToPath } from 'url' 11 | import { Glob } from '../dist/esm/index.js' 12 | import { Pattern } from '../dist/esm/pattern.js' 13 | import { GlobWalker } from '../dist/esm/walker.js' 14 | 15 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 16 | 17 | t.test('default platform is process.platform', t => { 18 | const g = new Glob('.', {}) 19 | t.equal(g.platform, process.platform) 20 | t.end() 21 | }) 22 | 23 | t.test('default linux when not found', async t => { 24 | const prop = Object.getOwnPropertyDescriptor(process, 'platform') 25 | if (!prop) throw new Error('no platform?') 26 | t.teardown(() => { 27 | Object.defineProperty(process, 'platform', prop) 28 | }) 29 | Object.defineProperty(process, 'platform', { 30 | value: null, 31 | configurable: true, 32 | }) 33 | const { Glob } = (await t.mockImport( 34 | '../dist/esm/index.js', 35 | {}, 36 | )) as typeof import('../dist/esm/index.js') 37 | const g = new Glob('.', {}) 38 | t.equal(g.platform, 'linux') 39 | t.end() 40 | }) 41 | 42 | t.test('set platform, get appropriate scurry object', t => { 43 | t.equal( 44 | new Glob('.', { platform: 'darwin' }).scurry.constructor, 45 | PathScurryDarwin, 46 | ) 47 | t.equal( 48 | new Glob('.', { platform: 'linux' }).scurry.constructor, 49 | PathScurryPosix, 50 | ) 51 | t.equal( 52 | new Glob('.', { platform: 'win32' }).scurry.constructor, 53 | PathScurryWin32, 54 | ) 55 | t.equal(new Glob('.', {}).scurry.constructor, PathScurry) 56 | t.end() 57 | }) 58 | 59 | t.test('set scurry, sets nocase and scurry', t => { 60 | const scurry = new PathScurryWin32('.') 61 | t.throws(() => new Glob('.', { scurry, nocase: false })) 62 | const g = new Glob('.', { scurry }) 63 | t.equal(g.scurry, scurry) 64 | t.equal(g.nocase, true) 65 | t.end() 66 | }) 67 | 68 | t.test('instantiate to hit a coverage line', async t => { 69 | const s = new PathScurry(resolve(__dirname, 'fixtures/a/b')) 70 | const p = new Pattern([/./, /./], ['?', '?'], 0, process.platform) 71 | new GlobWalker([p], s.cwd, { 72 | platform: 'win32', 73 | }) 74 | new GlobWalker([p], s.cwd, { 75 | platform: 'linux', 76 | }) 77 | t.pass('this is fine') 78 | }) 79 | -------------------------------------------------------------------------------- /test/progra-tilde.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/isaacs/node-glob/issues/547 2 | import t from 'tap' 3 | 4 | import { globSync } from '../dist/esm/index.js' 5 | 6 | if (process.platform !== 'win32') { 7 | t.pass('no need to test this except on windows') 8 | process.exit(0) 9 | } 10 | 11 | const dir = t.testdir({ 12 | 'program files': { 13 | a: '', 14 | b: '', 15 | c: '', 16 | }, 17 | }) 18 | 19 | t.strictSame( 20 | globSync('progra~1\\*', { cwd: dir, windowsPathsNoEscape: true }).sort( 21 | (a, b) => a.localeCompare(b, 'en'), 22 | ), 23 | ['progra~1\\a', 'progra~1\\b', 'progra~1\\c'], 24 | ) 25 | -------------------------------------------------------------------------------- /test/readme-issue.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { glob } from '../dist/esm/index.js' 3 | 4 | const dir = t.testdir({ 5 | 'package.json': '{}', 6 | README: 'x', 7 | }) 8 | 9 | t.test('glob', async t => { 10 | var opt = { 11 | cwd: dir, 12 | nocase: true, 13 | mark: true, 14 | } 15 | 16 | t.same(await glob('README?(.*)', opt), ['README']) 17 | }) 18 | -------------------------------------------------------------------------------- /test/realpath.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as fsp from 'fs/promises' 3 | import { resolve } from 'path' 4 | import t from 'tap' 5 | import { glob } from '../dist/esm/index.js' 6 | import { GlobOptionsWithFileTypesUnset } from '../dist/esm/glob.js' 7 | import { fileURLToPath } from 'url' 8 | 9 | const alphasort = (a: string, b: string) => a.localeCompare(b, 'en') 10 | 11 | // pattern to find a bunch of duplicates 12 | const pattern = 'a/symlink/{*,**/*/*/*,*/*/**,*/*/*/*/*/*}' 13 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 14 | const fixtureDir = resolve(__dirname, 'fixtures') 15 | const origCwd = process.cwd() 16 | process.chdir(fixtureDir) 17 | 18 | if (process.platform === 'win32') { 19 | t.plan(0, 'skip on windows') 20 | } else { 21 | // options, results 22 | // realpath:true set on each option 23 | 24 | type Case = [ 25 | options: GlobOptionsWithFileTypesUnset, 26 | results: string[], 27 | pattern?: string, 28 | ] 29 | const cases: Case[] = [ 30 | [{}, ['a/symlink', 'a/symlink/a', 'a/symlink/a/b']], 31 | 32 | [{ mark: true }, ['a/symlink/', 'a/symlink/a/', 'a/symlink/a/b/']], 33 | 34 | [{ follow: true }, ['a/symlink', 'a/symlink/a', 'a/symlink/a/b']], 35 | 36 | [ 37 | { cwd: 'a' }, 38 | ['symlink', 'symlink/a', 'symlink/a/b'], 39 | pattern.substring(2), 40 | ], 41 | 42 | [{ cwd: 'a' }, [], 'no one here but us chickens'], 43 | 44 | [ 45 | { mark: true, follow: true }, 46 | [ 47 | // this one actually just has HELLA entries, don't list them all here 48 | // plus it differs based on the platform. follow:true is kinda cray. 49 | 'a/symlink/', 50 | 'a/symlink/a/', 51 | 'a/symlink/a/b/', 52 | ], 53 | ], 54 | ] 55 | 56 | for (const [opt, expect, p = pattern] of cases) { 57 | expect.sort(alphasort) 58 | t.test(p + ' ' + JSON.stringify(opt), async t => { 59 | opt.realpath = true 60 | t.same(glob.globSync(p, opt).sort(alphasort), expect, 'sync') 61 | const a = await glob(p, opt) 62 | t.same(a.sort(alphasort), expect, 'async') 63 | }) 64 | } 65 | 66 | t.test('realpath failure', async t => { 67 | // failing realpath means that it does not include the result 68 | process.chdir(origCwd) 69 | const { glob } = (await t.mockImport('../dist/esm/index.js', { 70 | fs: { 71 | ...fs, 72 | realpathSync: Object.assign(fs.realpathSync, { 73 | native: () => { 74 | throw new Error('no error for you sync') 75 | }, 76 | }), 77 | }, 78 | 'fs/promises': { 79 | ...fsp, 80 | realpath: async () => { 81 | throw new Error('no error for you async') 82 | }, 83 | }, 84 | })) as typeof import('../dist/esm/index.js') 85 | const pattern = 'a/symlink/a/b/c/a/b/**' 86 | t.test('setting cwd explicitly', async t => { 87 | const opt = { realpath: true, cwd: fixtureDir } 88 | t.same(glob.globSync(pattern, opt), []) 89 | t.same(await glob(pattern, opt), []) 90 | }) 91 | t.test('looking in cwd', async t => { 92 | process.chdir(fixtureDir) 93 | const opt = { realpath: true } 94 | t.same(glob.globSync(pattern, opt), []) 95 | t.same(await glob(pattern, opt), []) 96 | }) 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /test/root.ts: -------------------------------------------------------------------------------- 1 | import { resolve, sep } from 'path' 2 | import t from 'tap' 3 | import { Glob } from '../dist/esm/index.js' 4 | 5 | const alphasort = (a: string, b: string) => a.localeCompare(b, 'en') 6 | const j = (a: string[]) => 7 | a 8 | .map(s => s.split(process.cwd()).join('{CWD}').split(sep).join('/')) 9 | .sort(alphasort) 10 | 11 | t.test('set root option', t => { 12 | const cwd = t.testdir({ 13 | x: { 14 | a: '', 15 | x: { 16 | a: '', 17 | x: { 18 | a: '', 19 | y: { 20 | r: '', 21 | }, 22 | }, 23 | y: { 24 | r: '', 25 | }, 26 | }, 27 | y: { 28 | r: '', 29 | }, 30 | }, 31 | y: { 32 | r: '', 33 | }, 34 | }) 35 | 36 | const pattern = ['**/r', '/**/a', '/**/../y'] 37 | const root = resolve(cwd, 'x/x') 38 | t.plan(3) 39 | for (const absolute of [true, false, undefined]) { 40 | t.test(`absolute=${absolute}`, async t => { 41 | const g = new Glob(pattern, { root, absolute, cwd }) 42 | t.matchSnapshot(j(await g.walk()), 'async') 43 | t.matchSnapshot(j(g.walkSync()), 'sync') 44 | }) 45 | } 46 | }) 47 | -------------------------------------------------------------------------------- /test/signal.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import { resolve } from 'path' 3 | import t from 'tap' 4 | import { fileURLToPath } from 'url' 5 | import { 6 | glob, 7 | globStream, 8 | globStreamSync, 9 | globSync, 10 | } from '../dist/esm/index.js' 11 | 12 | const mocks = (ac: AbortController) => ({ 13 | fs: { 14 | ...fs, 15 | readdirSync: (path: string, options: any) => { 16 | ac.abort(yeet) 17 | return fs.readdirSync(path, options) 18 | }, 19 | }, 20 | }) 21 | 22 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 23 | const cwd = resolve(__dirname, 'fixtures/a') 24 | 25 | const yeet = new Error('yeet') 26 | 27 | t.test('pre abort walk', async t => { 28 | const ac = new AbortController() 29 | ac.abort(yeet) 30 | await t.rejects(glob('./**', { cwd, signal: ac.signal }), yeet) 31 | }) 32 | 33 | t.test('mid-abort walk', async t => { 34 | const ac = new AbortController() 35 | const res = glob('./**', { cwd, signal: ac.signal }) 36 | ac.abort(yeet) 37 | await t.rejects(res, yeet) 38 | }) 39 | 40 | t.test('pre abort sync walk', t => { 41 | const ac = new AbortController() 42 | ac.abort(yeet) 43 | t.throws(() => globSync('./**', { cwd, signal: ac.signal })) 44 | t.end() 45 | }) 46 | 47 | t.test('mid-abort sync walk', async t => { 48 | const ac = new AbortController() 49 | const { globSync } = await t.mockImport( 50 | '../dist/esm/index.js', 51 | mocks(ac), 52 | ) 53 | t.throws(() => globSync('./**', { cwd, signal: ac.signal })) 54 | }) 55 | 56 | t.test('pre abort stream', t => { 57 | const ac = new AbortController() 58 | ac.abort(yeet) 59 | const s = globStream('./**', { cwd, signal: ac.signal }) 60 | s.on('error', er => { 61 | t.equal(er, yeet) 62 | t.end() 63 | }) 64 | }) 65 | 66 | t.test('mid-abort stream', t => { 67 | const ac = new AbortController() 68 | const s = globStream('./**', { cwd, signal: ac.signal }) 69 | s.on('error', er => { 70 | t.equal(er, yeet) 71 | t.end() 72 | }) 73 | s.once('data', () => ac.abort(yeet)) 74 | }) 75 | 76 | t.test('pre abort sync stream', t => { 77 | const ac = new AbortController() 78 | ac.abort(yeet) 79 | const s = globStreamSync('./**', { cwd, signal: ac.signal }) 80 | s.on('error', er => { 81 | t.equal(er, yeet) 82 | t.end() 83 | }) 84 | }) 85 | 86 | t.test('mid-abort sync stream', t => { 87 | const ac = new AbortController() 88 | const s = globStreamSync('./**', { cwd, signal: ac.signal }) 89 | s.on('error', er => { 90 | t.equal(er, yeet) 91 | t.end() 92 | }) 93 | s.on('data', () => ac.abort(yeet)) 94 | }) 95 | -------------------------------------------------------------------------------- /test/slash-cwd.ts: -------------------------------------------------------------------------------- 1 | // regression test to make sure that slash-ended patterns 2 | // don't match files when using a different cwd. 3 | import t from 'tap' 4 | import { fileURLToPath } from 'url' 5 | import type { GlobOptions } from '../dist/esm/index.js' 6 | import { glob } from '../dist/esm/index.js' 7 | 8 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 9 | const pattern = '../{*.md,test}/' 10 | const expect = ['.'] 11 | const cwd = __dirname 12 | const opt: GlobOptions = { cwd } 13 | process.chdir(__dirname + '/..') 14 | 15 | t.test('slashes only match directories', async t => { 16 | t.same(glob.globSync(pattern, opt), expect, 'sync test') 17 | t.same(await glob(pattern, opt), expect, 'async test') 18 | }) 19 | -------------------------------------------------------------------------------- /test/stat.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import t from 'tap' 3 | import { fileURLToPath } from 'url' 4 | import { glob, globSync } from '../dist/esm/index.js' 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 7 | 8 | t.test('stat: true', async t => { 9 | const cwd = resolve(__dirname, 'fixtures') 10 | const pattern = '*' 11 | const asyncRes = await glob(pattern, { 12 | cwd, 13 | withFileTypes: true, 14 | stat: true, 15 | }) 16 | const syncRes = globSync(pattern, { 17 | cwd, 18 | withFileTypes: true, 19 | stat: true, 20 | }) 21 | t.type(asyncRes[0]?.mode, 'number') 22 | t.type(syncRes[0]?.mode, 'number') 23 | 24 | const noStat = await glob(pattern, { cwd, withFileTypes: true }) 25 | t.equal(noStat[0]?.mode, undefined) 26 | }) 27 | -------------------------------------------------------------------------------- /test/stream.ts: -------------------------------------------------------------------------------- 1 | import { resolve, sep } from 'path' 2 | import t from 'tap' 3 | import { fileURLToPath } from 'url' 4 | import { 5 | Glob, 6 | globIterate, 7 | globIterateSync, 8 | globStream, 9 | globStreamSync, 10 | } from '../dist/esm/index.js' 11 | import { glob, globSync } from '../dist/esm/index.js' 12 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 13 | const cwd = resolve(__dirname, 'fixtures/a') 14 | const j = (a: string[]) => a.map(a => a.split('/').join(sep)) 15 | const expect = j([ 16 | '.', 17 | 'z', 18 | 'x', 19 | 'cb', 20 | 'c', 21 | 'bc', 22 | 'b', 23 | 'abcfed', 24 | 'abcdef', 25 | 'cb/e', 26 | 'cb/e/f', 27 | 'c/d', 28 | 'c/d/c', 29 | 'c/d/c/b', 30 | 'bc/e', 31 | 'bc/e/f', 32 | 'b/c', 33 | 'b/c/d', 34 | 'abcfed/g', 35 | 'abcfed/g/h', 36 | 'abcdef/g', 37 | 'abcdef/g/h', 38 | ...(process.platform !== 'win32' ? 39 | ['symlink', 'symlink/a', 'symlink/a/b', 'symlink/a/b/c'] 40 | : []), 41 | ]) 42 | 43 | t.test('stream', t => { 44 | let sync: boolean = true 45 | const s = new Glob('./**', { cwd }) 46 | const stream = s.stream() 47 | const e = new Set(expect) 48 | stream.on('data', c => { 49 | t.equal(e.has(c), true, JSON.stringify(c)) 50 | e.delete(c) 51 | }) 52 | stream.on('end', () => { 53 | t.equal(e.size, 0, 'saw all entries') 54 | t.equal(sync, false, 'did not finish in one tick') 55 | const d = new Glob('./**', s) 56 | const dream = d.stream() 57 | const f = new Set(expect) 58 | dream.on('data', c => { 59 | t.equal(f.has(c), true, JSON.stringify(c)) 60 | f.delete(c) 61 | }) 62 | dream.on('end', () => { 63 | t.equal(f.size, 0, 'saw all entries') 64 | t.end() 65 | }) 66 | }) 67 | sync = false 68 | }) 69 | 70 | t.test('streamSync', t => { 71 | let sync: boolean = true 72 | const s = new Glob('./**', { cwd }) 73 | const stream = s.streamSync() 74 | const e = new Set(expect) 75 | stream.on('data', c => { 76 | t.equal(e.has(c), true, JSON.stringify(c)) 77 | e.delete(c) 78 | }) 79 | stream.on('end', () => { 80 | t.equal(e.size, 0, 'saw all entries') 81 | const d = new Glob('./**', s) 82 | const dream = d.streamSync() 83 | const f = new Set(expect) 84 | dream.on('data', c => { 85 | t.equal(f.has(c), true, JSON.stringify(c)) 86 | f.delete(c) 87 | }) 88 | dream.on('end', () => { 89 | t.equal(f.size, 0, 'saw all entries') 90 | t.equal(sync, true, 'finished synchronously') 91 | t.end() 92 | }) 93 | }) 94 | sync = false 95 | }) 96 | 97 | t.test('iterate', async t => { 98 | const s = new Glob('./**', { cwd }) 99 | const e = new Set(expect) 100 | for await (const c of s.iterate()) { 101 | t.equal(e.has(c), true, JSON.stringify(c)) 102 | e.delete(c) 103 | } 104 | t.equal(e.size, 0, 'saw all entries') 105 | 106 | const f = new Set(expect) 107 | const d = new Glob('./**', s) 108 | for await (const c of d.iterate()) { 109 | t.equal(f.has(c), true, JSON.stringify(c)) 110 | f.delete(c) 111 | } 112 | t.equal(f.size, 0, 'saw all entries') 113 | }) 114 | 115 | t.test('iterateSync', t => { 116 | const s = new Glob('./**', { cwd }) 117 | const e = new Set(expect) 118 | for (const c of s.iterateSync()) { 119 | t.equal(e.has(c), true, JSON.stringify(c)) 120 | e.delete(c) 121 | } 122 | t.equal(e.size, 0, 'saw all entries') 123 | 124 | const f = new Set(expect) 125 | const d = new Glob('./**', s) 126 | for (const c of d.iterateSync()) { 127 | t.equal(f.has(c), true, JSON.stringify(c)) 128 | f.delete(c) 129 | } 130 | t.equal(f.size, 0, 'saw all entries') 131 | t.end() 132 | }) 133 | 134 | t.test('walk', async t => { 135 | const s = new Glob('./**', { cwd }) 136 | const e = new Set(expect) 137 | const actual = new Set(await s.walk()) 138 | t.same(actual, e) 139 | const d = new Glob('./**', s) 140 | const dactual = new Set(await d.walk()) 141 | t.same(dactual, e) 142 | }) 143 | 144 | t.test('walkSync', t => { 145 | const s = new Glob('./**', { cwd }) 146 | const e = new Set(expect) 147 | const actual = new Set(s.walkSync()) 148 | t.same(actual, e) 149 | const d = new Glob('./**', s) 150 | const dactual = new Set(d.walkSync()) 151 | t.same(dactual, e) 152 | t.end() 153 | }) 154 | 155 | t.test('for await', async t => { 156 | const s = new Glob('./**', { cwd }) 157 | const e = new Set(expect) 158 | for await (const c of s) { 159 | t.equal(e.has(c), true, JSON.stringify(c)) 160 | e.delete(c) 161 | } 162 | t.equal(e.size, 0, 'saw all entries') 163 | 164 | const f = new Set(expect) 165 | const d = new Glob('./**', s) 166 | for await (const c of d) { 167 | t.equal(f.has(c), true, JSON.stringify(c)) 168 | f.delete(c) 169 | } 170 | t.equal(f.size, 0, 'saw all entries') 171 | }) 172 | 173 | t.test('for of', t => { 174 | const s = new Glob('./**', { cwd }) 175 | const e = new Set(expect) 176 | for (const c of s) { 177 | t.equal(e.has(c), true, JSON.stringify(c)) 178 | e.delete(c) 179 | } 180 | t.equal(e.size, 0, 'saw all entries') 181 | 182 | const f = new Set(expect) 183 | const d = new Glob('./**', s) 184 | for (const c of d) { 185 | t.equal(f.has(c), true, JSON.stringify(c)) 186 | f.delete(c) 187 | } 188 | t.equal(f.size, 0, 'saw all entries') 189 | t.end() 190 | }) 191 | 192 | t.test('iterate on main', async t => { 193 | const s = globIterate('./**', { cwd }) 194 | const e = new Set(expect) 195 | for await (const c of s) { 196 | t.equal(e.has(c), true, JSON.stringify(c)) 197 | e.delete(c) 198 | } 199 | t.equal(e.size, 0, 'saw all entries') 200 | }) 201 | 202 | t.test('iterateSync on main', t => { 203 | const s = globIterateSync('./**', { cwd }) 204 | const e = new Set(expect) 205 | for (const c of s) { 206 | t.equal(e.has(c), true, JSON.stringify(c)) 207 | e.delete(c) 208 | } 209 | t.equal(e.size, 0, 'saw all entries') 210 | t.end() 211 | }) 212 | 213 | t.test('stream on main', t => { 214 | let sync: boolean = true 215 | const stream = globStream('./**', { cwd }) 216 | const e = new Set(expect) 217 | stream.on('data', c => { 218 | t.equal(e.has(c), true, JSON.stringify(c)) 219 | e.delete(c) 220 | }) 221 | stream.on('end', () => { 222 | t.equal(e.size, 0, 'saw all entries') 223 | t.equal(sync, false, 'did not finish in one tick') 224 | t.end() 225 | }) 226 | sync = false 227 | }) 228 | 229 | t.test('streamSync on main', t => { 230 | let sync: boolean = true 231 | const stream = globStreamSync('./**', { cwd }) 232 | const e = new Set(expect) 233 | stream.on('data', c => { 234 | t.equal(e.has(c), true, JSON.stringify(c)) 235 | e.delete(c) 236 | }) 237 | stream.on('end', () => { 238 | t.equal(e.size, 0, 'saw all entries') 239 | t.equal(sync, true, 'finished synchronously') 240 | t.end() 241 | }) 242 | sync = false 243 | }) 244 | 245 | t.test('walk on main', async t => { 246 | const s = glob('./**', { cwd }) 247 | const e = new Set(expect) 248 | const actual = new Set(await s) 249 | t.same(actual, e) 250 | }) 251 | 252 | t.test('walkSync', t => { 253 | const s = globSync('./**', { cwd }) 254 | const e = new Set(expect) 255 | const actual = new Set(s) 256 | t.same(actual, e) 257 | t.end() 258 | }) 259 | -------------------------------------------------------------------------------- /test/url-cwd.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { pathToFileURL } from 'url' 3 | import { Glob } from '../dist/esm/index.js' 4 | 5 | t.test('can use file url as cwd option', t => { 6 | const fileURL = pathToFileURL(process.cwd()) 7 | const fileURLString = String(fileURL) 8 | const ps = new Glob('.', { cwd: process.cwd() }) 9 | const pu = new Glob('.', { cwd: fileURL }) 10 | const pus = new Glob('.', { cwd: fileURLString }) 11 | t.equal(ps.cwd, process.cwd()) 12 | t.equal(pu.cwd, process.cwd()) 13 | t.equal(pus.cwd, process.cwd()) 14 | t.end() 15 | }) 16 | -------------------------------------------------------------------------------- /test/windows-paths-fs.ts: -------------------------------------------------------------------------------- 1 | // test that escape chars are handled properly according to configs 2 | // when found in patterns and paths containing glob magic. 3 | 4 | import t from 'tap' 5 | import { glob } from '../dist/esm/index.js' 6 | 7 | const dir = t.testdir({ 8 | // treat escapes as path separators 9 | a: { 10 | '[x': { 11 | ']b': { 12 | y: '', 13 | }, 14 | }, 15 | }, 16 | // escape parent dir name only, not filename 17 | 'a[x]b': { 18 | y: '', 19 | }, 20 | // no path separators, all escaped 21 | 'a[x]by': '', 22 | }) 23 | 24 | t.test('treat backslash as escape', t => { 25 | const cases = Object.entries({ 26 | 'a[x]b/y': [], 27 | 'a\\[x\\]b/y': ['a[x]b/y'], 28 | 'a\\[x\\]b\\y': ['a[x]by'], 29 | }) 30 | t.plan(cases.length) 31 | for (const [pattern, expect] of cases) { 32 | t.test(pattern, async t => { 33 | t.strictSame( 34 | glob.globSync(pattern, { cwd: dir, posix: true }), 35 | expect, 36 | 'sync', 37 | ) 38 | t.strictSame( 39 | (await glob(pattern, { cwd: dir })).map(s => 40 | s.replace(/\\/g, '/'), 41 | ), 42 | expect, 43 | 'async', 44 | ) 45 | }) 46 | } 47 | }) 48 | 49 | t.test('treat backslash as separator', t => { 50 | Object.defineProperty(process, 'platform', { 51 | value: 'win32', 52 | }) 53 | const cases = Object.entries({ 54 | 'a[x]b/y': [], 55 | 'a\\[x\\]b/y': ['a/[x/]b/y'], 56 | 'a\\[x\\]b\\y': ['a/[x/]b/y'], 57 | }) 58 | t.plan(cases.length) 59 | for (const [pattern, expect] of cases) { 60 | t.test(pattern, async t => { 61 | t.strictSame( 62 | glob 63 | .globSync(pattern, { cwd: dir, windowsPathsNoEscape: true }) 64 | .map(s => s.replace(/\\/g, '/')), 65 | expect, 66 | 'sync', 67 | ) 68 | t.strictSame( 69 | ( 70 | await glob(pattern, { cwd: dir, windowsPathsNoEscape: true }) 71 | ).map(s => s.replace(/\\/g, '/')), 72 | expect, 73 | 'async', 74 | ) 75 | }) 76 | } 77 | }) 78 | -------------------------------------------------------------------------------- /test/windows-paths-no-escape.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { Glob } from '../dist/esm/index.js' 3 | 4 | const platforms = ['win32', 'posix'] 5 | const originalPlatform = Object.getOwnPropertyDescriptor( 6 | process, 7 | 'platform', 8 | ) as PropertyDescriptor 9 | t.teardown(() => { 10 | Object.defineProperty(process, 'platform', originalPlatform) 11 | }) 12 | 13 | for (const p of platforms) { 14 | t.test(p, t => { 15 | Object.defineProperty(process, 'platform', { 16 | value: p, 17 | enumerable: true, 18 | configurable: true, 19 | writable: true, 20 | }) 21 | t.equal(process.platform, p, 'gut check: actually set platform') 22 | const pattern = '/a/b/c/x\\[a-b\\]y\\*' 23 | const def = new Glob(pattern, {}) 24 | const winpath = new Glob(pattern, { 25 | windowsPathsNoEscape: true, 26 | }) 27 | const winpathLegacy = new Glob(pattern, { 28 | allowWindowsEscape: false, 29 | }) 30 | const nowinpath = new Glob(pattern, { 31 | windowsPathsNoEscape: false, 32 | }) 33 | 34 | t.strictSame( 35 | [ 36 | def.pattern, 37 | nowinpath.pattern, 38 | winpath.pattern, 39 | winpathLegacy.pattern, 40 | ], 41 | [ 42 | ['/a/b/c/x\\[a-b\\]y\\*'], 43 | ['/a/b/c/x\\[a-b\\]y\\*'], 44 | ['/a/b/c/x/[a-b/]y/*'], 45 | ['/a/b/c/x/[a-b/]y/*'], 46 | ], 47 | ) 48 | t.end() 49 | }) 50 | } 51 | 52 | Object.defineProperty(process, 'platform', originalPlatform) 53 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationMap": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "inlineSources": true, 8 | "jsx": "react", 9 | "module": "nodenext", 10 | "moduleResolution": "nodenext", 11 | "noUncheckedIndexedAccess": true, 12 | "resolveJsonModule": true, 13 | "skipLibCheck": true, 14 | "sourceMap": true, 15 | "strict": true, 16 | "target": "es2022" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationLinks": { 3 | "GitHub": "https://github.com/isaacs/node-glob", 4 | "isaacs projects": "https://isaacs.github.io/" 5 | } 6 | } 7 | --------------------------------------------------------------------------------