Optique 0.5.0: エラーハンドリングとメッセージカスタマイズの強化

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

Optique 0.5.0のリリースをお知らせします。このバージョンでは、エラーハンドリング、ヘルプテキスト生成、および全体的な開発者体験に大幅な改善が加えられています。このリリースは完全な後方互換性を維持しているため、既存のコードを変更することなくアップグレードできます。

モジュール分離によるコード構成の改善

大きな @optique/core/parser モジュールが、目的をより明確に反映した3つの集中モジュールにリファクタリングされました。option()argument() などのプリミティブパーサーは現在 @optique/core/primitives に、optional()withDefault() などの修飾関数は @optique/core/modifiers に移動し、object()or() などの結合関数は現在 @optique/core/constructs にあります。

// 以前: すべてを1つのモジュールから
import { 
  option, flag, argument,        // プリミティブ
  optional, withDefault, multiple, // 修飾子
  object, or, merge              // 構成要素
} from "@optique/core/parser";

// 現在: 整理されたインポート(推奨)
import { option, flag, argument } from "@optique/core/primitives";
import { optional, withDefault, multiple } from "@optique/core/modifiers";
import { object, or, merge } from "@optique/core/constructs";

より明確にするためにこれらの専門モジュールからインポートすることをお勧めしますが、すべての関数は引き続き元の @optique/core/parser モジュールから再エクスポートされており、既存のコードが変更なく動作することを保証しています。この再編成によりコードベースがより保守しやすくなり、開発者が異なるパーサータイプ間の関係を理解するのに役立ちます。

自動変換によるスマートなエラーハンドリング

最も要望の多かった機能の1つは、withDefault() におけるデフォルト値コールバックのより良いエラーハンドリングです。以前は、コールバックがエラーをスローした場合(例えば、環境変数が設定されていない場合など)、そのエラーはランタイム例外としてバブルアップしていました。0.5.0からは、これらのエラーは自動的にキャッチされ、パーサーレベルのエラーに変換され、一貫したエラーフォーマットと適切な終了コードを提供します。

// 以前 (0.4.x): アプリをクラッシュさせるランタイム例外
const parser = object({
  apiUrl: withDefault(option("--url", url()), () => {
    if (!process.env.API_URL) {
      throw new Error("API_URL not set"); // キャッチされない例外!
    }
    return new URL(process.env.API_URL);
  })
});

// 現在 (0.5.0): 適切なパーサーエラー
const parser = object({
  apiUrl: withDefault(option("--url", url()), () => {
    if (!process.env.API_URL) {
      throw new Error("API_URL not set"); // 自動的にキャッチされフォーマットされる
    }
    return new URL(process.env.API_URL);
  })
});

また、プレーンな文字列の代わりに構造化されたメッセージを受け入れる WithDefaultError クラスも導入されました。これにより、Optiqueの他のエラー出力と一致するリッチなフォーマットでエラーをスローできるようになりました:

import { WithDefaultError, message, envVar } from "@optique/core";

const parser = object({
  // プレーンなエラー - 自動的にテキストに変換される
  databaseUrl: withDefault(option("--db", url()), () => {
    if (!process.env.DATABASE_URL) {
      throw new Error("Database URL not configured");
    }
    return new URL(process.env.DATABASE_URL);
  }),

  // 構造化されたメッセージを持つリッチなエラー
  apiToken: withDefault(option("--token", string()), () => {
    if (!process.env.API_TOKEN) {
      throw new WithDefaultError(
        message`環境変数 ${envVar("API_TOKEN")} は認証に必要です`
      );
    }
    return process.env.API_TOKEN;
  })
});

新しい envVar メッセージコンポーネントにより、環境変数がエラーメッセージ内で視覚的に区別されるようになり、カラー出力では太字と下線付きで表示され、プレーンテキストではバッククォートで囲まれます。

カスタムデフォルト説明によるより役立つヘルプテキスト

ヘルプテキスト内のデフォルト値は、特に環境変数から取得される場合や実行時に計算される場合、誤解を招くことがあります。Optique 0.5.0では、withDefault()の3番目のオプションパラメータを通じて、ヘルプ出力でのデフォルト値の表示方法をカスタマイズできるようになりました。

