2つのnpmパッケージを同時公開した話

·
#npm#OSS#Node.js#Neomutt#テスト駆動開発

問題を分割して考えてみた

はじめに

昨日、ターミナルでEmail操作を求めてという記事で、NeomuttとGmailをOAuth2.0で連携させるまでの苦労を書きました。

あの試行錯誤をnpmパッケージにして、同じ問題で困っている人の役に立てないか?と思い、2つのパッケージを作成・公開しました。

この記事では、設計の考え方と実装の過程を記録します。


なぜ2つのパッケージに分けたのか

最初は「全部入りのCLIツールを作ろう」と思っていました。でも、アドバイスをもらって考えが変わりました。

従来の考え(1パッケージ)

create-neomutt-gmail(全部入り)
├─ 設定ファイル生成ロジック
├─ ファイル書き込みロジック
├─ ユーザー対話(CLI)
└─ 依存関係10個

問題点:

  • 他のツールから使えない
  • テストが複雑
  • 責務が混在

実際の設計(3パッケージ)

Phase 1: mutt-config-core(純粋関数)
└─ 設定テキストを生成するだけ

Phase 2: config-fs-utils(副作用)
└─ ファイル操作だけを担当

Phase 3: create-neomutt-gmail(UI)
└─ 上記2つを組み合わせる

メリット:

  • 各パッケージが独立して使える
  • テストが簡単
  • 依存関係が最小限
  • 実務的な設計思想

この分割は、md-blog-coreと同じ考え方です。


Phase 1: mutt-config-core

コンセプト

入力を受け取って、設定ファイルのテキストを返すだけ。ファイルは書き込まない。

const { generateMuttConfigs } = require('mutt-config-core');

const configs = generateMuttConfigs({
  email: 'user@gmail.com',
  realName: 'John Doe',
  editor: 'nvim',
  locale: 'ja'
});

// 文字列が返ってくる
console.log(configs.accountMuttrc);  // Neomuttの設定内容
console.log(configs.mainMuttrc);     // メイン設定内容
console.log(configs.setupGuide);     // 次のステップガイド

特徴

  • 依存関係ゼロ - Node.js標準機能のみ
  • 純粋関数 - 同じ入力なら同じ出力
  • テストしやすい - 副作用がない
  • 型定義付き - JSDocで型情報提供

実装のポイント

1. 日本語/英語フォルダ名対応

Gmailの言語設定で、フォルダ名が変わります:

const GMAIL_FOLDERS = {
  ja: {
    drafts: '下書き',
    sent: '送信済みメール',
    trash: 'ゴミ箱',
    allMail: 'すべてのメール'
  },
  en: {
    drafts: 'Drafts',
    sent: 'Sent Mail',
    trash: 'Trash',
    allMail: 'All Mail'
  }
};

2. 失敗から学んだ設定

以前の記事で試行錯誤した結果を全て反映:

  • GPG暗号化を使う(--encryption-pipe catは使わない)
  • サイドバーは無効化してマクロで操作
  • OAuth2.0認証設定
  • キャッシュ設定で高速化
set imap_oauth_refresh_command = "python3 ~/.config/mutt/mutt_oauth2.py --decryption-pipe 'gpg -d' ~/.local/etc/oauth-tokens/gmail.tokens"

3. バリデーション

入力チェックも実装:

function validateInput(input) {
  if (!input.email || !/^[^\s@]+@gmail\.com$/.test(input.email)) {
    throw new Error('Invalid Gmail address');
  }
  // ... 他のチェック
}

テスト結果

Tests: 24 passed, 24 total
Time: 0.188 s

全てのテストが通過しました。


Phase 2: config-fs-utils

コンセプト

ファイル操作を安全に行うユーティリティ。設定ファイルの書き込みに特化。

const { setupStandardMuttDirs, writeConfigFiles } = require('config-fs-utils');

// 1. ディレクトリ作成
await setupStandardMuttDirs();

// 2. ファイル書き込み(自動でバックアップ&パーミッション設定)
await writeConfigFiles({
  '~/.config/mutt/muttrc': configs.mainMuttrc,
  '~/.config/mutt/accounts/user@gmail.com.muttrc': configs.accountMuttrc
});

特徴

  • 依存関係ゼロ - Node.js標準モジュール(fs, path, os)のみ
  • 安全な書き込み - 既存ファイルを自動バックアップ
  • ~展開 - ~/pathを自動で/Users/username/pathに変換
  • パーミッション管理 - 設定ファイルに適切な権限(0o600)

実装のポイント

1. シンプル&柔軟なAPI

99%のユーザー向けのシンプルAPIと、カスタマイズしたい人向けの柔軟APIを両方提供:

// シンプルAPI(標準パスに全部作る)
await setupStandardMuttDirs();

// 柔軟API(カスタムパスに対応)
await ensureDirectories(['/custom/path']);

