`canonicalize`サブコマンドに`--stream`フラグを追加
tailwindcss canonicalizeコマンドに--streamフラグが追加され、デザインシステムをメモリに保持したまま標準入力からクラス群を継続的に受け取って正規化結果を返せるようになりました。これにより、フォーマッターやエディタプラグインなどの非JSツールが、毎回の起動コストを払わずにTailwindクラスの正規化処理を繰り返し実行できます。
背景
フォーマッターやエディタプラグインのような非JS環境のツールには、Tailwindクラスを正規化する軽量な手段がありませんでした。既存のバッチモードは一度きりの使用には適していますが、繰り返し正規化を行うツールでは、実行のたびにデザインシステムのロードコストが発生するという問題がありました。
この課題はdiscussion #19736でも議論されており、ロングランニングプロセスとして起動し続けるサイドカーモデルが解決策として検討されていました。--streamフラグはまさにこのモデルを実現するもので、ツールがtailwindcss canonicalize --streamを一度起動すれば、その後は標準入力に候補グループを送り込むだけで正規化結果を得られます。
実際の使用例は以下の通りです:
$ echo -e "py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2" | tailwindcss canonicalize --stream
p-3
m-2
技術的な変更
--streamフラグの追加は、CLIのオプション定義から実装までpackages/@tailwindcss-cli/src/commands/canonicalize/index.tsに集約されています。
まず、オプション定義に--streamが追加されました:
'--stream': {
type: 'boolean',
description: 'Read candidate groups from stdin line by line and write results to stdout',
default: false,
},
runCommandLine関数内では、flags['--stream']が真の場合に新たにエクスポートされたstreamStdin関数へ処理を委譲します。streamStdin関数はnode:readlineのcreateInterfaceを使って標準入力を1行ずつ読み取り、各行を候補グループとして正規化処理を実行して結果を即座に書き出します。空行はそのまま透過させる設計となっており、リクエストとレスポンスのペアのアライメントが崩れません。
出力フォーマットは既存の--formatフラグと組み合わせられます。テキスト形式(デフォルト)ではシンプルに正規化後のクラス文字列を1行ずつ出力し、JSON形式ではinput・output・changedの3フィールドを持つオブジェクト配列として出力します。テストでその動作が検証されています:
test('streams json output when requested', async () => {
let input = Readable.from('py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2\n')
let { stream: output, collect: collectOutput } = createOutput()
await streamStdin({ css, cwd: path.dirname(css), format: 'json', input, output })
expect(JSON.parse(collectOutput())).toEqual([
{ input: 'py-3 p-1 px-3', output: 'p-3', changed: true },
{ input: 'mt-2 mr-2 mb-2 ml-2', output: 'm-2', changed: true },
])
})
テストではnode:streamのReadableとPassThroughを用いてstdin/stdoutをモックしており、streamStdin関数がテスト可能なインターフェースとして独立してエクスポートされていることが確認できます。
設計判断
デザインシステムをプロセスライフタイム全体で保持する設計が採用されました。--streamモードではプロセス起動時に一度だけデザインシステムをロードし、その後の各行の処理では再ロードを行いません。これが「サイドカープロセス」としての主要な価値であり、ロードコストを償却できます。
空行のパススルー設計も注目に値します。リクエスト側が空行を区切りとして送信し、レスポンス側も空行を受け取ることで、非同期に複数の候補グループを送り込む場合でもリクエストとレスポンスの対応関係を行番号で追跡できます。createInterfaceによるライン単位の読み取りはこの設計に自然に適合します。
また、streamStdin関数をrunCommandLineから分離してエクスポートすることで、テストからI/Oをモックして注入できるようにしています。ReadableとWritableを引数として受け取るインターフェースは、Node.jsのストリームAPIの慣用的なパターンに沿っており、テスタビリティと実装の分離を両立しています。
まとめ
--streamフラグは、Tailwindクラスの正規化を繰り返し行う外部ツールに対して、プロセスの再起動コストを排除したサイドカーモデルを提供します。stdin/stdoutの行単位プロトコルというシンプルなインターフェースにより、言語やプラットフォームを問わずあらゆるツールからの利用が可能となりました。