import { withDefault, message, envVar } from "@optique/core";

const parser = object({
  // 以前: ヘルプに実際のURL値を表示
  apiUrl: withDefault(
    option("--api-url", url()),
    new URL("https://api.example.com")
  ),
  // ヘルプ表示: --api-url URL [https://api.example.com]

  // 現在: 説明的なテキストを表示
  apiUrl: withDefault(
    option("--api-url", url()),
    new URL("https://api.example.com"),
    { message: message`デフォルトAPIエンドポイント` }
  ),
  // ヘルプ表示: --api-url URL [デフォルトAPIエンドポイント]
});

これは特に環境変数や計算されたデフォルト値に役立ちます:

const parser = object({
  // 環境変数
  authToken: withDefault(
    option("--token", string()),
    () => process.env.AUTH_TOKEN || "anonymous",
    { message: message`${envVar("AUTH_TOKEN")} または anonymous` }
  ),
  // ヘルプ表示: --token STRING [AUTH_TOKEN または anonymous]

  // 計算された値
  workers: withDefault(
    option("--workers", integer()),
    () => os.cpus().length,
    { message: message`CPUコア数` }
  ),
  // ヘルプ表示: --workers INT [CPUコア数]

  // 機密情報
  apiKey: withDefault(
    option("--api-key", string()),
    () => process.env.SECRET_KEY || "",
    { message: message`セキュアストレージから` }
  ),
  // ヘルプ表示: --api-key STRING [セキュアストレージから]
});

実際のデフォルト値を表示する代わりに、値の出所をより適切に説明する説明的なテキストを表示できるようになりました。これは、APIトークンのような機密情報や、CPUコア数のような計算されたデフォルト値に特に役立ちます。

ヘルプシステムは、デフォルト値表示でANSIカラーコードを適切に処理するようになり、内部コンポーネントが独自の色フォーマットを持っている場合でも、薄暗いスタイルを維持します。これにより、デフォルト値がメインのヘルプテキストから視覚的に区別されたままになります。

包括的なエラーメッセージのカスタマイズ

すべてのパーサータイプとコンビネータにわたってエラーメッセージをカスタマイズする体系的な方法を追加しました。すべてのパーサーは、一般的なエラーメッセージの代わりにコンテキスト固有のフィードバックを提供できる errors オプションを受け入れるようになりました。これはプリミティブパーサー、値パーサー、コンビネータ、さらには関連パッケージの特殊パーサーにも適用されます。

プリミティブパーサーのエラー

import { option, flag, argument, command } from "@optique/core/primitives";
import { message, optionName, metavar } from "@optique/core/message";

// カスタムエラーを持つオプションパーサー
const serverPort = option("--port", integer(), {
  errors: {
    missing: message`サーバーポートが必要です。${optionName("--port")} を使用して指定してください。`,
    invalidValue: (error) => message`無効なポート番号: ${error}`,
    endOfInput: message`${optionName("--port")} には ${metavar("PORT")} 番号が必要です。`
  }
});

// カスタムエラーを持つコマンドパーサー
const deployCommand = command("deploy", deployParser, {
  errors: {
    notMatched: (expected, actual) => 
      message`不明なコマンド "${actual}"。"${expected}" を意図していませんか?`
  }
});

値パーサーのエラー

エラーのカスタマイズは、一貫したエラーのための静的メッセージや、問題のある入力を組み込む動的関数にすることができます:

import { integer, choice, string } from "@optique/core/valueparser";

// 範囲検証を持つ整数
const port = integer({
  min: 1024,
  max: 65535,
  errors: {
    invalidInteger: message`ポートは有効な数値である必要があります。`,
    belowMinimum: (value, min) =>
      message`ポート ${String(value)} は予約されています。${String(min)} 以上を使用してください。`,
    aboveMaximum: (value, max) =>
      message`ポート ${String(value)} は最大値を超えています。${String(max)} 以下を使用してください。`
  }
});

// 役立つ提案を持つ選択肢
const logLevel = choice(["debug", "info", "warn", "error"], {
  errors: {
    invalidChoice: (input, choices) =>
      message`"${input}" は有効なログレベルではありません。以下から選択してください: ${values(choices)}.`
  }
});

