import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { tmpdir } from 'node:os'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { globbySync } from 'globby'
import { globSync as fastGlobSync } from 'fast-glob'
import { globSync as tinyglobbySync } from 'tinyglobby'
function glob(lib: string, patterns: string[], cwd: string): string[] {
if (lib === 'fast-glob') {
return fastGlobSync(patterns, { cwd, absolute: true })
} else if (lib === 'tinyglobby') {
return tinyglobbySync(patterns, { cwd, absolute: true })
} else {
return globbySync(patterns, { cwd, absolute: true })
}
}
describe.each(['fast-glob', 'globby', 'tinyglobby'])('$0', lib => {
let tempDir: string
function writeTestFile(path: string) {
mkdirSync(join(tempDir, dirname(path)), { recursive: true })
writeFileSync(join(tempDir, path), '')
}
beforeEach(() => {
// Create a unique temporary directory for each test
tempDir = join(tmpdir(), `glob-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
mkdirSync(tempDir, { recursive: true })
})
afterEach(() => {
// Clean up the temporary directory
rmSync(tempDir, { recursive: true, force: true })
})
it('should handle simple file paths', () => {
// Create test files
writeTestFile('a.js')
writeTestFile('b/c.js')
// Test with a simple file path (non-glob)
const resolved = glob(lib, ['a.js', 'b/c.js'], tempDir)
// Should resolve to absolute paths
expect(resolved).toEqual([join(tempDir, 'a.js'), join(tempDir, 'b', 'c.js')])
})
it('should handle glob patterns without negation', () => {
// Create test directory structure
writeTestFile('config/a.js')
writeTestFile('config/b/c.js')
// Test with a glob pattern
const watchPaths = ['config/**']
const resolved = glob(lib, watchPaths, tempDir)
// Should resolve to absolute paths of all files in config directory
expect(resolved.length).toBe(2)
expect(resolved).toContain(join(tempDir, 'config', 'a.js'))
expect(resolved).toContain(join(tempDir, 'config', 'b', 'c.js'))
})
it('should handle single glob pattern with negation using *', () => {
// Create test directory structure
writeTestFile('loc/en.po')
writeTestFile('loc/es.po')
writeTestFile('loc/fr.po')
writeTestFile('loc/one/en.po')
writeTestFile('loc/one/es.po')
writeTestFile('loc/one/fr.po')
writeTestFile('loc/two/en.po')
writeTestFile('loc/two/es.po')
writeTestFile('loc/two/fr.po')
// Test with a negation pattern (should match everything except en.po)
const watchPaths = ['loc/*/!(en.po)']
const resolved = glob(lib, watchPaths, tempDir)
// Should resolve to absolute paths, excluding en.po files
expect(resolved.length).toBe(4)
expect(resolved).toContain(join(tempDir, 'loc', 'one', 'es.po'))
expect(resolved).toContain(join(tempDir, 'loc', 'one', 'fr.po'))
expect(resolved).toContain(join(tempDir, 'loc', 'two', 'es.po'))
expect(resolved).toContain(join(tempDir, 'loc', 'two', 'fr.po'))
})
it('should handle single glob pattern with negation using **', () => {
// Create test directory structure
writeTestFile('loc/en.po')
writeTestFile('loc/es.po')
writeTestFile('loc/fr.po')
writeTestFile('loc/one/en.po')
writeTestFile('loc/one/es.po')
writeTestFile('loc/one/fr.po')
writeTestFile('loc/two/en.po')
writeTestFile('loc/two/es.po')
writeTestFile('loc/two/fr.po')
// Test with a negation pattern (should match everything except en.po)
const watchPaths = ['loc/**/!(en.po)']
const resolved = glob(lib, watchPaths, tempDir)
// Should resolve to absolute paths, excluding en.po files
expect(resolved.length).toBe(6)
expect(resolved).toContain(join(tempDir, 'loc', 'es.po'))
expect(resolved).toContain(join(tempDir, 'loc', 'fr.po'))
expect(resolved).toContain(join(tempDir, 'loc', 'one', 'es.po'))
expect(resolved).toContain(join(tempDir, 'loc', 'one', 'fr.po'))
expect(resolved).toContain(join(tempDir, 'loc', 'two', 'es.po'))
expect(resolved).toContain(join(tempDir, 'loc', 'two', 'fr.po'))
})
it('should handle multiple glob patterns that involve negation', () => {
// Create test directory structure
writeTestFile('loc/en.po')
writeTestFile('loc/es.po')
writeTestFile('loc/fr.po')
writeTestFile('loc/one/en.po')
writeTestFile('loc/one/es.po')
writeTestFile('loc/one/fr.po')
writeTestFile('loc/two/en.po')
writeTestFile('loc/two/es.po')
writeTestFile('loc/two/fr.po')
// Test with a negation pattern (should match everything except en.po)
const watchPaths = ['loc/**/*.po', '!loc/**/en.po']
const resolved = glob(lib, watchPaths, tempDir)
// Should resolve to absolute paths, excluding en.po files
expect(resolved.length).toBe(6)
expect(resolved).toContain(join(tempDir, 'loc', 'es.po'))
expect(resolved).toContain(join(tempDir, 'loc', 'fr.po'))
expect(resolved).toContain(join(tempDir, 'loc', 'one', 'es.po'))
expect(resolved).toContain(join(tempDir, 'loc', 'one', 'fr.po'))
expect(resolved).toContain(join(tempDir, 'loc', 'two', 'es.po'))
expect(resolved).toContain(join(tempDir, 'loc', 'two', 'fr.po'))
})
it('should handle mixed simple paths and glob patterns', () => {
// Create test files
writeTestFile('x.js')
writeTestFile('config/a.js')
writeTestFile('config/b/c.js')
// Mix of simple path and glob pattern
const watchPaths = ['x.js', 'config/**']
const resolved = glob(lib, watchPaths, tempDir)
// Should resolve to absolute paths
expect(resolved.length).toBe(3)
expect(resolved).toContain(join(tempDir, 'x.js'))
expect(resolved).toContain(join(tempDir, 'config', 'a.js'))
expect(resolved).toContain(join(tempDir, 'config', 'b', 'c.js'))
})
})
I recently started using tinyglobby to handle glob patterns after upgrading from chokidar 3.x to 4.x (which no longer has built-in glob support). Everything with tinyglobby has been great (thank you!) except I found one extglob-style negation pattern that worked in chokidar (and also works in fast-glob and globby) but doesn't seem to work in tinyglobby. (See related issue climateinteractive/SDEverywhere#760.)
Here's the pattern that doesn't work in tinyglobby (but works in fast-glob and globby):
I created a full vitest file that runs a number of tests for fast-glob, globby, and tinyglobby. See "Full test file" section below. Here's the one test case from it that fails:
When run with tinyglobby, it returns 8 files: it does correctly exclude
loc/en.po, but doesn't excludeloc/one/en.poandloc/two/en.po.Workarounds
For my use case, I found two workarounds that work with tinyglobby:
['loc/*/!(en.po)']['loc/**/*.po', '!loc/**/en.po']Full test file
Click to show full test file