Skip to content

Commit

Permalink
feat!: replace minimatch with fast-glob (#126)
Browse files Browse the repository at this point in the history
`fast-glob` is indeed fast. When running Helia's add-dir benchmark:

Before:

```
┌─────────┬───────────────────────────┬──────────┬──────────┬──────┬──────────┬──────────────────────────────────────────────────┐
│ (index) │ Implementation            │ ops/s    │ ms/op    │ runs │ p99      │ CID                                              │
├─────────┼───────────────────────────┼──────────┼──────────┼──────┼──────────┼──────────────────────────────────────────────────┤
│ 0       │ 'helia-fs - src'          │ '56.95'  │ '17.56'  │ 5    │ '53.93'  │ 'QmY7YuAvtqk9AaMR8mivfyCdSVFT4sJJwLCLYtKYfwAEKf' │
│ 1       │ 'helia-fs - dist'         │ '51.74'  │ '19.33'  │ 5    │ '81.24'  │ 'QmdHWaHWXugHHvP2y4SueC1tWUgQBrrzUHUwEMrdNEd7Cn' │
│ 2       │ 'helia-fs - ../gc/src'    │ '190.84' │ '5.24'   │ 5    │ '21.17'  │ 'QmQvJeSxJdCVi8qWwujznXq8Zs2g3gQVNqJUtr9SDUAK9X' │
│ 3       │ 'helia-mem - src'         │ '657.37' │ '1.52'   │ 5    │ '2.33'   │ 'QmY7YuAvtqk9AaMR8mivfyCdSVFT4sJJwLCLYtKYfwAEKf' │
│ 4       │ 'helia-mem - dist'        │ '287.28' │ '3.48'   │ 5    │ '4.09'   │ 'QmdHWaHWXugHHvP2y4SueC1tWUgQBrrzUHUwEMrdNEd7Cn' │
│ 5       │ 'helia-mem - ../gc/src'   │ '860.15' │ '1.16'   │ 5    │ '1.32'   │ 'QmQvJeSxJdCVi8qWwujznXq8Zs2g3gQVNqJUtr9SDUAK9X' │
└─────────┴───────────────────────────┴──────────┴──────────┴──────┴──────────┴──────────────────────────────────────────────────┘
```

After:

```
┌─────────┬───────────────────────────┬──────────┬──────────┬──────┬──────────┬──────────────────────────────────────────────────┐
│ (index) │ Implementation            │ ops/s    │ ms/op    │ runs │ p99      │ CID                                              │
├─────────┼───────────────────────────┼──────────┼──────────┼──────┼──────────┼──────────────────────────────────────────────────┤
│ 0       │ 'helia-fs - src'          │ '110.42' │ '9.06'   │ 5    │ '35.54'  │ 'QmY7YuAvtqk9AaMR8mivfyCdSVFT4sJJwLCLYtKYfwAEKf' │
│ 1       │ 'helia-fs - dist'         │ '44.90'  │ '22.27'  │ 5    │ '96.31'  │ 'QmdHWaHWXugHHvP2y4SueC1tWUgQBrrzUHUwEMrdNEd7Cn' │
│ 2       │ 'helia-fs - ../gc/src'    │ '121.47' │ '8.23'   │ 5    │ '35.53'  │ 'QmQvJeSxJdCVi8qWwujznXq8Zs2g3gQVNqJUtr9SDUAK9X' │
│ 3       │ 'helia-mem - src'         │ '697.10' │ '1.43'   │ 5    │ '1.99'   │ 'QmY7YuAvtqk9AaMR8mivfyCdSVFT4sJJwLCLYtKYfwAEKf' │
│ 4       │ 'helia-mem - dist'        │ '332.42' │ '3.01'   │ 5    │ '3.38'   │ 'QmdHWaHWXugHHvP2y4SueC1tWUgQBrrzUHUwEMrdNEd7Cn' │
│ 5       │ 'helia-mem - ../gc/src'   │ '852.31' │ '1.17'   │ 5    │ '2.06'   │ 'QmQvJeSxJdCVi8qWwujznXq8Zs2g3gQVNqJUtr9SDUAK9X' │
└─────────┴───────────────────────────┴──────────┴──────────┴──────┴──────────┴──────────────────────────────────────────────────┘
```

BREAKING CHANGE: The options type used to extend `minimatch`'s options, now it's `fast-glob`'s options - some field names are different. Windows paths will also be yielded with forward slashes and not backslashes.
  • Loading branch information
achingbrain authored Apr 25, 2024
1 parent f6f7040 commit bea32ab
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 83 deletions.
28 changes: 20 additions & 8 deletions packages/it-glob/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,39 @@
# About

<!--
!IMPORTANT!
Everything in this README between "# About" and "# Install" is automatically
generated and will be overwritten the next time the doc generator is run.
To make changes to this section, please update the @packageDocumentation section
of src/index.js or src/index.ts
To experiment with formatting, please run "npm run docs" from the root of this
repo and examine the changes made.
-->

Like [`glob`](https://npmjs.com/package/glob) but async iterable.

File separators on Windows will be yielded as `/` and not \`\`.

## Example

```javascript
import glob from 'it-glob'

const options = {
cwd // defaults to process.cwd
absolute // return absolute paths, defaults to false
nodir // only yield file paths, skip directories

// all other options are passed to minimatch
}
// All options are passed through to fast-glob
const options = {}

for await (const path of glob('/path/to/file', '**/*', options)) {
console.info(path)
}
```

See the [minimatch docs](https://www.npmjs.com/package/minimatch#options) for the full list of options.
See the [fast-glob docs](https://github.com/mrmlnc/fast-glob#options-3) for the full list of options.

# Install

Expand Down
7 changes: 3 additions & 4 deletions packages/it-glob/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,20 @@
}
},
"scripts": {
"build": "aegir build",
"build": "aegir build --bundle false",
"lint": "aegir lint",
"dep-check": "aegir dep-check",
"test": "aegir test -t node",
"test:node": "aegir test -t node --cov"
},
"dependencies": {
"minimatch": "^9.0.4"
"fast-glob": "^3.3.2"
},
"devDependencies": {
"aegir": "^42.2.5",
"it-all": "^3.0.0"
},
"browser": {
"fs/promises": false,
"path": false
"./dist/src/index.js": "./dist/src/index.browser.js"
}
}
4 changes: 4 additions & 0 deletions packages/it-glob/src/index.browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// eslint-disable-next-line require-yield
export default async function * glob (): AsyncGenerator<string, void, undefined> {
throw new Error('it-glob does not work in browsers')
}
80 changes: 15 additions & 65 deletions packages/it-glob/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,90 +3,40 @@
*
* Like [`glob`](https://npmjs.com/package/glob) but async iterable.
*
* File separators on Windows will be yielded as `/` and not `\`.
*
* @example
*
* ```javascript
* import glob from 'it-glob'
*
* const options = {
* cwd // defaults to process.cwd
* absolute // return absolute paths, defaults to false
* nodir // only yield file paths, skip directories
*
* // all other options are passed to minimatch
* }
* // All options are passed through to fast-glob
* const options = {}
*
* for await (const path of glob('/path/to/file', '**\/*', options)) {
* console.info(path)
* }
* ```
*
* See the [minimatch docs](https://www.npmjs.com/package/minimatch#options) for the full list of options.
* See the [fast-glob docs](https://github.com/mrmlnc/fast-glob#options-3) for the full list of options.
*/

import fs from 'fs/promises'
import path from 'path'
import { minimatch } from 'minimatch'
import type { MinimatchOptions } from 'minimatch'

export interface GlobOptions extends MinimatchOptions {
/**
* The current working directory
*/
cwd?: string

/**
* If true produces absolute paths (default: false)
*/
absolute?: boolean

/**
* If true yields file paths and skip directories (default: false)
*/
nodir?: boolean
}
import fs from 'node:fs/promises'
import path from 'node:path'
import fastGlob from 'fast-glob'
import type { Options } from 'fast-glob'

/**
* Async iterable filename pattern matcher
*/
export default async function * glob (dir: string, pattern: string, options: GlobOptions = {}): AsyncGenerator<string, void, undefined> {
export default async function * glob (dir: string, pattern: string, options: Options = {}): AsyncGenerator<string, void, undefined> {
const absoluteDir = path.resolve(dir)
const relativeDir = path.relative(options.cwd ?? process.cwd(), dir)

const stats = await fs.stat(absoluteDir)

if (stats.isDirectory()) {
for await (const entry of _glob(absoluteDir, '', pattern, options)) {
yield entry
}

return
}

if (minimatch(relativeDir, pattern, options)) {
yield options.absolute === true ? absoluteDir : relativeDir
}
}

async function * _glob (base: string, dir: string, pattern: string, options: GlobOptions): AsyncGenerator<string, void, undefined> {
for await (const entry of await fs.opendir(path.join(base, dir))) {
const relativeEntryPath = path.join(dir, entry.name)
const absoluteEntryPath = path.join(base, dir, entry.name)

let match = minimatch(relativeEntryPath, pattern, options)

const isDirectory = entry.isDirectory()

if (isDirectory && options.nodir === true) {
match = false
}

if (match) {
yield options.absolute === true ? absoluteEntryPath : relativeEntryPath
}

if (isDirectory) {
yield * _glob(base, relativeEntryPath, pattern, options)
}
for await (const entry of fastGlob.stream(pattern, {
...options,
cwd: stats.isDirectory() ? dir : process.cwd()
})) {
yield entry.toString()
}
}
14 changes: 8 additions & 6 deletions packages/it-glob/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('it-glob', () => {
it('should match file in subdirectory', async () => {
const files = await all(glob('.', '**/*'))

expect(files.includes(path.join('dist', 'src', 'index.d.ts'))).to.be.true()
expect(files.includes('dist/src/index.d.ts')).to.be.true()
})

it('should match one', async () => {
Expand Down Expand Up @@ -64,18 +64,20 @@ describe('it-glob', () => {
})

it('should match directories', async () => {
const files = await all(glob(path.resolve(dir, '..', '..'), 'dist/*'))
const files = await all(glob(path.resolve(dir, '..', '..'), 'dist/*', {
onlyFiles: false
}))

expect(files.includes(path.join('dist', 'src'))).to.be.true()
expect(files.includes('dist/src')).to.be.true()
})

it('should skip directories', async () => {
const files = await all(glob(path.resolve(dir, '..', '..'), 'dist/**/*', {
nodir: true,
onlyFiles: true,
dot: true
}))

expect(files.includes(path.join('dist', 'src'))).to.be.false()
expect(files.includes(path.join('dist', 'src', 'index.js'))).to.be.true()
expect(files.includes('dist/src')).to.be.false()
expect(files.includes('dist/src/index.js')).to.be.true()
})
})

0 comments on commit bea32ab

Please sign in to comment.