Skip to content

Commit 4e155d1

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 4e155d1

File tree

3 files changed

+281
-14
lines changed

3 files changed

+281
-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: 251 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313
* Used by Socket tools for file discovery and npm package analysis.
1414
*/
1515

16+
import path from 'node:path'
1617
import {
1718
defaultIgnore,
1819
getGlobMatcher,
20+
glob,
1921
globStreamLicenses,
22+
globSync,
2023
} from '@socketsecurity/lib/globs'
2124
import { describe, expect, it } from 'vitest'
2225

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

179194
describe('globStreamLicenses', () => {
@@ -184,6 +199,19 @@ describe('globs', () => {
184199
expect(typeof stream.pipe).toBe('function')
185200
})
186201

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

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

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

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

212271
it('should handle absolute option', () => {
@@ -229,14 +288,22 @@ describe('globs', () => {
229288
expect(stream).toBeDefined()
230289
})
231290

232-
it('should handle multiple options together', () => {
233-
const stream = globStreamLicenses('.', {
291+
it('should handle multiple options together', async () => {
292+
const files: string[] = []
293+
const stream = globStreamLicenses(process.cwd(), {
234294
recursive: true,
235295
ignoreOriginals: true,
236296
dot: true,
237297
absolute: true,
238298
})
239-
expect(stream).toBeDefined()
299+
300+
await new Promise<void>((resolve, reject) => {
301+
stream.on('data', (file: string) => files.push(file))
302+
stream.on('end', () => resolve())
303+
stream.on('error', reject)
304+
})
305+
306+
expect(Array.isArray(files)).toBe(true)
240307
})
241308

242309
it('should be a function', () => {
@@ -250,6 +317,178 @@ describe('globs', () => {
250317
})
251318
})
252319

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

0 commit comments

Comments
 (0)