Skip to content

Commit cdfde4b

Browse files
committed
add custom Ignore support
Fix: #261 Fix: #363 Fix: #335
1 parent a2fb688 commit cdfde4b

File tree

6 files changed

+134
-24
lines changed

6 files changed

+134
-24
lines changed

README.md

+49-4
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,43 @@ const timeSortedFiles = results
9797
const groupReadableFiles = results
9898
.filter(path => path.mode & 0o040)
9999
.map(path => path.fullpath())
100+
101+
// custom ignores can be done like this, for example by saying
102+
// you'll ignore all markdown files, and all folders named 'docs'
103+
const customIgnoreResults = await glob('**', {
104+
ignore: {
105+
ignored: (p) => /\.md$/.test(p.name),
106+
childrenIgnored: (p) => p.isNamed('docs'),
107+
},
108+
})
109+
110+
// another fun use case, only return files with the same name as
111+
// their parent folder, plus either `.ts` or `.js`
112+
const folderNamedModules = await glob('**/*.{ts,js}', {
113+
ignore: {
114+
ignored: (p) => {
115+
const pp = p.parent
116+
return !(p.isNamed(pp.name + '.ts') || p.isNamed(pp.name + '.js'))
117+
}
118+
}
119+
})
120+
121+
// find all files edited in the last hour
122+
const newFiles = await glob('**', {
123+
// need stat so we have mtime
124+
stat: true,
125+
// only want the files, not the dirs
126+
nodir: true,
127+
ignore: {
128+
ignored: (p) => {
129+
return (new Date() - p.mtime) <= (60 * 60 * 1000)
130+
},
131+
// could add similar childrenIgnored here as well, but
132+
// directory mtime is inconsistent across platforms, so
133+
// probably better not to, unless you know the system
134+
// tracks this reliably.
135+
}
136+
})
100137
```
101138

102139
**Note** Glob patterns should always use `/` as a path separator,
@@ -342,14 +379,22 @@ share the previously loaded cache.
342379
as modified time, permissions, and so on. Note that this will
343380
incur a performance cost due to the added system calls.
344381

345-
- `ignore` string or string[]. A glob pattern or array of glob
346-
patterns to exclude from matches. To ignore all children within
347-
a directory, as well as the entry itself, append `/**'` to the
348-
ignore pattern.
382+
- `ignore` string or string[], or an object with `ignore` and
383+
`ignoreChildren` methods.
384+
385+
If a string or string[] is provided, then this is treated as a
386+
glob pattern or array of glob patterns to exclude from matches.
387+
To ignore all children within a directory, as well as the entry
388+
itself, append `'/**'` to the ignore pattern.
349389

350390
**Note** `ignore` patterns are _always_ in `dot:true` mode,
351391
regardless of any other settings.
352392

393+
If an object is provided that has `ignored(path)` and/or
394+
`childrenIgnored(path)` methods, then these methods will be
395+
called to determine whether any Path is a match or if its
396+
children should be traversed, respectively.
397+
353398
- `follow` Follow symlinked directories when expanding `**`
354399
patterns. This can result in a lot of duplicate references in
355400
the presence of cyclic links, and make performance quite bad.

src/glob.ts

+19-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
PathScurryWin32,
1010
} from 'path-scurry'
1111
import { fileURLToPath } from 'url'
12-
import { Ignore } from './ignore.js'
12+
import { IgnoreLike } from './ignore.js'
1313
import { Pattern } from './pattern.js'
1414
import { GlobStream, GlobWalker } from './walker.js'
1515

@@ -99,11 +99,23 @@ export interface GlobOptions {
9999
follow?: boolean
100100

101101
/**
102-
* A glob pattern or array of glob patterns to exclude from matches. To
103-
* ignore all children within a directory, as well as the entry itself,
104-
* append `/**'` to the ignore pattern.
102+
* string or string[], or an object with `ignore` and `ignoreChildren`
103+
* methods.
104+
*
105+
* If a string or string[] is provided, then this is treated as a glob
106+
* pattern or array of glob patterns to exclude from matches. To ignore all
107+
* children within a directory, as well as the entry itself, append `'/**'`
108+
* to the ignore pattern.
109+
*
110+
* **Note** `ignore` patterns are _always_ in `dot:true` mode, regardless of
111+
* any other settings.
112+
*
113+
* If an object is provided that has `ignored(path)` and/or
114+
* `childrenIgnored(path)` methods, then these methods will be called to
115+
* determine whether any Path is a match or if its children should be
116+
* traversed, respectively.
105117
*/
106-
ignore?: string | string[] | Ignore
118+
ignore?: string | string[] | IgnoreLike
107119

