zudo-tauri

Type to search...

to open search from anywhere

ウォッチャーループの防止

作成2026年3月29日更新2026年3月29日Takeshi Takatsudo

ビルド出力が監視対象ディレクトリに書き込まれる際の無限リビルドループの回避方法

ウォッチャーループの防止

開発サーバーで最もフラストレーションの溜まる問題の一つが、無限リビルドループである。ビルドがウォッチャーの監視対象ディレクトリにファイルを書き込み、それが新たなビルドをトリガーし、さらにファイルが書き込まれ、再びビルドがトリガーされ…という繰り返しが発生する。CPU 使用率が100%に跳ね上がり、ターミナルがビルド出力で埋め尽くされる。

問題

この問題は、ビルドが生成するコンテンツがソースコンテンツと同じ場所に配置される場合に発生する。典型的な例を以下に示す。

content/
  docs/
    getting-started.md    <-- ソース(手書き)
    api-reference.md      <-- ソース(手書き)
    claude/               <-- 生成物(ビルドが自動作成)
      index.md
      commands.md

ウォッチャーが content/docs/ を監視していて、ビルドが content/docs/claude/ にファイルを生成する場合、無限ループが発生する。

graph LR A[ファイル変更を検出] --> B[ビルド実行] B --> C[content/docs/claude/ に書き込み] C --> D[ウォッチャーが変更を検出] D --> A

解決策:生成パスのフィルタリング

修正方法はシンプルである。ウォッチャーのコールバック内で、変更されたファイルが生成ディレクトリ内のものかどうかを確認し、該当する場合はリビルドをスキップする。

import chokidar from 'chokidar';

const GENERATED_DIRS = [
  'content/docs/claude/',
  'content/docs/auto-generated/',
];

function isGeneratedPath(filePath) {
  return GENERATED_DIRS.some(dir => filePath.includes(dir));
}

const watcher = chokidar.watch('./content', {
  ignoreInitial: true,
});

watcher.on('all', (event, filePath) => {
  // Skip changes to generated content
  if (isGeneratedPath(filePath)) {
    return;
  }

  // Proceed with rebuild
  triggerRebuild();
});

パスマッチングのパターン

単純な文字列マッチング

ほとんどの場合、パスに生成ディレクトリが含まれているかどうかを確認するだけで十分である。

function isGeneratedPath(filePath) {
  // Normalize to forward slashes for cross-platform
  const normalized = filePath.replace(/\\/g, '/');
  return normalized.includes('content/docs/claude/');
}

複数の生成ディレクトリ

生成ディレクトリが複数ある場合は、配列を使用する。

const GENERATED_PATTERNS = [
  'content/docs/claude/',
  'dist/',
  '.cache/',
  'node_modules/',
];

function isGeneratedPath(filePath) {
  const normalized = filePath.replace(/\\/g, '/');
  return GENERATED_PATTERNS.some(pattern => normalized.includes(pattern));
}

chokidar の ignored オプションの使用

chokidar 自体にこれらのパスを無視するよう設定することもできる。ウォッチャーがファイルシステムイベントを登録すらしないため、わずかに効率的である。

const watcher = chokidar.watch('./content', {
  ignoreInitial: true,
  ignored: [
    '**/content/docs/claude/**',
    '**/node_modules/**',
    '**/.git/**',
  ],
});

💡 Tip

コールバック内でのフィルタリングよりも、chokidar の ignored オプションを優先すること。OS レベルでファイルシステムイベントのノイズを削減できるため、大規模プロジェクトではパフォーマンスが向上する。

実際の使用例

以下は、Claude Code のリソースからコンテンツの一部を生成しつつ、手動でのコンテンツ変更も監視するドキュメントサイトのウォッチャー設定例である。

import chokidar from 'chokidar';

const WATCH_DIRS = ['./content', './src'];
const IGNORE_PATTERNS = [
  // Generated documentation from Claude Code resources
  'content/docs/claude/',
  // Build output
  'dist/',
  '.astro/',
  // Dependencies
  'node_modules/',
];

function shouldRebuild(filePath) {
  const normalized = filePath.replace(/\\/g, '/');
  return !IGNORE_PATTERNS.some(pattern => normalized.includes(pattern));
}

let debounceTimer = null;

const watcher = chokidar.watch(WATCH_DIRS, {
  ignoreInitial: true,
  ignored: [
    '**/node_modules/**',
    '**/.git/**',
  ],
});

watcher.on('all', (event, filePath) => {
  if (!shouldRebuild(filePath)) {
    // Log skipped paths during development to verify filtering works
    console.log(`[watcher] skipping generated: ${filePath}`);
    return;
  }

  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    console.log(`[watcher] rebuilding due to: ${filePath}`);
    triggerRebuild();
  }, 200);
});

ループのデバッグ

無限ループが疑われる場合、ログを追加してどのファイルがリビルドをトリガーしているかを特定する。

watcher.on('all', (event, filePath) => {
  console.log(`[watcher] ${event}: ${filePath}`);
  // ... rest of handler
});

出力で繰り返されるパターンを探す。同じ生成ファイルが何度も出現している場合、ループの原因が特定できたことになる。

⚠️ Warning

ウォッチャーの無視設定の根拠として .gitignore を使用しないこと。この2つの関心事は関連しているが異なるものである。gitignore されているファイルを監視したい場合もあるし(ローカル設定ファイルなど)、git で追跡されているファイルを無視したい場合もある(コミットされた生成ドキュメントなど)。

まとめ

アプローチ利点欠点
コールバック内でフィルタリングログの追加が容易、柔軟性が高いウォッチャーはイベントを受け取り続ける
chokidar の ignored効率的、ノイズが少ないデバッグが困難(イベントが到達しない)
ディレクトリの分離最もクリーンな分離プロジェクト構造に合わない場合がある

最良の長期的な解決策は、生成コンテンツをソースコンテンツと明確に分離されたディレクトリに配置することである。しかし、それが不可能な場合は、ウォッチャーのコールバック内でのパスフィルタリングがシンプルかつ信頼性の高い解決策となる。