node-semver

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

1post · 30d
Nov 40 posts
  1. 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 ↩︎

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

Scan pending.

README badges

Embed a badge in your README or website.

Live mentions · last 30 days
npm/node-semver mentions in the last 30 days