src/**/*.js が拾うファイル、ツールごとに違う問題
はじめに
src//.ts、!/.test.js、*.{js,ts}。設定ファイルを書いていると、こうした「glob」パターンを毎日のように目にします。
ただ、同じ src/**/*.js でも、ESLint では拾われるファイルが TypeScript の include では拾われない、ということが実際に起きます。この記事では、
glob とは何か(どこまでが標準で、どこからが拡張か)
Node.js でよく使われる glob(node-glob)・minimatch・brace-expansion がそれぞれ何をしているか
.gitignore / ESLint / tsconfig / Vite / Jest で挙動がどう違うか
を、手元で動かせるコードと実行結果つきでまとめます。
動作確認した環境
項目
バージョン
Node.js
v20.19.2
npm
11.21.1
glob
14.0.3
minimatch
11.0.3(makeRe の確認のみ 10.2.6)
brace-expansion
5.0.12
ESLint
10.14.0
TypeScript
7.0.2
Vite
8.3.5
Jest
30.5.2
git
2.47.3
globとは
glob は、* や ? を使ってファイルのパス名をまとめて指定するパターン記法です。Linux の man ページ glob(7) によると、起源は UNIX V6 の /etc/glob というプログラムで、まもなくシェルの組み込み機能になりました。
ls *.ts
POSIX1 で定められている基本の記号は次の3つです。
記号
意味
*
任意の文字列(空文字列を含む)。パス区切りの / にはマッチしない
?
任意の1文字(/ を除く)
[...]
括弧内のいずれか1文字。[!...] で否定、[a-z] で範囲
よく使う ** と {a,b} は、実はこの基本セットに含まれていません。
**(globstar): 0個以上のディレクトリにまたがってマッチする拡張です。bash でも shopt -s globstar を有効にしたときだけ使えます。
{a,b}(ブレース展開): bash のマニュアルでは、ファイル名展開とは別の「文字列を生成する仕組み」として説明されています。
先頭の !(否定パターン): POSIX の記法に出てくる ! は [!abc] のように括弧の中だけです。先頭につけるパターンは定められた記法ではありません。
つまり「glob」と一口に言っても、ツールごとに方言が生まれる余地があります。
Node.js においては、.gitignore に書く node_modules/ や .log、ESLint の設定の files: ["/.ts"]、tsconfig.json の "include": ["src//*"]、テストランナーが *.test.ts を探すときの指定も、すべて glob パターンです。どれも「このパターンに合うファイルはどれか」を判定していますが、それぞれのツールが採用する判定処理には差異があるのです。
この記事ではまず、そもそもglobパターンの判定処理のイメージを掴むために、手で確かめやすい glob・minimatch・brace-expansion というglobパターンを扱うライブラリを代表例として、取り上げます。
代表的な3つのライブラリで仕組みを見る
Node.js でよく使われる3つのパッケージ。次のような依存関係でもあります(npm view で確認)。
glob@14.0.3
└─ minimatch ^11.0.2
└─ brace-expansion ^5.0.12
パッケージ
やること
ディスクを見るか
glob
パターンに合うファイルをディレクトリをたどって探す
見る
minimatch
1つのパス文字列がパターンに合うかを判定する
見ない
brace-expansion
{a,b} や {1..3} を文字列の配列に展開する
見ない
処理の流れはおおむね次のとおりです。
brace-expansion が src/.{js,ts} を src/.js と src/*.ts に展開する
minimatch がそれぞれを / で区切り、区切りごとの照合器を作る
glob がディレクトリをたどりながら、各パスを minimatch で判定する
以下のコードは、空のディレクトリで順に実行すれば再現できます。
サンプルコードはGitHubで公開しています: ikushimayujiro/glob-demo
準備:デモ用のファイル構成
mkdir glob-demo && cd glob-demo
npm init -y
npm i glob@14.0.3 minimatch@11.0.3 minimatch10@npm:minimatch@10.2.6 brace-expansion@5.0.12
minimatch10 は、後で makeRe を見るために v10 系を別名で入れています。
!
minimatch 10.2.6 は、2026年10月31日に公開された GHSA-g3hv-75wg-m249(minimatch <11.0.1 が対象)に該当するため、インストール時に npm audit の警告が出ます。makeRe を確かめるためにわざと古い版を入れているので、このデモ以外では使わないでください。
00-setup.sh
#!/usr/bin/env bash
set -eu
rm -rf fixture && mkdir -p fixture/src/{deep/nested,.config,tests}
for f in src/a.js src/b.ts src/.hidden.js src/.config/c.js src/deep/nested/d.js \
src/app.test.js src/tests/e.js src/.config/f.test.js; do
echo "export const x = 1;" > "fixture/$f"
done
fixture/src/
├── .config/
│ ├── c.js
│ └── f.test.js
├── .hidden.js
├── tests/
│ └── e.js
├── a.js
├── app.test.js
├── b.ts
└── deep/nested/d.js
ドットで始まるファイル(.hidden.js)とディレクトリ(.config/)をわざと混ぜています。
glob:実際にファイルを探す
01-glob.mjs
import { glob } from 'glob'
const cwd = new URL('./fixture/', import.meta.url).pathname
const show = async (pattern, opts = {}) => {
const files = (await glob(pattern, { cwd, posix: true, ...opts })).sort()
console.log(${pattern} ${JSON.stringify(opts)}\n ->, files)
}
await show('src/*.js')
await show('src//*.js')
await show('src//*.js', { dot: true })
await show('src//*.{js,ts}')
await show('src//.js', { ignore: ['/.test.js', 'src/__tests__/'] })
src/*.js {}
-> [ 'src/a.js', 'src/app.test.js' ]
src//*.js {}
-> [ 'src/__tests__/e.js', 'src/a.js', 'src/app.test.js', 'src/deep/nested/d.js' ]
src//*.js {"dot":true}
-> [ 'src/.config/c.js', 'src/.config/f.test.js', 'src/.hidden.js',
'src/tests/e.js', 'src/a.js', 'src/app.test.js', 'src/deep/nested/d.js' ]
src//*.{js,ts}
-> [ 'src/__tests__/e.js', 'src/a.js', 'src/app.test.js', 'src/b.ts', 'src/deep/nested/d.js' ]
src//.js {"ignore":["/.test.js","src/__tests__/"]}
-> [ 'src/a.js', 'src/deep/nested/d.js' ]
(出力は読みやすいよう改行を詰めています)
ポイントは2つあります。
- は / をまたがないので、src/*.js は deep/nested/d.js を拾いません。
既定では、ドットで始まるファイルやディレクトリはマッチしません。dot: true を付けると拾います。README にも「パターン側で明示的に . を書けば、ドットファイルにマッチする」とあります。
minimatch:1つのパスを判定する
minimatch はファイルシステムを見ません。文字列とパターンを受け取って true / false を返すだけです。
02-minimatch.mjs
import { minimatch } from 'minimatch'
const cases = [
['src/a.js', 'src/*.js'],
['src/deep/nested/d.js', 'src/*.js'],
['src/deep/nested/d.js', 'src//*.js'],
['src/.hidden.js', 'src/*.js'],
['src/.hidden.js', 'src/*.js', { dot: true }],
['src/.config/c.js', 'src//*.js'],
['src/b.ts', 'src/*.{js,ts}'],
['src/b.ts', 'src/*.{js,ts}', { nobrace: true }],
['src/a.js', '!src/*.ts'],
['src/a.js', 'src/?.js'],
['src/a.js', 'src/[ab].js'],
]
for (const [path, pattern, opts] of cases) {
console.log(String(minimatch(path, pattern, opts)).padEnd(5), path.padEnd(22), pattern, opts ? JSON.stringify(opts) : '')
}
console.log('\nbraceExpand("src/.{js,ts}") ->', minimatch.braceExpand('src/.{js,ts}'))
true src/a.js src/*.js
false src/deep/nested/d.js src/*.js
true src/deep/nested/d.js src//*.js
false src/.hidden.js src/*.js
true src/.hidden.js src/*.js {"dot":true}
false src/.config/c.js src//*.js
true src/b.ts src/*.{js,ts}
false src/b.ts src/*.{js,ts} {"nobrace":true}
true src/a.js !src/*.ts
true src/a.js src/?.js
true src/a.js src/[ab].js
braceExpand("src/.{js,ts}") -> [ 'src/.js', 'src/*.ts' ]
** も既定ではドットで始まるディレクトリ(.config/)には入りません。
nobrace: true にするとブレース展開が行われず、{js,ts} は文字どおりの文字列として扱われます。
minimatch 単体では、先頭の ! は「このパターンに合わなければ true」という否定になります。src/a.js は src/*.ts に合わないので true です。
minimatch.braceExpand() の結果からわかるように、ブレースはマッチングの前に展開されて、複数のパターンになります。
makeRe:正規表現になる(v10まで)
minimatch v10 系までは、パターンを正規表現に変換して照合していました。makeRe() で中身を見られます。
03-makere.cjs
const mm10 = require('minimatch10') // minimatch@10.2.6
const mm11 = require('minimatch') // minimatch@11.0.3
for (const p of ['.js', 'src/*/.js', '.{js,ts}']) {
console.log(p.padEnd(14), '->', mm10.makeRe(p))
}
console.log('\nminimatch@11 に makeRe はある? ->', typeof mm11.makeRe, '/', typeof mm11.minimatch.makeRe)
.js -> /^(?!.)[^/]?.js$/
src/**/.js -> /^src(?:\/|\/(?:(?!(?:\/|^).).)?\/)(?!.)[^/]*?.js$/
.{js,ts} -> /^(?:(?!.)[^/]?.js|(?!.)[^/]*?.ts)$/
minimatch@11 に makeRe はある? -> undefined / undefined
正規表現を読むと、ここまでの挙動がそのまま書かれています。
[^/]?: は / 以外の文字列
(?!.):先頭がドットならマッチしない
*.{js,ts}:ブレース展開した2つのパターンを | でつないだもの
一方、2026年10月に出た minimatch v11 では、照合に正規表現を使わなくなりました。changelog によると、パスの区切りごとに @isaacs/nonrecursive-glob-match の Matcher で照合する方式に変わり、makeRe() は削除されています。README では、正規表現はバックトラックによる ReDoS(正規表現によるサービス拒否)を起こしうるため、という理由が説明されています。
ESLint 10.14.0 は minimatch ^10.2.6 に依存しているので、プロジェクトによっては v10 系と v11 系が両方入っていることもあります。
brace-expansion:文字列を展開する
04-brace.mjs
import { expand } from 'brace-expansion'
console.log(expand('file.{js,ts}'))
console.log(expand('img{1..3}.png'))
console.log(expand('{a,b}{x,y}'))
console.log(expand('src/{components,hooks}/**/*.{ts,tsx}'))
[ 'file.js', 'file.ts' ]
[ 'img1.png', 'img2.png', 'img3.png' ]
[ 'ax', 'ay', 'bx', 'by' ]
[
'src/components//*.ts',
'src/components//*.tsx',
'src/hooks//*.ts',
'src/hooks//*.tsx'
]
{a,b}{x,y} が4件になるように、ブレースを並べると展開数は掛け算で増えます。小さい規模で確かめてみます。
for (const n of [1, 2, 4, 8, 12, 16]) {
const r = expand('{a,b}'.repeat(n))
const totalChars = r.reduce((s, x) => s + x.length, 0)
console.log(n=${String(n).padStart(2)} 入力${String(n * 5).padStart(3)}文字 -> ${String(r.length).padStart(6)}件 (理論値 2^n=${2 ** n}) 合計${totalChars}文字)
}
n= 1 入力 5文字 -> 2件 (理論値 2^n=2) 合計2文字
n= 2 入力 10文字 -> 4件 (理論値 2^n=4) 合計8文字
n= 4 入力 20文字 -> 16件 (理論値 2^n=16) 合計64文字
n= 8 入力 40文字 -> 256件 (理論値 2^n=256) 合計2048文字
n=12 入力 60文字 -> 4096件 (理論値 2^n=4096) 合計49152文字
n=16 入力 80文字 -> 65536件 (理論値 2^n=65536) 合計1048576文字
入力80文字で、出力は65,536件・合計約100万文字になりました。件数は 2^n、合計文字数は n × 2^n で増えます。
!
ここでは n=16 までにとどめています。これより大きい値を試すとメモリを大量に使うので、手元でも同じくらいの規模にとどめてください。
brace-expansion には、この増え方を抑える上限オプションがあります(README)。
max:展開する件数の上限(既定 100,000)
maxLength:展開結果の合計文字数の上限(既定 4,000,000)。5.0.8 で追加
console.log(expand('{1..100}', { max: 10 }).length) // 10
const capped = expand('{a,b}'.repeat(12), { maxLength: 1000 })
console.log(capped.length, capped.reduce((s, x) => s + x.length, 0)) // 83 996
上限に達すると、エラーにはならず結果が途中で打ち切られます。
いざ、ツールごとの違い
それではいよいよ、本題。
同じ fixture/ に対して、各ツールが実際にどのファイルを拾うかを試しました(デモの 05-tools.sh)。
ツール
設定項目
使用ライブラリ
ドットファイル
**
ブレース {a,b}
先頭 !
git
.gitignore
独自(fnmatch(3) 準拠の記述)
- でマッチする
対応
非対応(文字どおり扱う)
前のパターンで除外したものを再び含める。ただし親ディレクトリが除外されていると戻せない
ESLint(flat config)
files / ignores
minimatch
マッチする(ignores はドキュメントに明記、files は実測)
対応
対応
ignores 内で除外の取り消しに使える
TypeScript
include / exclude
独自(* ? **/ のみ記載)
ワイルドカードではマッチしない。src/.config/* のように明示すれば拾う
**/ 対応
非対応(該当なしでエラー)
記載なし
Vite
import.meta.glob()
tinyglobby
既定ではマッチしない。exhaustive:true で指定
対応
対応
除外パターンとして使える
Jest
testMatch
micromatch
マッチする
対応
対応
除外。配列の順序が効く(否定は後ろに置く)
実測の出力(抜粋)は次のとおりです。
=== git (.gitignore)
-- .gitignore: src/*.js
src/a.js ignored
src/.hidden.js ignored
src/deep/nested/d.js not-ignored
-- .gitignore: src/*.{js,ts}
src/a.js not-ignored
src/b.ts not-ignored
=== TypeScript (tsconfig include) Version 7.0.2
-- include: ["src//*"]
src/__tests__/e.js
src/a.js
src/app.test.js
src/b.ts
src/deep/nested/d.js
-- include: ["src//*.{js,ts}"]
error TS18003: No inputs were found in config file 'tsconfig.json'. ...
-- include: ["src/.config/*"]
src/.config/c.js
src/.config/f.test.js
=== ESLint (flat config files: ['src/**/*.{js,ts}']) v10.14.0
src/.config/c.js
src/.config/f.test.js
src/.hidden.js
src/tests/e.js
src/a.js
src/app.test.js
src/b.ts
src/deep/nested/d.js
=== Jest (testMatch) 30.5.2
-- default testMatch
src/.config/f.test.js
src/tests/e.js
src/app.test.js
=== Vite (import.meta.glob) vite/8.3.5
plain ./src/tests/e.js, ./src/a.js, ./src/app.test.js, ./src/deep/nested/d.js
brace ./src/tests/e.js, ./src/a.js, ./src/app.test.js, ./src/b.ts, ./src/deep/nested/d.js
negated ./src/tests/e.js, ./src/a.js, ./src/deep/nested/d.js
exhaustive ./src/.config/c.js, ./src/.config/f.test.js, ./src/.hidden.js, ...
読み取れることをまとめます。
ドットファイル: ESLint と Jest は .config/ の中まで拾いますが、TypeScript と Vite は拾いません。.storybook/ のような設定ディレクトリが lint やテストの対象に入る・入らないの差は、ここから来ます。
ブレース展開: .gitignore と tsconfig の include では使えません。tsconfig で "src/**/*.{ts,tsx}" と書くと、ファイルが1つも見つからずエラーになりました。
否定: .gitignore の ! は「再び含める」、Jest の testMatch では「除外」で、しかも順序が効きます。同じ記号でも意味が違います。
まとめ:なぜツールごとに方言が生まれるのか
ここまで見てきた違いは、大きく3つの原因に分けられます。
1. パターン判別のライブラリが違う
git と TypeScript は独自の実装、ESLint は minimatch、Vite は tinyglobby、Jest は micromatch を使っています。
2. 同じライブラリでも、渡すオプションが違う
ESLint は minimatch を使っていますが、ドットファイルにもマッチします。minimatch は既定ではドットファイルを拾いませんが、ESLint が dot: true を付けて呼び出しているからです。
3. 設定ファイル側の文法が違う
先頭の ! の意味は、ライブラリだけでなく、各ツールの設定ファイルの文法にもよります。.gitignore では「前のパターンで除外したものを再び含める」、Jest の testMatch では「除外」で、しかも配列の順序が効きます。
同じ glob パターンでも、どのライブラリを、どのオプションで、どんな文法の中で使っているかによって結果が変わるということですね。迷ったら、各ツールのファイル一覧を出すコマンド(git check-ignore、tsc --listFilesOnly、eslint --inspect-config、jest --listTests)で実際の結果を確認するのが確実です。
おまけ:glob と minimatch を書いた人
最後に少し寄り道です。この記事で何度も出てきた glob と minimatch は、npm registry の author 欄を見ると同じ人の名前になっています。
npm view glob author # Isaac Z. Schlueter <i@izs.me> (blog.izs.me)
npm view minimatch author # Isaac Z. Schlueter <i@izs.me> (blog.izs.me)
Isaac Z. Schlueter さん(GitHub では isaacs)は、npm を作った人です。本人の GitHub プロフィールには「npm inventor, founder npm, Inc. Former Node BDFL.」と書かれています。
author が Isaac Z. Schlueter さんになっているパッケージには、ほかにも次のようなものがあります。
パッケージ
用途
週間ダウンロード数(2026-10-23〜10-29)
minimatch
glob パターンの照合
951,240,180
lru-cache
LRU キャッシュ
789,165,914
glob
ファイル検索
536,547,167
rimraf
rm -rf 相当の削除
214,603,009
tar
tar アーカイブの読み書き
119,327,291
(npm の downloads API で取得)
package.json の ^5.0.8 のようなバージョン範囲を解釈する semver も、LICENSE に「Copyright (c) Isaac Z. Schlueter and Contributors」とあります(現在の author 欄は GitHub Inc.、リポジトリは npm organization の管理です)。
普段使っている npm を作った人が、node_modules によく入っている小さな部品もたくさん書いている、ということになります。npm CLI 自身が、glob・minimatch・semver・tar に依存しているのを見ると、開発の中で自身が必要になったものを、片っ端から生み出していった結果なのかもしれません。
参考資料
glob(7) - Linux manual page: man7.org
Bash Reference Manual - Brace Expansion: gnu.org
Bash Reference Manual - The Shopt Builtin(globstar / dotglob): gnu.org
isaacs/node-glob README: isaacs/node-glob
isaacs/minimatch README / changelog: isaacs/minimatch , github.com/isaacs/minimatch/blob/main/changelog.md
juliangruber/brace-expansion README: juliangruber/brace-expansion
gitignore Documentation: git-scm.com
ESLint - Configuration Files: eslint.org
ESLint v10.14.0 リリースノート: github.com/eslint/eslint/releases/tag/v10.14.0
TSConfig Reference - include: typescriptlang.org
Vite - Glob Import: vite.dev
tinyglobby documentation: superchupu.dev
Jest - testMatch: jestjs.io
脚注
POSIX(Portable Operating System Interface)は、UNIX 系 OS の間でアプリケーションを移植しやすくするための標準規格です。最新版の POSIX.1-2024 は IEEE Std 1003.1-2024 と The Open Group の Base Specifications Issue 8 を兼ねていて、OS のインターフェースに加えてシェルや共通コマンドも定めています。glob の記号は、シェルの章の「2.14 Pattern Matching Notation」で規定されています。出典:The Open Group Base Specifications Issue 8、同 2.14 Pattern Matching Notation、IEEE 1003.1-2024 ↩︎
Mentions
src/**/*.js が拾うファイル、ツールごとに違う問題 はじめに src//.ts、!/.test.js、*.{js,ts}。設定ファイルを書いていると、こうした「glob」パターンを毎日のように目にします。 ただ、同じ src/**/*.js でも、ESLint では拾われるファイルが TypeScript の include では拾われない、ということが実際に起きます。この記事では、 glob とは何か(どこまでが標準で、どこからが拡張か) Node.js でよく使われる glob(node-glob)・minimatch・brace-expansion がそれぞれ何をしているか .gitignore / ESLint / tsconfig / Vite / Jest で挙動がどう違うか を、手元で動かせるコードと実行結果つきでまとめます。 動作確認した環境 項目 バージョン Node.js v20.19.2 npm 11.21.1 glob 14.0.3 minimatch 11.0.3(makeRe の確認のみ 10.2.6) brace-expansion 5.0.12 ESLint 10.14.0 TypeScript 7.0.2 Vite 8.3.5 Jest 30.5.2 git 2.47.3 globとは glob は、* や ? を使ってファイルのパス名をまとめて指定するパターン記法です。Linux の man ページ glob(7) によると、起源は UNIX V6 の /etc/glob というプログラムで、まもなくシェルの組み込み機能になりました。 ls *.ts POSIX1 で定められている基本の記号は次の3つです。 記号 意味 * 任意の文字列(空文字列を含む)。パス区切りの / にはマッチしない ? 任意の1文字(/ を除く) [...] 括弧内のいずれか1文字。[!...] で否定、[a-z] で範囲 よく使う ** と {a,b} は、実はこの基本セットに含まれていません。 **(globstar): 0個以上のディレクトリにまたがってマッチする拡張です。bash でも shopt -s globstar を有効にしたときだけ使えます。 {a,b}(ブレース展開): bash のマニュアルでは、ファイル名展開とは別の「文字列を生成する仕組み」として説明されています。 先頭の !(否定パターン): POSIX の記法に出てくる ! は [!abc] のように括弧の中だけです。先頭につけるパターンは定められた記法ではありません。 つまり「glob」と一口に言っても、ツールごとに方言が生まれる余地があります。 Node.js においては、.gitignore に書く node_modules/ や .log、ESLint の設定の files: ["/.ts"]、tsconfig.json の "include": ["src//*"]、テストランナーが *.test.ts を探すときの指定も、すべて glob パターンです。どれも「このパターンに合うファイルはどれか」を判定していますが、それぞれのツールが採用する判定処理には差異があるのです。 この記事ではまず、そもそもglobパターンの判定処理のイメージを掴むために、手で確かめやすい glob・minimatch・brace-expansion というglobパターンを扱うライブラリを代表例として、取り上げます。 代表的な3つのライブラリで仕組みを見る Node.js でよく使われる3つのパッケージ。次のような依存関係でもあります(npm view で確認)。 glob@14.0.3 └─ minimatch ^11.0.2 └─ brace-expansion ^5.0.12 パッケージ やること ディスクを見るか glob パターンに合うファイルをディレクトリをたどって探す 見る minimatch 1つのパス文字列がパターンに合うかを判定する 見ない brace-expansion {a,b} や {1..3} を文字列の配列に展開する 見ない 処理の流れはおおむね次のとおりです。 brace-expansion が src/.{js,ts} を src/.js と src/*.ts に展開する minimatch がそれぞれを / で区切り、区切りごとの照合器を作る glob がディレクトリをたどりながら、各パスを minimatch で判定する 以下のコードは、空のディレクトリで順に実行すれば再現できます。 サンプルコードはGitHubで公開しています: ikushimayujiro/glob-demo 準備:デモ用のファイル構成 mkdir glob-demo && cd glob-demo npm init -y npm i glob@14.0.3 minimatch@11.0.3 minimatch10@npm:minimatch@10.2.6 brace-expansion@5.0.12 minimatch10 は、後で makeRe を見るために v10 系を別名で入れています。 ! minimatch 10.2.6 は、2026年10月31日に公開された GHSA-g3hv-75wg-m249(minimatch <11.0.1 が対象)に該当するため、インストール時に npm audit の警告が出ます。makeRe を確かめるためにわざと古い版を入れているので、このデモ以外では使わないでください。 00-setup.sh #!/usr/bin/env bash set -eu rm -rf fixture && mkdir -p fixture/src/{deep/nested,.config,tests} for f in src/a.js src/b.ts src/.hidden.js src/.config/c.js src/deep/nested/d.js \ src/app.test.js src/tests/e.js src/.config/f.test.js; do echo "export const x = 1;" > "fixture/$f" done fixture/src/ ├── .config/ │ ├── c.js │ └── f.test.js ├── .hidden.js ├── tests/ │ └── e.js ├── a.js ├── app.test.js ├── b.ts └── deep/nested/d.js ドットで始まるファイル(.hidden.js)とディレクトリ(.config/)をわざと混ぜています。 glob:実際にファイルを探す 01-glob.mjs import { glob } from 'glob' const cwd = new URL('./fixture/', import.meta.url).pathname const show = async (pattern, opts = {}) => { const files = (await glob(pattern, { cwd, posix: true, ...opts })).sort() console.log(${pattern} ${JSON.stringify(opts)}\n ->, files) } await show('src/*.js') await show('src//*.js') await show('src//*.js', { dot: true }) await show('src//*.{js,ts}') await show('src//.js', { ignore: ['/.test.js', 'src/__tests__/'] }) src/*.js {} -> [ 'src/a.js', 'src/app.test.js' ] src//*.js {} -> [ 'src/__tests__/e.js', 'src/a.js', 'src/app.test.js', 'src/deep/nested/d.js' ] src//*.js {"dot":true} -> [ 'src/.config/c.js', 'src/.config/f.test.js', 'src/.hidden.js', 'src/tests/e.js', 'src/a.js', 'src/app.test.js', 'src/deep/nested/d.js' ] src//*.{js,ts} -> [ 'src/__tests__/e.js', 'src/a.js', 'src/app.test.js', 'src/b.ts', 'src/deep/nested/d.js' ] src//.js {"ignore":["/.test.js","src/__tests__/"]} -> [ 'src/a.js', 'src/deep/nested/d.js' ] (出力は読みやすいよう改行を詰めています) ポイントは2つあります。 - は / をまたがないので、src/*.js は deep/nested/d.js を拾いません。 既定では、ドットで始まるファイルやディレクトリはマッチしません。dot: true を付けると拾います。README にも「パターン側で明示的に . を書けば、ドットファイルにマッチする」とあります。 minimatch:1つのパスを判定する minimatch はファイルシステムを見ません。文字列とパターンを受け取って true / false を返すだけです。 02-minimatch.mjs import { minimatch } from 'minimatch' const cases = [ ['src/a.js', 'src/*.js'], ['src/deep/nested/d.js', 'src/*.js'], ['src/deep/nested/d.js', 'src//*.js'], ['src/.hidden.js', 'src/*.js'], ['src/.hidden.js', 'src/*.js', { dot: true }], ['src/.config/c.js', 'src//*.js'], ['src/b.ts', 'src/*.{js,ts}'], ['src/b.ts', 'src/*.{js,ts}', { nobrace: true }], ['src/a.js', '!src/*.ts'], ['src/a.js', 'src/?.js'], ['src/a.js', 'src/[ab].js'], ] for (const [path, pattern, opts] of cases) { console.log(String(minimatch(path, pattern, opts)).padEnd(5), path.padEnd(22), pattern, opts ? JSON.stringify(opts) : '') } console.log('\nbraceExpand("src/.{js,ts}") ->', minimatch.braceExpand('src/.{js,ts}')) true src/a.js src/*.js false src/deep/nested/d.js src/*.js true src/deep/nested/d.js src//*.js false src/.hidden.js src/*.js true src/.hidden.js src/*.js {"dot":true} false src/.config/c.js src//*.js true src/b.ts src/*.{js,ts} false src/b.ts src/*.{js,ts} {"nobrace":true} true src/a.js !src/*.ts true src/a.js src/?.js true src/a.js src/[ab].js braceExpand("src/.{js,ts}") -> [ 'src/.js', 'src/*.ts' ] ** も既定ではドットで始まるディレクトリ(.config/)には入りません。 nobrace: true にするとブレース展開が行われず、{js,ts} は文字どおりの文字列として扱われます。 minimatch 単体では、先頭の ! は「このパターンに合わなければ true」という否定になります。src/a.js は src/*.ts に合わないので true です。 minimatch.braceExpand() の結果からわかるように、ブレースはマッチングの前に展開されて、複数のパターンになります。 makeRe:正規表現になる(v10まで) minimatch v10 系までは、パターンを正規表現に変換して照合していました。makeRe() で中身を見られます。 03-makere.cjs const mm10 = require('minimatch10') // minimatch@10.2.6 const mm11 = require('minimatch') // minimatch@11.0.3 for (const p of ['.js', 'src/*/.js', '.{js,ts}']) { console.log(p.padEnd(14), '->', mm10.makeRe(p)) } console.log('\nminimatch@11 に makeRe はある? ->', typeof mm11.makeRe, '/', typeof mm11.minimatch.makeRe) .js -> /^(?!.)[^/]?.js$/ src/**/.js -> /^src(?:\/|\/(?:(?!(?:\/|^).).)?\/)(?!.)[^/]*?.js$/ .{js,ts} -> /^(?:(?!.)[^/]?.js|(?!.)[^/]*?.ts)$/ minimatch@11 に makeRe はある? -> undefined / undefined 正規表現を読むと、ここまでの挙動がそのまま書かれています。 [^/]?: は / 以外の文字列 (?!.):先頭がドットならマッチしない *.{js,ts}:ブレース展開した2つのパターンを | でつないだもの 一方、2026年10月に出た minimatch v11 では、照合に正規表現を使わなくなりました。changelog によると、パスの区切りごとに @isaacs/nonrecursive-glob-match の Matcher で照合する方式に変わり、makeRe() は削除されています。README では、正規表現はバックトラックによる ReDoS(正規表現によるサービス拒否)を起こしうるため、という理由が説明されています。 ESLint 10.14.0 は minimatch ^10.2.6 に依存しているので、プロジェクトによっては v10 系と v11 系が両方入っていることもあります。 brace-expansion:文字列を展開する 04-brace.mjs import { expand } from 'brace-expansion' console.log(expand('file.{js,ts}')) console.log(expand('img{1..3}.png')) console.log(expand('{a,b}{x,y}')) console.log(expand('src/{components,hooks}/**/*.{ts,tsx}')) [ 'file.js', 'file.ts' ] [ 'img1.png', 'img2.png', 'img3.png' ] [ 'ax', 'ay', 'bx', 'by' ] [ 'src/components//*.ts', 'src/components//*.tsx', 'src/hooks//*.ts', 'src/hooks//*.tsx' ] {a,b}{x,y} が4件になるように、ブレースを並べると展開数は掛け算で増えます。小さい規模で確かめてみます。 for (const n of [1, 2, 4, 8, 12, 16]) { const r = expand('{a,b}'.repeat(n)) const totalChars = r.reduce((s, x) => s + x.length, 0) console.log(n=${String(n).padStart(2)} 入力${String(n * 5).padStart(3)}文字 -> ${String(r.length).padStart(6)}件 (理論値 2^n=${2 ** n}) 合計${totalChars}文字) } n= 1 入力 5文字 -> 2件 (理論値 2^n=2) 合計2文字 n= 2 入力 10文字 -> 4件 (理論値 2^n=4) 合計8文字 n= 4 入力 20文字 -> 16件 (理論値 2^n=16) 合計64文字 n= 8 入力 40文字 -> 256件 (理論値 2^n=256) 合計2048文字 n=12 入力 60文字 -> 4096件 (理論値 2^n=4096) 合計49152文字 n=16 入力 80文字 -> 65536件 (理論値 2^n=65536) 合計1048576文字 入力80文字で、出力は65,536件・合計約100万文字になりました。件数は 2^n、合計文字数は n × 2^n で増えます。 ! ここでは n=16 までにとどめています。これより大きい値を試すとメモリを大量に使うので、手元でも同じくらいの規模にとどめてください。 brace-expansion には、この増え方を抑える上限オプションがあります(README)。 max:展開する件数の上限(既定 100,000) maxLength:展開結果の合計文字数の上限(既定 4,000,000)。5.0.8 で追加 console.log(expand('{1..100}', { max: 10 }).length) // 10 const capped = expand('{a,b}'.repeat(12), { maxLength: 1000 }) console.log(capped.length, capped.reduce((s, x) => s + x.length, 0)) // 83 996 上限に達すると、エラーにはならず結果が途中で打ち切られます。 いざ、ツールごとの違い それではいよいよ、本題。 同じ fixture/ に対して、各ツールが実際にどのファイルを拾うかを試しました(デモの 05-tools.sh)。 ツール 設定項目 使用ライブラリ ドットファイル ** ブレース {a,b} 先頭 ! git .gitignore 独自(fnmatch(3) 準拠の記述) - でマッチする 対応 非対応(文字どおり扱う) 前のパターンで除外したものを再び含める。ただし親ディレクトリが除外されていると戻せない ESLint(flat config) files / ignores minimatch マッチする(ignores はドキュメントに明記、files は実測) 対応 対応 ignores 内で除外の取り消しに使える TypeScript include / exclude 独自(* ? **/ のみ記載) ワイルドカードではマッチしない。src/.config/* のように明示すれば拾う **/ 対応 非対応(該当なしでエラー) 記載なし Vite import.meta.glob() tinyglobby 既定ではマッチしない。exhaustive:true で指定 対応 対応 除外パターンとして使える Jest testMatch micromatch マッチする 対応 対応 除外。配列の順序が効く(否定は後ろに置く) 実測の出力(抜粋)は次のとおりです。 === git (.gitignore) -- .gitignore: src/*.js src/a.js ignored src/.hidden.js ignored src/deep/nested/d.js not-ignored -- .gitignore: src/*.{js,ts} src/a.js not-ignored src/b.ts not-ignored === TypeScript (tsconfig include) Version 7.0.2 -- include: ["src//*"] src/__tests__/e.js src/a.js src/app.test.js src/b.ts src/deep/nested/d.js -- include: ["src//*.{js,ts}"] error TS18003: No inputs were found in config file 'tsconfig.json'. ... -- include: ["src/.config/*"] src/.config/c.js src/.config/f.test.js === ESLint (flat config files: ['src/**/*.{js,ts}']) v10.14.0 src/.config/c.js src/.config/f.test.js src/.hidden.js src/tests/e.js src/a.js src/app.test.js src/b.ts src/deep/nested/d.js === Jest (testMatch) 30.5.2 -- default testMatch src/.config/f.test.js src/tests/e.js src/app.test.js === Vite (import.meta.glob) vite/8.3.5 plain ./src/tests/e.js, ./src/a.js, ./src/app.test.js, ./src/deep/nested/d.js brace ./src/tests/e.js, ./src/a.js, ./src/app.test.js, ./src/b.ts, ./src/deep/nested/d.js negated ./src/tests/e.js, ./src/a.js, ./src/deep/nested/d.js exhaustive ./src/.config/c.js, ./src/.config/f.test.js, ./src/.hidden.js, ... 読み取れることをまとめます。 ドットファイル: ESLint と Jest は .config/ の中まで拾いますが、TypeScript と Vite は拾いません。.storybook/ のような設定ディレクトリが lint やテストの対象に入る・入らないの差は、ここから来ます。 ブレース展開: .gitignore と tsconfig の include では使えません。tsconfig で "src/**/*.{ts,tsx}" と書くと、ファイルが1つも見つからずエラーになりました。 否定: .gitignore の ! は「再び含める」、Jest の testMatch では「除外」で、しかも順序が効きます。同じ記号でも意味が違います。 まとめ:なぜツールごとに方言が生まれるのか ここまで見てきた違いは、大きく3つの原因に分けられます。 1. パターン判別のライブラリが違う git と TypeScript は独自の実装、ESLint は minimatch、Vite は tinyglobby、Jest は micromatch を使っています。 2. 同じライブラリでも、渡すオプションが違う ESLint は minimatch を使っていますが、ドットファイルにもマッチします。minimatch は既定ではドットファイルを拾いませんが、ESLint が dot: true を付けて呼び出しているからです。 3. 設定ファイル側の文法が違う 先頭の ! の意味は、ライブラリだけでなく、各ツールの設定ファイルの文法にもよります。.gitignore では「前のパターンで除外したものを再び含める」、Jest の testMatch では「除外」で、しかも配列の順序が効きます。 同じ glob パターンでも、どのライブラリを、どのオプションで、どんな文法の中で使っているかによって結果が変わるということですね。迷ったら、各ツールのファイル一覧を出すコマンド(git check-ignore、tsc --listFilesOnly、eslint --inspect-config、jest --listTests)で実際の結果を確認するのが確実です。 おまけ:glob と minimatch を書いた人 最後に少し寄り道です。この記事で何度も出てきた glob と minimatch は、npm registry の author 欄を見ると同じ人の名前になっています。 npm view glob author # Isaac Z. Schlueter <i@izs.me> (blog.izs.me) npm view minimatch author # Isaac Z. Schlueter <i@izs.me> (blog.izs.me) Isaac Z. Schlueter さん(GitHub では isaacs)は、npm を作った人です。本人の GitHub プロフィールには「npm inventor, founder npm, Inc. Former Node BDFL.」と書かれています。 author が Isaac Z. Schlueter さんになっているパッケージには、ほかにも次のようなものがあります。 パッケージ 用途 週間ダウンロード数(2026-10-23〜10-29) minimatch glob パターンの照合 951,240,180 lru-cache LRU キャッシュ 789,165,914 glob ファイル検索 536,547,167 rimraf rm -rf 相当の削除 214,603,009 tar tar アーカイブの読み書き 119,327,291 (npm の downloads API で取得) package.json の ^5.0.8 のようなバージョン範囲を解釈する semver も、LICENSE に「Copyright (c) Isaac Z. Schlueter and Contributors」とあります(現在の author 欄は GitHub Inc.、リポジトリは npm organization の管理です)。 普段使っている npm を作った人が、node_modules によく入っている小さな部品もたくさん書いている、ということになります。npm CLI 自身が、glob・minimatch・semver・tar に依存しているのを見ると、開発の中で自身が必要になったものを、片っ端から生み出していった結果なのかもしれません。 参考資料 glob(7) - Linux manual page: man7.org Bash Reference Manual - Brace Expansion: gnu.org Bash Reference Manual - The Shopt Builtin(globstar / dotglob): gnu.org isaacs/node-glob README: isaacs/node-glob isaacs/minimatch README / changelog: isaacs/minimatch , github.com/isaacs/minimatch/blob/main/changelog.md juliangruber/brace-expansion README: juliangruber/brace-expansion gitignore Documentation: git-scm.com ESLint - Configuration Files: eslint.org ESLint v10.14.0 リリースノート: github.com/eslint/eslint/releases/tag/v10.14.0 TSConfig Reference - include: typescriptlang.org Vite - Glob Import: vite.dev tinyglobby documentation: superchupu.dev Jest - testMatch: jestjs.io 脚注 POSIX(Portable Operating System Interface)は、UNIX 系 OS の間でアプリケーションを移植しやすくするための標準規格です。最新版の POSIX.1-2024 は IEEE Std 1003.1-2024 と The Open Group の Base Specifications Issue 8 を兼ねていて、OS のインターフェースに加えてシェルや共通コマンドも定めています。glob の記号は、シェルの章の「2.14 Pattern Matching Notation」で規定されています。出典:The Open Group Base Specifications Issue 8、同 2.14 Pattern Matching Notation、IEEE 1003.1-2024 ↩︎