Skip to content

Some extglob negation patterns are not working with expandDirectories: true #188

@chrispcampbell

Description

@chrispcampbell

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):

['loc/**/!(en.po)']

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:

  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'))
  })

When run with tinyglobby, it returns 8 files: it does correctly exclude loc/en.po, but doesn't exclude loc/one/en.po and loc/two/en.po.

Image

Workarounds

For my use case, I found two workarounds that work with tinyglobby:

  1. If the files to ignore are always one folder deep, then this works: ['loc/*/!(en.po)']
  2. Using two patterns like this also works: ['loc/**/*.po', '!loc/**/en.po']

Full test file

Click to show full test file
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'))
  })
})

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingupstreamNeeds work upstream

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions