Inquirer.js

対話型コマンドラインユーザーインターフェースのコレクション。美しいCLIプロンプトを簡単に作成できます。

javascriptnodejsclicommand-linepromptsinteractive

フレームワーク

Inquirer.js

概要

Inquirer.jsは、Node.jsでインタラクティブなコマンドラインユーザーインターフェースを作成するための包括的なライブラリです。美しい対話型プロンプトを簡単に作成でき、入力、選択、確認、パスワード入力など、様々な種類のプロンプトを提供します。多くの著名なプロジェクト(npm init、create-react-app、Yeomanなど)で採用されており、Node.jsエコシステムにおけるCLIプロンプトのデファクトスタンダードとなっています。

なぜInquirer.jsが選ばれるのか:

  • 豊富なプロンプトタイプ: input、confirm、list、checkbox、password、editorなど11種類以上
  • モジュラー設計: 必要な機能だけを選択してインストール可能(@inquirer/prompts)
  • TypeScript完全サポート: 型安全性とIntelliSenseによる優れた開発体験
  • 高度なカスタマイズ: テーマ、バリデーション、フィルタリング機能
  • 非同期処理対応: async/awaitとPromiseベースのAPI

詳細

歴史と発展

Inquirer.jsは2013年にSimon Boudrias氏によって開発が開始され、10年以上にわたってNode.jsコミュニティで愛用されています。2024年には大幅なリアーキテクチャが行われ、モジュラー設計の@inquirer/promptsパッケージが導入されました。この新しい設計により、バンドルサイズの最適化と個別プロンプトの独立した使用が可能になりました。

エコシステムでの位置づけ

Node.jsの対話型CLI開発において最も重要なライブラリの一つとして位置づけられています:

  • npm ecosystem: npm initやcreate-react-appなどの基盤技術
  • 開発ツール: Yeoman、Angular CLI、Vue CLIなどで広く採用
  • エンタープライズツール: 多くの企業内ツールやCI/CDパイプラインで使用
  • 教育分野: Node.js学習の入門教材として頻繁に紹介

最新動向(2024-2025年)

  • モジュラー設計への移行: @inquirer/promptsによる軽量化とツリーシェイキング対応
  • パフォーマンス向上: 新しいコアアーキテクチャによる高速化
  • React-like hooks: useState、useEffect、useKeypressなどのモダンなAPI
  • ESM対応: ES Modulesの完全サポート
  • AbortController対応: プロンプトのキャンセル機能
  • テスト支援強化: @inquirer/testingパッケージによるユニットテスト対応

主な特徴

コアプロンプトタイプ

  • input: テキスト入力プロンプト
  • number: 数値入力プロンプト(自動バリデーション付き)
  • confirm: はい/いいえの確認プロンプト
  • list/select: 単一選択リストプロンプト
  • rawlist: 数字キーによる選択プロンプト
  • expand: コンパクトな選択プロンプト(キーボードショートカット付き)
  • checkbox: 複数選択チェックボックスプロンプト
  • password: パスワード入力プロンプト(マスク表示)
  • editor: 外部エディタ起動プロンプト

高度な機能

  • search: 検索可能な選択プロンプト(非同期データソース対応)
  • autocomplete: オートコンプリート機能付きプロンプト
  • table: テーブル形式の複数選択プロンプト
  • file-selector: ファイル/ディレクトリ選択プロンプト
  • datepicker: 日付選択プロンプト

開発者体験

  • TypeScript型推論: プロンプトの戻り値型の自動推論
  • バリデーション: 同期・非同期バリデーション関数
  • フィルタリング: 入力値の変換・正規化
  • 条件分岐: 前の回答に基づく動的な質問表示
  • テーマサポート: カラー、プレフィックス、スタイルのカスタマイズ
  • i18n対応: 多言語メッセージのサポート

メリット・デメリット

メリット

  • 豊富なプロンプトタイプ: 様々なUI要素を簡単に実装
  • 優れたUX: 直感的で美しいインターフェース
  • モジュラー設計: 必要最小限のバンドルサイズ
  • TypeScript完全サポート: 型安全性と開発効率
  • 豊富なエコシステム: プラグインとテーマの豊富さ
  • 活発なコミュニティ: 継続的な開発とサポート
  • テスト支援: 包括的なテスト機能

デメリット

  • 学習コスト: 高度な機能には慣れが必要
  • パフォーマンス: 非常にシンプルなプロンプトには過剰
  • 依存関係: 他のライブラリに依存する場合がある
  • 設定の複雑さ: 高度なカスタマイズには詳細な設定が必要

主要リンク

書き方の例

基本的な使用例(新しいAPI)

import { input, confirm, select } from '@inquirer/prompts';

// テキスト入力
const name = await input({ message: 'あなたの名前を入力してください' });

// 確認プロンプト
const confirmed = await confirm({ message: '続行しますか?' });

// 選択プロンプト
const framework = await select({
  message: 'フレームワークを選択してください',
  choices: [
    { name: 'React', value: 'react' },
    { name: 'Vue', value: 'vue' },
    { name: 'Angular', value: 'angular' }
  ]
});

console.log(`こんにちは、${name}さん!`);
console.log(`選択したフレームワーク: ${framework}`);

複数のプロンプトを組み合わせた例

import { input, select, checkbox, confirm } from '@inquirer/prompts';

async function setupProject() {
  const projectName = await input({
    message: 'プロジェクト名を入力してください',
    validate: (input) => {
      if (input.length < 3) {
        return 'プロジェクト名は3文字以上で入力してください';
      }
      return true;
    }
  });

  const projectType = await select({
    message: 'プロジェクトの種類を選択してください',
    choices: [
      { name: 'Webアプリケーション', value: 'web' },
      { name: 'CLIツール', value: 'cli' },
      { name: 'ライブラリ', value: 'library' }
    ]
  });

  const features = await checkbox({
    message: '必要な機能を選択してください',
    choices: [
      { name: 'TypeScript', value: 'typescript' },
      { name: 'ESLint', value: 'eslint' },
      { name: 'Prettier', value: 'prettier' },
      { name: 'Jest', value: 'jest' },
      { name: 'Husky (Git hooks)', value: 'husky' }
    ]
  });

  const useGit = await confirm({
    message: 'Gitリポジトリを初期化しますか?',
    default: true
  });

  return {
    projectName,
    projectType,
    features,
    useGit
  };
}

// 使用例
const config = await setupProject();
console.log('プロジェクト設定:', config);

バリデーションとフィルタリングの例

import { input, number } from '@inquirer/prompts';

// 同期バリデーション
const email = await input({
  message: 'メールアドレスを入力してください',
  validate: (input) => {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(input)) {
      return '有効なメールアドレスを入力してください';
    }
    return true;
  }
});

// 非同期バリデーション
const username = await input({
  message: 'ユーザー名を入力してください',
  validate: async (input) => {
    if (input.length < 3) {
      return 'ユーザー名は3文字以上で入力してください';
    }
    
    // 外部APIでの重複チェック(例)
    try {
      const response = await fetch(`/api/users/check/${input}`);
      const { available } = await response.json();
      if (!available) {
        return 'このユーザー名は既に使用されています';
      }
    } catch (error) {
      return 'ユーザー名の確認に失敗しました';
    }
    
    return true;
  }
});

// 数値入力(自動バリデーション)
const age = await number({
  message: '年齢を入力してください',
  min: 0,
  max: 150,
  validate: (input) => {
    if (input < 18) {
      return '18歳以上である必要があります';
    }
    return true;
  }
});

検索プロンプトの例

import { search } from '@inquirer/prompts';

const selectedPackage = await search({
  message: 'npmパッケージを検索してください',
  source: async (input, { signal }) => {
    // 入力がない場合は空の配列を返す
    if (!input) {
      return [];
    }

    try {
      // npm APIから検索
      const response = await fetch(
        `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(input)}&size=10`,
        { signal }
      );
      
      if (!response.ok) {
        return [];
      }

      const data = await response.json();
      
      return data.objects.map((pkg) => ({
        name: pkg.package.name,
        value: pkg.package.name,
        description: pkg.package.description
      }));
    } catch (error) {
      // リクエストがキャンセルされた場合などのエラーハンドリング
      if (error.name === 'AbortError') {
        return [];
      }
      throw error;
    }
  }
});

console.log('選択されたパッケージ:', selectedPackage);

パスワードプロンプトとエディタの例

import { password, editor, confirm } from '@inquirer/prompts';

// パスワード入力
const userPassword = await password({
  message: 'パスワードを入力してください',
  mask: '*',
  validate: (input) => {
    if (input.length < 8) {
      return 'パスワードは8文字以上で入力してください';
    }
    if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(input)) {
      return 'パスワードには小文字、大文字、数字をそれぞれ1文字以上含めてください';
    }
    return true;
  }
});

// エディタ起動
const description = await editor({
  message: '詳細な説明を入力してください(エディタが開きます)',
  default: '# プロジェクトの説明\n\nここに詳細を記述してください...',
  validate: (input) => {
    if (input.trim().length === 0) {
      return '説明を入力してください';
    }
    return true;
  }
});

// 確認
const saveToFile = await confirm({
  message: '設定をファイルに保存しますか?',
  default: true
});

if (saveToFile) {
  console.log('設定を保存しました');
}

テーマカスタマイズの例

import { input, select } from '@inquirer/prompts';
import colors from 'picocolors';

// カスタムテーマを使用
const customTheme = {
  prefix: {
    idle: colors.blue('?'),
    done: colors.green('✓')
  },
  spinner: {
    interval: 80,
    frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
  },
  style: {
    answer: (text) => colors.cyan(text),
    message: (text, status) => {
      if (status === 'done') {
        return colors.bold(text);
      }
      return text;
    },
    error: (text) => colors.red(`✗ ${text}`),
    help: (text) => colors.dim(text),
    highlight: (text) => colors.yellow(text),
    key: (text) => colors.cyan(`<${text}>`)
  }
};

const name = await input({
  message: 'プロジェクト名を入力してください',
  theme: customTheme
});

const framework = await select({
  message: 'フレームワークを選択してください',
  choices: [
    { name: 'React', value: 'react' },
    { name: 'Vue.js', value: 'vue' },
    { name: 'Angular', value: 'angular' }
  ],
  theme: customTheme
});

条件分岐とエラーハンドリングの例

import { input, confirm, select } from '@inquirer/prompts';

async function interactiveSetup() {
  try {
    const projectType = await select({
      message: 'プロジェクトの種類を選択してください',
      choices: [
        { name: 'Webアプリケーション', value: 'web' },
        { name: 'モバイルアプリ', value: 'mobile' },
        { name: 'デスクトップアプリ', value: 'desktop' }
      ]
    });

    let framework;
    // プロジェクトタイプに応じた条件分岐
    if (projectType === 'web') {
      framework = await select({
        message: 'Webフレームワークを選択してください',
        choices: [
          { name: 'React', value: 'react' },
          { name: 'Vue.js', value: 'vue' },
          { name: 'Angular', value: 'angular' },
          { name: 'Svelte', value: 'svelte' }
        ]
      });
    } else if (projectType === 'mobile') {
      framework = await select({
        message: 'モバイルフレームワークを選択してください',
        choices: [
          { name: 'React Native', value: 'react-native' },
          { name: 'Flutter', value: 'flutter' },
          { name: 'Ionic', value: 'ionic' }
        ]
      });
    }

    const useTypeScript = await confirm({
      message: 'TypeScriptを使用しますか?',
      default: true
    });

    // 設定に応じた追加の質問
    let testFramework;
    if (useTypeScript) {
      testFramework = await select({
        message: 'テストフレームワークを選択してください',
        choices: [
          { name: 'Jest', value: 'jest' },
          { name: 'Vitest', value: 'vitest' },
          { name: 'Mocha + Chai', value: 'mocha' }
        ]
      });
    }

    return {
      projectType,
      framework,
      useTypeScript,
      testFramework
    };

  } catch (error) {
    if (error.name === 'ExitPromptError') {
      console.log('\n👋 セットアップがキャンセルされました');
      process.exit(0);
    }
    throw error;
  }
}

// AbortControllerを使用したタイムアウト
const controller = new AbortController();
setTimeout(() => {
  controller.abort();
}, 30000); // 30秒でタイムアウト

try {
  const config = await interactiveSetup();
  console.log('✅ セットアップ完了:', config);
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('⏰ セットアップがタイムアウトしました');
  } else {
    console.error('❌ エラーが発生しました:', error.message);
  }
}