I spent twenty minutes last week convinced my tsconfig was broken.
The pattern was src/**. It works in a .gitignore. It works in every glob library I have ever installed. In tsconfig.json it does nothing at all, and the reason turned out to be that TypeScript rejects it outright:
File specification cannot end in a recursive directory wildcard ('**'): 'src/**'
Not "matched nothing". Rejected. The diagnostic is right there in the output if you go looking, and I had not gone looking, because it had never occurred to me that a glob could be a syntax error in one file and completely ordinary in the next one.
A glob is not one language
Count the places you write a glob pattern in a normal week. I get six without trying:
.gitignore-
tsconfig.json, inincludeandexclude - an ESLint config, which is minimatch underneath
- a Vite or rollup config, which is picomatch
- lint-staged, which is micromatch
- Node itself,
path.matchesGlobandfs.glob
Those are six dialects that share a syntax and disagree about what it means. Nothing warns you when you cross from one to another, because each tool answers only for itself, and none of them has any reason to tell you that the other five read your pattern differently.
So I took 25 patterns straight out of config files I actually had lying around, 20 ordinary paths, and asked all six engines every combination. 500 questions, no sampling, same answers on a re-run.
- 19 of the 25 patterns are not read the same way by all six.
-
5 of them are refused outright by one engine and accepted by the other five. Every one of those five ends in
**. - Of the 400 pattern and path pairs drawn from the patterns that every engine accepts, 48 come back with different answers.
- 6 patterns are read identically by all six.
Six out of twenty-five. If you have ever written a pattern meant to mean the same thing in your .gitignore and your tsconfig and your bundler config, you had roughly a one in four chance of getting one.
Where the disagreements actually live
Two rules cause most of it.
Dotfiles. src/** matches src/.hidden/file.ts under git. It does not match under minimatch, picomatch, micromatch or Node, because those four skip dotfiles unless you ask for them and git does not. This is the one that bites, because the file you were trying to ignore is usually the dotfile.
Braces. **/*.{ts,tsx} expands in the three npm matchers and in Node. git does not expand braces at all. Neither does tsconfig. So a pattern that is unremarkable in a Vite config quietly matches nothing in a .gitignore, and nothing anywhere tells you.
Put those together on one line and you get the case that started this:
src/** vs src/.hidden/file.ts
git match
micromatch no match
minimatch no match
node no match
picomatch no match
typescript rejected: cannot end in a recursive directory wildcard
Three different answers. One pattern, one path.
Asking all six at once
That output is a command, not a screenshot. I packaged the harness up so it is one line:
npx glob-consensus 'src/**' src/.hidden/file.ts
It exits 0 when every engine agreed and 1 when they did not, so it works in a script. There is a library underneath it if you want the report as data.
It implements no matching logic of its own, deliberately. Every verdict comes from an engine that already exists and that somebody already maintains, which means the answer to "is this correct" is never a matter of my opinion. Two of the six answer about a filesystem rather than about a string, so those two get asked by building a temporary tree of empty files and throwing it away afterwards.
One caveat worth knowing before you read the output: tsconfig filters by file extension as well as by pattern, so a .log file is never included by a tsconfig no matter what you write. That is tsconfig being tsconfig rather than a glob dialect difference, and the report shows it as it is rather than trying to be clever about it.
This is the second time I have built a thing whose whole job is to ask several implementations the same question and print where they disagree. The first was schema-parity, which does it for JSON Schema validators, and the shape keeps being useful for the same reason: the interesting bug is almost never inside one implementation, it is in the gap between two of them that everybody assumed were the same.
It does not tell you which engine is right, because there is no right. They are all correct about themselves. It just stops you spending twenty minutes blaming your tsconfig.
Top comments (0)