108120
/**
109121
* Treat brace expansion like `{a,b}` as a "magic" pattern. Has no
@@ -306,7 +318,7 @@ export class Glob<Opts extends GlobOptions> implements GlobOptions {
306318
dot: boolean
307319
dotRelative: boolean
308320
follow: boolean
309-
ignore?: Ignore
321+
ignore?: string | string[] | IgnoreLike
310322
magicalBraces: boolean
311323
mark?: boolean
312324
matchBase: boolean
@@ -373,6 +385,7 @@ export class Glob<Opts extends GlobOptions> implements GlobOptions {
373385
this.maxDepth =
374386
typeof opts.maxDepth === 'number' ? opts.maxDepth : Infinity
375387
this.stat = !!opts.stat
388+
this.ignore = opts.ignore
376389

377390
if (this.withFileTypes && this.absolute !== undefined) {
378391
throw new Error('cannot set absolute and withFileTypes:true')

src/ignore.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import { Path } from 'path-scurry'
88
import { Pattern } from './pattern.js'
99
import { GlobWalkerOpts } from './walker.js'
1010

11+
export interface IgnoreLike {
12+
ignored?: (p: Path) => boolean
13+
childrenIgnored?: (p: Path) => boolean
14+
}
15+
1116
const defaultPlatform: NodeJS.Platform =
1217
typeof process === 'object' &&
1318
process &&
@@ -18,7 +23,7 @@ const defaultPlatform: NodeJS.Platform =
1823
/**
1924
* Class used to process ignored patterns
2025
*/
21-
export class Ignore {
26+
export class Ignore implements IgnoreLike {
2227
relative: Minimatch[]
2328
relativeChildren: Minimatch[]
2429
absolute: Minimatch[]
@@ -46,6 +51,8 @@ export class Ignore {
4651
noglobstar,
4752
optimizationLevel: 2,
4853
platform,
54+
nocomment: true,
55+
nonegate: true,
4956
}
5057

5158
// this is a little weird, but it gives us a clean set of optimized

src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,10 @@ export type {
182182
GlobOptionsWithFileTypesUnset,
183183
} from './glob.js'
184184
export { hasMagic } from './has-magic.js'
185+
export type { IgnoreLike } from './ignore.js'
185186
export type { MatchStream } from './walker.js'
186-
187187
/* c8 ignore stop */
188+
188189
export default Object.assign(glob, {
189190
glob,
190191
globSync,

src/walker.ts

+10-12
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
import Minipass from 'minipass'
88
import { Path } from 'path-scurry'
9-
import { Ignore } from './ignore.js'
9+
import { Ignore, IgnoreLike } from './ignore.js'
1010

1111
// XXX can we somehow make it so that it NEVER processes a given path more than
1212
// once, enough that the match set tracking is no longer needed? that'd speed
@@ -23,7 +23,7 @@ export interface GlobWalkerOpts {
2323
dot?: boolean
2424
dotRelative?: boolean
2525
follow?: boolean
26-
ignore?: string | string[] | Ignore
26+
ignore?: string | string[] | IgnoreLike
2727
mark?: boolean
2828
matchBase?: boolean
2929
// Note: maxDepth here means "maximum actual Path.depth()",
@@ -79,16 +79,14 @@ export type MatchStream<O extends GlobWalkerOpts> =
7979
: Minipass<Path | string, Path | string>
8080

8181
const makeIgnore = (
82-
ignore: string | string[] | Ignore,
82+
ignore: string | string[] | IgnoreLike,
8383
opts: GlobWalkerOpts
84-
): Ignore =>
84+
): IgnoreLike =>
8585
typeof ignore === 'string'
8686
? new Ignore([ignore], opts)
8787
: Array.isArray(ignore)
8888
? new Ignore(ignore, opts)
89-
: /* c8 ignore start */
90-
ignore
91-
/* c8 ignore stop */
89+
: ignore
9290

9391
/**
9492
* basic walking utilities that all the glob walker types use
@@ -101,7 +99,7 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
10199
paused: boolean = false
102100
aborted: boolean = false
103101
#onResume: (() => any)[] = []
104-
#ignore?: Ignore
102+
#ignore?: IgnoreLike
105103
#sep: '\\' | '/'
106104
signal?: AbortSignal
107105
maxDepth: number
@@ -129,10 +127,10 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
129127
}
130128

131129
#ignored(path: Path): boolean {
132-
return this.seen.has(path) || !!this.#ignore?.ignored(path)
130+
return this.seen.has(path) || !!this.#ignore?.ignored?.(path)
133131
}
134132
#childrenIgnored(path: Path): boolean {
135-
return !!this.#ignore?.childrenIgnored(path)
133+
return !!this.#ignore?.childrenIgnored?.(path)
136134
}
137135

138136
// backpressure mechanism
@@ -177,9 +175,9 @@ export abstract class GlobUtil<O extends GlobWalkerOpts = GlobWalkerOpts> {
177175
matchCheckTest(e: Path | undefined, ifDir: boolean): Path | undefined {
178176
return e &&
179177
(this.maxDepth === Infinity || e.depth() <= this.maxDepth) &&
180-
!this.#ignored(e) &&
181178
(!ifDir || e.canReaddir()) &&
182-
(!this.opts.nodir || !e.isDirectory())
179+
(!this.opts.nodir || !e.isDirectory()) &&
180+
!this.#ignored(e)
183181
? e
184182
: undefined
185183
}

test/custom-ignore.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { basename, resolve } from 'path'
2+
import { Path } from 'path-scurry'
3+
import t from 'tap'
4+
import { glob, globSync, IgnoreLike } from '../'
5+
const cwd = resolve(__dirname, 'fixtures')
6+
7+
const j = (a: string[]) =>
8+
a
9+
.map(s => s.replace(/\\/g, '/'))
10+
.sort((a, b) => a.localeCompare(b, 'en'))
11+
12+
t.test('ignore files with long names', async t => {
13+
const ignore: IgnoreLike = {
14+
ignored: (p: Path) => p.name.length > 1,
15+
}
16+
const syncRes = globSync('**', { cwd, ignore })
17+
const asyncRes = await glob('**', { cwd, ignore })
18+
const expect = j(
19+
globSync('**', { cwd }).filter(p => basename(p).length === 1)
20+
)
21+
t.same(j(syncRes), expect)
22+
t.same(j(asyncRes), expect)
23+
for (const r of syncRes) {
24+
if (basename(r).length > 1) t.fail(r)
25+
}
26+
})
27+
28+
t.test('ignore symlink and abcdef directories', async t => {
29+
const ignore: IgnoreLike = {
30+
childrenIgnored: (p: Path) => {
31+
return p.isNamed('symlink') || p.isNamed('abcdef')
32+
},
33+
}
34+
const syncRes = globSync('**', { cwd, ignore, nodir: true })
35+
const asyncRes = await glob('**', { cwd, ignore, nodir: true })
36+
const expect = j(
37+
globSync('**', { nodir: true, cwd }).filter(p => {
38+
return !/\bsymlink\b|\babcdef\b/.test(p)
39+
})
40+
)
41+
t.same(j(syncRes), expect)
42+
t.same(j(asyncRes), expect)
43+
for (const r of syncRes) {
44+
if (r === 'symlink' || r === 'basename') t.fail(r)
45+
}
46+
})

0 commit comments

Comments
 (0)