Skip to content

Commit f46c6c1

Browse files
committed
feat(globs): expose glob and globSync wrappers for fast-glob
Add async and sync file pattern matching functions as wrappers around fast-glob. These complement the existing globStreamLicenses and getGlobMatcher utilities. - Add glob() for async file pattern matching - Add globSync() for sync file pattern matching - Export glob, globStream, and globSync from fast-glob external module
1 parent 23281d2 commit f46c6c1

File tree

3 files changed

+280
-14
lines changed

3 files changed

+280
-14
lines changed

src/external/fast-glob.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
// Export only what we use to reduce bundle size
44
const fastGlob = require('fast-glob')
55

6-
// Export just globStream - the only method we use
6+
// Export the methods we use
77
module.exports = fastGlob.globStream
8-
? { globStream: fastGlob.globStream }
8+
? {
9+
glob: fastGlob,
10+
globStream: fastGlob.globStream,
11+
globSync: fastGlob.sync,
12+
}
913
: fastGlob

src/globs.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,27 @@ export function getGlobMatcher(
162162
matcherCache.set(key, matcher)
163163
return matcher
164164
}
165+
166+
/**
167+
* Asynchronously find files matching glob patterns.
168+
* Wrapper around fast-glob.
169+
*/
170+
/*@__NO_SIDE_EFFECTS__*/
171+
export function glob(
172+
patterns: Pattern | Pattern[],
173+
options?: FastGlobOptions,
174+
): Promise<string[]> {
175+
return fastGlob.glob(patterns, options as import('fast-glob').Options)
176+
}
177+
178+
/**
179+
* Synchronously find files matching glob patterns.
180+
* Wrapper around fast-glob.sync.
181+
*/
182+
/*@__NO_SIDE_EFFECTS__*/
183+
export function globSync(
184+
patterns: Pattern | Pattern[],
185+
options?: FastGlobOptions,
186+
): string[] {
187+
return fastGlob.globSync(patterns, options as import('fast-glob').Options)
188+
}

test/unit/globs.test.ts

Lines changed: 250 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
import {
1717
defaultIgnore,
1818
getGlobMatcher,
19+
glob,
1920
globStreamLicenses,
21+
globSync,
2022
} from '@socketsecurity/lib/globs'
2123
import { describe, expect, it } from 'vitest'
2224

@@ -174,6 +176,18 @@ describe('globs', () => {
174176
expect(matcher('src/app.js')).toBe(true)
175177
expect(matcher('src/utils/helper.js')).toBe(true)
176178
})
179+
180+
it('should handle only negative patterns', () => {
181+
const matcher = getGlobMatcher(['!*.test.js', '!*.spec.js'])
182+
expect(typeof matcher).toBe('function')
183+
})
184+
185+
it('should map negative patterns correctly', () => {
186+
const matcher = getGlobMatcher(['*.js', '!test/*.js', '!spec/*.js'])
187+
expect(matcher('app.js')).toBe(true)
188+
expect(matcher('test/app.js')).toBe(false)
189+
expect(matcher('spec/app.js')).toBe(false)
190+
})
177191
})
178192

