Optique 0.3.0: 依存オプションと柔軟な構成

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

Optique 0.3.0をリリースし、複雑なCLIアプリケーションの構築をより簡単にするいくつかの改善を行いました。このリリースでは、コミュニティからのフィードバック、特にFedifyプロジェクトのCliffyからOptiqueへの移行に基づいて、パーサーの柔軟性の拡張とヘルプシステムの改善に焦点を当てています。このプロセスにおいて貴重な洞察を提供してくれた@z9mb1@hackers.pubに特に感謝します。

新機能

  • 依存オプションパターン用の新しいflag()パーサーによる必須ブール型フラグ
  • 条件付きCLI構造をサポートするユニオン型に対応したwithDefault()柔軟な型デフォルト
  • 最大10個のパーサー(以前は5個)をサポートするように拡張されたor()容量
  • object()だけでなく、任意のオブジェクト生成パーサーで動作する強化されたmerge()コンビネータ
  • 新しいlongestMatch()コンビネータを使用したコンテキスト対応ヘルプ
  • @optique/core@optique/runの両方でのバージョン表示サポート
  • 一貫したターミナルフォーマットのための構造化出力関数

flag()による必須ブール型フラグ

新しいflag()パーサーは、明示的に提供する必要があるブール型フラグを作成します。option()が存在しない場合にfalseをデフォルトとするのに対し、flag()は存在しない場合にパース処理全体が失敗します。この微妙な違いにより、依存オプションのためのよりクリーンなパターンが可能になります。

特定のモードが明示的に有効になっている場合にのみ意味を持つオプションのシナリオを考えてみましょう:

import { flag, object, option, withDefault } from "@optique/core/parser";
import { integer } from "@optique/core/valueparser";

// --advancedフラグがなければ、これらのオプションは利用できません
const parser = withDefault(
  object({
    advanced: flag("--advanced"),
    maxThreads: option("--threads", integer()),
    cacheSize: option("--cache-size", integer())
  }),
  { advanced: false as const }
);

// 使用例:
// myapp                    → { advanced: false }
// myapp --advanced         → エラー: --threadsと--cache-sizeが必要
// myapp --advanced --threads 4 --cache-size 100 → 成功

このパターンは、確認フラグ(--yes-i-am-sure)やCLIの動作を根本的に変更するモードスイッチに特に役立ちます。

withDefault()でのユニオン型

以前は、withDefault()はデフォルト値がパーサーの型と完全に一致する必要がありました。現在は異なる型をサポートし、条件付きCLI構造を可能にするユニオン型を作成します:

const conditionalParser = withDefault(
  object({
    server: flag("-s", "--server"),
    port: option("-p", "--port", integer()),
    host: option("-h", "--host", string())
  }),
  { server: false as const }
);

// 結果の型は現在ユニオン型です:
// | { server: false }
// | { server: true, port: number, host: string }

この変更により、複雑なor()チェーンに頼ることなく、異なるフラグが異なるオプションセットを有効にするCLIを構築することがはるかに簡単になりました。

より柔軟なmerge()コンビネータ

merge()コンビネータは、オブジェクトのような値を生成する任意のパーサーを受け入れるようになりました。以前はobject()パーサーに限定されていましたが、現在はwithDefault()map()、およびその他の変換パーサーでも動作します:

const transformedConfig = map(
  object({
    host: option("--host", string()),
    port: option("--port", integer())
  }),
  ({ host, port }) => ({ endpoint: `${host}:${port}` })
);

const conditionalFeatures = withDefault(
  object({
    experimental: flag("--experimental"),
    debugLevel: option("--debug-level", integer())
  }),
  { experimental: false as const }
);

// 異なるパーサータイプをマージできるようになりました
const appConfig = merge(
  transformedConfig,        // map()の結果
  conditionalFeatures,      // withDefault()パーサー
  object({                  // 従来のobject()
    verbose: option("-v", "--verbose")
  })
);

この改善は、多くのパーサーが最終的にオブジェクトを生成し、merge()object()パーサーのみに人為的に制限することが構成パターンを制限していたという認識から生まれました。

longestMatch()によるコンテキスト対応ヘルプ