2. バックアップ機能

既存ファイルを上書きする前に、タイムスタンプ付きでバックアップ:

// 既存ファイルがあれば
~/.config/mutt/muttrc
// ↓ バックアップ作成
~/.config/mutt/muttrc.backup-2026-01-26T12-34-56-789Z
// ↓ 新しい内容で上書き
~/.config/mutt/muttrc

3. mutt-config-coreとの連携

mutt-config-coregetConfigPaths()が返すオブジェクトから、必要なディレクトリを自動作成:

const paths = getConfigPaths('user@gmail.com');
// {
//   accountMuttrc: '.config/mutt/accounts/user@gmail.com.muttrc',  // ファイル
//   mainMuttrc: '.config/mutt/muttrc',                             // ファイル
//   oauthTokens: '.local/etc/oauth-tokens',                        // ディレクトリ
//   ...
// }

await ensureDirectoriesFromPaths(paths);
// ファイルは親ディレクトリを作成、ディレクトリはそのまま作成

テスト結果

Tests: 29 passed, 29 total
Time: 0.121 s

全てのテストが通過しました。


実際の使い方(2つのパッケージを組み合わせる)

const { generateMuttConfigs, getConfigPaths } = require('mutt-config-core');
const { setupStandardMuttDirs, writeConfigFiles } = require('config-fs-utils');

async function setupNeomutt() {
  // 1. 設定生成
  const configs = generateMuttConfigs({
    email: 'user@gmail.com',
    realName: 'John Doe',
    editor: 'nvim',
    locale: 'ja'
  });

  // 2. ディレクトリ作成
  await setupStandardMuttDirs();

  // 3. ファイル書き込み
  const paths = getConfigPaths('user@gmail.com');
  await writeConfigFiles({
    [`~/${paths.accountMuttrc}`]: configs.accountMuttrc,
    [`~/${paths.mainMuttrc}`]: configs.mainMuttrc
  });

  // 4. 次のステップを表示
  console.log(configs.setupGuide);
}

setupNeomutt();

これで設定ファイルが完成! あとはGoogle Cloud ConsoleでOAuth2.0を設定するだけです。


開発で学んだこと

1. 分割の力

「全部入り」より「組み合わせ可能」の方が強い。

各パッケージが独立しているので:

  • 他のプロジェクトで再利用できる
  • テストが簡単
  • 変更の影響範囲が限定される

2. テスト駆動開発の価値

合計53個のテストを書きましたが、おかげで:

  • リファクタリングが安心してできた
  • バグを早期発見できた
  • ドキュメントとしても機能した

3. 依存関係は最小限に

両方のパッケージとも依存関係ゼロを実現:

  • インストールが速い
  • セキュリティリスクが少ない
  • メンテナンスが楽

必要なのはNode.js標準機能だけ。

4. JSDocの有用性

TypeScriptに変換しなくても、JSDocで型情報を提供できる:

/**
 * @typedef {Object} MuttConfigInput
 * @property {string} email - Gmail address
 * @property {string} realName - User's real name
 * @property {'ja'|'en'} locale - Gmail language setting
 */

VSCodeで自動補完が効いて、型チェックもできます。


Phase 3: create-neomutt-gmail(次回予定)

次はCLIツールを作ります:

npx create-neomutt-gmail

ユーザーが質問に答えるだけで、Neomutt + Gmail + OAuth2.0の設定が完了する予定です。

設計方針

create-neomutt-gmail
├─ inquirer(対話的な質問)
├─ chalk(カラフルな出力)
├─ ora(プログレススピナー)
├─ mutt-config-core(設定生成)
└─ config-fs-utils(ファイル書き込み)

ここだけは依存関係が増えますが、ユーザー体験のためなので許容します。


統計

mutt-config-core

  • テスト: 24個全て通過
  • 依存関係: 0
  • パッケージサイズ: 5.4 kB
  • 公開: 2026-01-26

config-fs-utils

  • テスト: 29個全て通過
  • 依存関係: 0
  • パッケージサイズ: 6.5 kB
  • 公開: 2026-01-26

合計

  • 総テスト数: 53個
  • 総開発時間: 約6時間
  • 失敗から学んだこと: 無数

まとめ

失敗は最高の教材。

Neomuttの設定で何度も失敗したからこそ、他の人が同じ苦労をしなくて済むパッケージが作れました。

設計思想:

  • core: 純粋関数(ロジック)
  • fs-utils: 副作用(ファイル操作)
  • CLI: ユーザー体験

この分割は、実務でも使える設計パターンです。

次はCLIツールを作って、誰でも簡単にNeomuttを設定できるようにします。


リンク


追記

同じような問題で困っている人がいたら、ぜひ使ってみてください。フィードバックや改善案も大歓迎です!

npm install mutt-config-core config-fs-utils