`@tailwindcss/cli` に `--poll` オプションを再導入
Tailwind CSS v4 の CLI で失われていた --poll オプションが復活し、ファイルシステムイベントが取得できない環境でもインクリメンタルなビルドが可能になりました。
背景
--poll は v3 で提供されていたが、v4 で @parcel/watcher に切り替えた際に削除され、Docker などの bind‑mount 環境で --watch が機能しなくなったことが問題となっていました(#18109、#18540)。
ファイル変更の検知に依存する開発フローが途絶えると、手作業でビルドコマンドを走らせるしかなく、フルビルドのコストが増大します。この PR はそのギャップを埋め、Docker でも快適に --watch を利用できるようにすることを目的としています。
技術的な変更
CLI オプション定義 が packages/@tailwindcss-cli/src/commands/build/index.ts に追加され、--poll は boolean | number 型で受け取ります。デフォルトは false、true が指定された場合は内部定数 DEFAULT_POLL_INTERVAL_MS(250 ms)を使用します。
+const DEFAULT_POLL_INTERVAL_MS = 250
@@
+ '--poll': {
+ type: 'boolean | number',
+ description: 'Use polling instead of filesystem events when watching',
+ default: false,
+ values: ['ms'],
+ },
オプションのバリデーションロジックも同ファイルに実装され、--poll が未指定の場合はエラーメッセージを出してプロセスを終了します。また、数値が 0 以下の場合もエラーとし、正のミリ秒数だけが受け入れられます。
+ if (args['--poll'] === undefined) {
+ eprintln(`Use --poll with a non-zero value in milliseconds.`)
+ process.exit(1)
+ }
+ let pollInterval = args['--poll'] === true ? DEFAULT_POLL_INTERVAL_MS : args['--poll']
+ if (pollInterval !== false && pollInterval <= 0) {
+ eprintln(`Specified polling interval must be a positive number.`)
+ process.exit(1)
+ }
内部スキャナ が crates/oxide/src/scanner/mod.rs に拡張され、スキャン毎に走査したファイルパスを scanned_files ベクタに格納し、get_scanned_files アクセサで取得できるようになりました。これにより、ポーリング時に前回と変化がなかったファイルを高速にスキップできます。
+ /// Files that were scanned during the last `scan()` call.
+ scanned_files: Vec<String>,
@@
+ self.scanned_files = files;
@@
+ pub fn get_scanned_files(&self) -> Vec<String> {
+ self.scanned_files.clone()
+ }
Node バインディング側 (crates/node/src/lib.rs) でも同様に scanned_files の getter が公開され、JavaScript からも走査結果を参照できるようになっています。
+ #[napi(getter)]
+ pub fn scanned_files(&self) -> Vec<String> {
+ self.scanner.get_scanned_files()
+ }
テストは integrations/cli/index.test.ts に watch mode with polling が追加され、--poll=50 でファイル変更が正しく検知され、インクリメンタルに CSS が更新されることを確認しています。
+ test(
+ 'watch mode with polling',
+ {
+ fs: { ... },
+ },
+ async ({ fs, spawn }) => {
+ let process = await spawn(`${command} --input src/index.css --output dist/out.css --watch --poll=50`)
+ await process.onStderr((m) => m.includes('Done in'))
+ await fs.expectFileToContain('dist/out.css', [candidate`underline`])
+ await fs.write('src/index.html', html`<div class="underline flex"></div>`)
+ await fs.expectFileToContain('dist/out.css', [candidate`flex`])
+ },
+ )
設計判断
後方互換性の維持 が最優先され、既存の --watch 動作は変更せず --poll をオプションとして追加しました。true が指定された場合は既定の 250 ms が適用されるため、ユーザーはコマンドラインの変更だけで即座に利用できます。
boolean | number の型選択は、シンプルに「デフォルトでポーリング」か「カスタム間隔」を明示できる点が評価されました。実装は CLI のオプションパースに数行追加するだけで済み、コアのビルドロジックに大きな侵入を行っていません。
内部スキャナに scanned_files を導入したのは、ポーリング時の インクリメンタルビルド を実現するための最小限の情報提供です。走査対象を正確に把握できるため、変更が無いファイルを再パースせずに済み、パフォーマンス低下を抑えられます。
まとめ
--poll オプションの復活により、Docker などファイルシステムイベントが取得できない環境でも安全に --watch が利用でき、従来と同等のインクリメンタルビルド体験が提供されます。実装は CLI のオプション拡張とスキャナの軽微な状態追跡に留め、後方互換性とパフォーマンスのバランスを保った設計判断がなされています。