新しいlongestMatch()コンビネータは、最も多くの入力トークンを消費するパーサーを選択します。これにより、command --helpがグローバルヘルプではなく、その特定のコマンドのヘルプを表示する高度なヘルプシステムが可能になります:

const normalParser = object({
  help: constant(false),
  command: or(
    command("list", listOptions),
    command("add", addOptions)
  )
});

const contextualHelp = object({
  help: constant(true),
  commands: multiple(argument(string())),
  helpFlag: flag("--help")
});

const cli = longestMatch(normalParser, contextualHelp);

// myapp --help           → グローバルヘルプを表示
// myapp list --help      → 'list'コマンドのヘルプを表示
// myapp add --help       → 'add'コマンドのヘルプを表示

@optique/core/facade@optique/runの両方のrun()関数は、このパターンを自動的に使用するようになったため、追加の設定なしでCLIにコンテキスト対応のヘルプが提供されます。

バージョン表示サポート

@optique/core/facade@optique/runの両方が、--versionフラグとversionコマンドを通じてバージョン表示をサポートするようになりました。詳細はランナーのドキュメントを参照してください:

// @optique/run - シンプルなAPI
run(parser, {
  version: "1.0.0",  // --versionフラグを追加
  help: "both"
});

// @optique/core/facade - 詳細な制御
run(parser, "myapp", args, {
  version: {
    mode: "both",     // --versionフラグとversionコマンドの両方
    value: "1.0.0",
    onShow: process.exit
  }
});

APIはヘルプ設定と同じパターンに従い、一貫性と予測可能性を維持しています。

構造化出力関数

@optique/runの新しい出力関数は、自動的な機能検出による一貫したターミナルフォーマットを提供します。詳細はメッセージのドキュメントで確認できます:

import { print, printError, createPrinter } from "@optique/run";
import { message } from "@optique/core/message";

// 自動フォーマットによる標準出力
print(message`Processing ${filename}...`);

// オプションの終了コードを持つstderrへのエラー出力
printError(message`File ${filename} not found`, { exitCode: 1 });

// 特定のニーズに合わせたカスタムプリンター
const debugPrint = createPrinter({
  stream: "stderr",
  colors: true,
  maxWidth: 80
});

debugPrint(message`Debug: ${details}`);

これらの関数は、ターミナルの機能を自動的に検出して適切なフォーマットを適用し、異なる環境間でCLI出力の一貫性を確保します。

破壊的変更

後方互換性を維持するよう努めましたが、注意すべきいくつかの変更があります:

  • @optique/runhelpオプションは"none"を受け付けなくなりました。ヘルプを無効にするには、単にオプションを省略してください。
  • getDocFragments()を実装するカスタムパーサーは、直接の状態値ではなくDocState<TState>を使用するように署名を更新する必要があります。
  • object()パーサーは現在、貪欲なパースを使用し、一度のパスで一致するすべてのフィールドを消費しようとします。これはほとんどのユースケースには影響しませんが、複雑なシナリオではパース順序が変わる可能性があります。

0.3.0へのアップグレード

Optique 0.3.0にアップグレードするには、両方のパッケージを更新してください:

# Deno (JSR)
deno add @optique/core@^0.3.0 @optique/run@^0.3.0

# npm
npm update @optique/core @optique/run

# pnpm
pnpm update @optique/core @optique/run

# Yarn
yarn upgrade @optique/core @optique/run

# Bun
bun update @optique/core @optique/run

コアパッケージのみを使用している場合:

# Deno (JSR)
deno add @optique/core@^0.3.0

# npm
npm update @optique/core

今後の展望

これらの改善は、実際の使用とコミュニティからのフィードバックから生まれました。特に、新しい依存オプションパターンがあなたのユースケースにどのように機能するか、そしてコンテキスト対応ヘルプシステムがあなたのニーズを満たしているかどうかについて、ご意見をお聞かせいただければ幸いです。

いつものように、完全なドキュメントはoptique.devで確認でき、問題や提案はGitHubで報告できます。

3

No comments

If you have a fediverse account, you can comment on this article from your own instance. Search https://hackers.pub/ap/articles/0198f5c6-9253-7728-a7d5-5a7868e1f724 on your instance and reply to it.