// パターン検証を持つ文字列
const email = string({
  pattern: /^[^@]+@[^@]+\.[^@]+$/,
  errors: {
    patternMismatch: (input) =>
      message`"${input}" は有効なメールアドレスではありません。形式を使用してください: user@example.com`
  }
});

コンビネータのエラー

import { or, multiple, object } from "@optique/core/constructs";

// カスタムの一致しないエラーを持つOrコンビネータ
const format = or(
  flag("--json"),
  flag("--yaml"),
  flag("--xml"),
  {
    errors: {
      noMatch: message`出力形式を指定してください: --json, --yaml, または --xml。`,
      unexpectedInput: (token) =>
        message`不明な形式オプション "${token}"。`
    }
  }
);

// カウント検証を持つ複数パーサー
const inputFiles = multiple(argument(string()), {
  min: 1,
  max: 5,
  errors: {
    tooFew: (count, min) =>
      message`少なくとも ${String(min)} ファイルが必要ですが、${String(count)} しかありません。`,
    tooMany: (count, max) =>
      message`最大 ${String(max)} ファイルまで許可されていますが、${String(count)} あります。`
  }
});

パッケージ固有のエラー

@optique/run@optique/temporal の両パッケージは、それらの特殊パーサーのためのエラーカスタマイズサポートで更新されました:

// @optique/run パスパーサー
import { path } from "@optique/run/valueparser";

const configFile = option("--config", path({
  mustExist: true,
  type: "file",
  extensions: [".json", ".yaml"],
  errors: {
    pathNotFound: (input) =>
      message`設定ファイル "${input}" が見つかりません。パスを確認してください。`,
    notAFile: (input) =>
      message`"${input}" はディレクトリです。ファイルを指定してください。`,
    invalidExtension: (input, extensions, actual) =>
      message`無効な設定フォーマット "${actual}"。${values(extensions)} を使用してください。`
  }
}));

// @optique/temporal インスタントパーサー
import { instant, duration } from "@optique/temporal";

const timestamp = option("--time", instant({
  errors: {
    invalidFormat: (input) =>
      message`"${input}" は有効なタイムスタンプではありません。ISO 8601形式を使用してください: 2024-01-01T12:00:00Z`
  }
}));

const timeout = option("--timeout", duration({
  errors: {
    invalidFormat: (input) =>
      message`"${input}" は有効な期間ではありません。ISO 8601形式を使用してください: PT30S (30秒)、PT5M (5分)`
  }
}));

エラーカスタマイズはOptiqueの構造化メッセージフォーマットとシームレスに統合され、すべてのエラー出力全体で一貫したスタイリングを確保します。このシステムは、一般的なエラーメッセージで混乱させるのではなく、ユーザーを正しい使用法に導く役立つ、実行可能なフィードバックを提供するのに役立ちます。

今後の展望

このリリースは、既存のコードを壊すことなく開発者体験を向上させることに焦点を当てています。すべての新機能はオプトインであり、すべての変更は後方互換性を維持しています。これらの改善により、特に明確なエラーメッセージと役立つドキュメントが必要なユーザーフレンドリーなCLIアプリケーションを構築する際に、Optiqueがより快適に使用できるようになると考えています。

これらの改善を提案し、議論や問題報告を通じてこのリリースの形成を支援してくれたコミュニティメンバーに感謝します。皆さんのフィードバックは、OptiqueがTypeScriptのためのより有能で人間工学的なCLIパーサーになるための進化を継続的に推進しています。

Optique 0.5.0にアップグレードするには、依存関係を更新するだけです:

npm update @optique/core @optique/run
# または
deno update

詳細な移行ガイダンスとAPIドキュメントについては、公式ドキュメントを参照してください。コードの変更は必要ありませんが、CLIアプリケーションを強化するために、新しいエラーカスタマイズオプションとヘルプテキストの改善を探索することをお勧めします。

5

No comments

If you have a fediverse account, you can comment on this article from your own instance. Search https://hackers.pub/ap/articles/019975b3-b32a-7d0f-b15d-611d1895b480 on your instance and reply to it.