179193
describe('globStreamLicenses', () => {
@@ -184,6 +198,19 @@ describe('globs', () => {
184198
expect(typeof stream.pipe).toBe('function')
185199
})
186200

201+
it('should stream license files', async () => {
202+
const files: string[] = []
203+
const stream = globStreamLicenses(process.cwd(), { recursive: false })
204+
205+
await new Promise<void>((resolve, reject) => {
206+
stream.on('data', (file: string) => files.push(file))
207+
stream.on('end', () => resolve())
208+
stream.on('error', reject)
209+
})
210+
211+
expect(Array.isArray(files)).toBe(true)
212+
})
213+
187214
it('should accept dirname parameter', () => {
188215
expect(() => globStreamLicenses('.')).not.toThrow()
189216
expect(() => globStreamLicenses('./src')).not.toThrow()
@@ -194,19 +221,50 @@ describe('globs', () => {
194221
expect(() => globStreamLicenses('.', { recursive: true })).not.toThrow()
195222
})
196223

197-
it('should handle ignoreOriginals option', () => {
198-
const stream = globStreamLicenses('.', { ignoreOriginals: true })
199-
expect(stream).toBeDefined()
224+
it('should handle ignoreOriginals option', async () => {
225+
const files: string[] = []
226+
const stream = globStreamLicenses(process.cwd(), {
227+
ignoreOriginals: true,
228+
recursive: false,
229+
})
230+
231+
await new Promise<void>((resolve, reject) => {
232+
stream.on('data', (file: string) => files.push(file))
233+
stream.on('end', () => resolve())
234+
stream.on('error', reject)
235+
})
236+
237+
// Should not include files matching *.original pattern
238+
expect(files.every(f => !f.includes('.original'))).toBe(true)
200239
})
201240

202-
it('should handle recursive option', () => {
203-
const stream = globStreamLicenses('.', { recursive: false })
204-
expect(stream).toBeDefined()
241+
it('should handle recursive option', async () => {
242+
const files: string[] = []
243+
const stream = globStreamLicenses(process.cwd(), { recursive: true })
244+
245+
await new Promise<void>((resolve, reject) => {
246+
stream.on('data', (file: string) => files.push(file))
247+
stream.on('end', () => resolve())
248+
stream.on('error', reject)
249+
})
250+
251+
expect(Array.isArray(files)).toBe(true)
205252
})
206253

207-
it('should handle custom ignore patterns', () => {
208-
const stream = globStreamLicenses('.', { ignore: ['**/node_modules/**'] })
209-
expect(stream).toBeDefined()
254+
it('should handle custom ignore patterns as array', async () => {
255+
const files: string[] = []
256+
const stream = globStreamLicenses(process.cwd(), {
257+
ignore: ['**/test/**', '**/node_modules/**'],
258+
recursive: false,
259+
})
260+
261+
await new Promise<void>((resolve, reject) => {
262+
stream.on('data', (file: string) => files.push(file))
263+
stream.on('end', () => resolve())
264+
stream.on('error', reject)
265+
})
266+
267+
expect(Array.isArray(files)).toBe(true)
210268
})
211269

212270
it('should handle absolute option', () => {
@@ -229,14 +287,22 @@ describe('globs', () => {
229287
expect(stream).toBeDefined()
230288
})
231289

232-
it('should handle multiple options together', () => {
233-
const stream = globStreamLicenses('.', {
290+
it('should handle multiple options together', async () => {
291+
const files: string[] = []
292+
const stream = globStreamLicenses(process.cwd(), {
234293
recursive: true,
235294
ignoreOriginals: true,
236295
dot: true,
237296
absolute: true,
238297
})
239-
expect(stream).toBeDefined()
298+
299+
await new Promise<void>((resolve, reject) => {
300+
stream.on('data', (file: string) => files.push(file))
301+
stream.on('end', () => resolve())
302+
stream.on('error', reject)
303+
})
304+
305+
expect(Array.isArray(files)).toBe(true)
240306
})
241307

242308
it('should be a function', () => {
@@ -250,6 +316,178 @@ describe('globs', () => {
250316
})
251317
})
252318

319+
describe('glob', () => {
320+
it('should be a function', () => {
321+
expect(typeof glob).toBe('function')
322+
})
323+
324+
it('should return a promise', () => {
325+
const result = glob('*.js')
326+
expect(result).toBeInstanceOf(Promise)
327+
})
328+
329+
it('should find files matching pattern', async () => {
330+
const files = await glob('*.json', { cwd: process.cwd() })
331+
expect(Array.isArray(files)).toBe(true)
332+
expect(files.length).toBeGreaterThan(0)
333+
expect(files.some(f => f.includes('package.json'))).toBe(true)
334+
})
335+
336+
it('should accept array of patterns', async () => {
337+
const files = await glob(['*.json', '*.md'], { cwd: process.cwd() })
338+
expect(Array.isArray(files)).toBe(true)
339+
})
340+
341+
it('should respect cwd option', async () => {
342+
const files = await glob('*.ts', { cwd: 'src' })
343+
expect(Array.isArray(files)).toBe(true)
344+
})
345+
346+
it('should handle ignore patterns', async () => {
347+
const files = await glob('**/*.ts', {
348+
cwd: 'src',
349+
ignore: ['**/paths/**'],
350+
})
351+
expect(Array.isArray(files)).toBe(true)
352+
expect(files.every(f => !f.includes('paths/'))).toBe(true)
353+
})
354+
355+
it('should handle absolute option', async () => {
356+
const files = await glob('*.json', {
357+
cwd: process.cwd(),
358+
absolute: true,
359+
})
360+
expect(Array.isArray(files)).toBe(true)
361+
if (files.length > 0) {
362+
expect(files[0].startsWith('/')).toBe(true)
363+
}
364+
})
365+
366+
it('should handle onlyFiles option', async () => {
367+
const files = await glob('*', { cwd: process.cwd(), onlyFiles: true })
368+
expect(Array.isArray(files)).toBe(true)
369+
})
370+
371+
it('should handle dot option', async () => {
372+
const files = await glob('.*', { cwd: process.cwd(), dot: true })
373+
expect(Array.isArray(files)).toBe(true)
374+
})
375+
376+
it('should handle empty pattern array', async () => {
377+
const files = await glob([], { cwd: process.cwd() })
378+
expect(Array.isArray(files)).toBe(true)
379+
expect(files.length).toBe(0)
380+
})
381+
382+
it('should handle single pattern string', async () => {
383+
const files = await glob('package.json', { cwd: process.cwd() })
384+
expect(Array.isArray(files)).toBe(true)
385+
expect(files.some(f => f.includes('package.json'))).toBe(true)
386+
})
387+
388+
it('should handle negation patterns', async () => {
389+
const files = await glob(['*.json', '!package-lock.json'], {
390+
cwd: process.cwd(),
391+
})
392+
expect(Array.isArray(files)).toBe(true)
393+
expect(files.every(f => !f.includes('package-lock.json'))).toBe(true)
394+
})
395+
396+
it('should work without options parameter', async () => {
397+
const files = await glob('*.json')
398+
expect(Array.isArray(files)).toBe(true)
399+
})
400+
})
401+
402+
describe('globSync', () => {
403+
it('should be a function', () => {
404+
expect(typeof globSync).toBe('function')
405+
})
406+
407+
it('should return an array', () => {
408+
const result = globSync('*.json', { cwd: process.cwd() })
409+
expect(Array.isArray(result)).toBe(true)
410+
})
411+
412+
it('should find files matching pattern', () => {
413+
const files = globSync('*.json', { cwd: process.cwd() })
414+
expect(Array.isArray(files)).toBe(true)
415+
expect(files.length).toBeGreaterThan(0)
416+
expect(files.some(f => f.includes('package.json'))).toBe(true)
417+
})
418+
419+
it('should accept array of patterns', () => {
420+
const files = globSync(['*.json', '*.md'], { cwd: process.cwd() })
421+
expect(Array.isArray(files)).toBe(true)
422+
})
423+
424+
it('should respect cwd option', () => {
425+
const files = globSync('*.ts', { cwd: 'src' })
426+
expect(Array.isArray(files)).toBe(true)
427+
})
428+
429+
it('should handle ignore patterns', () => {
430+
const files = globSync('**/*.ts', {
431+
cwd: 'src',
432+
ignore: ['**/paths/**'],
433+
})
434+
expect(Array.isArray(files)).toBe(true)
435+
expect(files.every(f => !f.includes('paths/'))).toBe(true)
436+
})
437+
438+
it('should handle absolute option', () => {
439+
const files = globSync('*.json', {
440+
cwd: process.cwd(),
441+
absolute: true,
442+
})
443+
expect(Array.isArray(files)).toBe(true)
444+
if (files.length > 0) {
445+
expect(files[0].startsWith('/')).toBe(true)
446+
}
447+
})
448+
449+
it('should handle onlyFiles option', () => {
450+
const files = globSync('*', { cwd: process.cwd(), onlyFiles: true })
451+
expect(Array.isArray(files)).toBe(true)
452+
})
453+
454+
it('should handle dot option', () => {
455+
const files = globSync('.*', { cwd: process.cwd(), dot: true })
456+
expect(Array.isArray(files)).toBe(true)
457+
})
458+
459+
it('should return same results as async glob', async () => {
460+
const syncFiles = globSync('*.json', { cwd: process.cwd() })
461+
const asyncFiles = await glob('*.json', { cwd: process.cwd() })
462+
expect(syncFiles.sort()).toEqual(asyncFiles.sort())
463+
})
464+
465+
it('should handle empty pattern array', () => {
466+
const files = globSync([], { cwd: process.cwd() })
467+
expect(Array.isArray(files)).toBe(true)
468+
expect(files.length).toBe(0)
469+
})
470+
471+
it('should handle single pattern string', () => {
472+
const files = globSync('package.json', { cwd: process.cwd() })
473+
expect(Array.isArray(files)).toBe(true)
474+
expect(files.some(f => f.includes('package.json'))).toBe(true)
475+
})
476+
477+
it('should handle negation patterns', () => {
478+
const files = globSync(['*.json', '!package-lock.json'], {
479+
cwd: process.cwd(),
480+
})
481+
expect(Array.isArray(files)).toBe(true)
482+
expect(files.every(f => !f.includes('package-lock.json'))).toBe(true)
483+
})
484+
485+
it('should work without options parameter', () => {
486+
const files = globSync('*.json')
487+
expect(Array.isArray(files)).toBe(true)
488+
})
489+
})
490+
253491
describe('integration', () => {
254492
it('should have consistent behavior across calls', () => {
255493
const matcher1 = getGlobMatcher('*.js')

0 commit comments

Comments
 (0)