2つのnpmパッケージを同時公開した話
問題を分割して考えてみた
はじめに
昨日、ターミナルでEmail操作を求めてという記事で、NeomuttとGmailをOAuth2.0で連携させるまでの苦労を書きました。
あの試行錯誤をnpmパッケージにして、同じ問題で困っている人の役に立てないか?と思い、2つのパッケージを作成・公開しました。
- mutt-config-core - 設定ファイル生成
- config-fs-utils - ファイル操作ユーティリティ
この記事では、設計の考え方と実装の過程を記録します。
なぜ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-coreのgetConfigPaths()が返すオブジェクトから、必要なディレクトリを自動作成:
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を設定できるようにします。
リンク
- mutt-config-core on npm
- config-fs-utils on npm
- mutt-config-core on GitHub
- config-fs-utils on GitHub
- 前回の記事: ターミナルでEmail操作を求めて
追記
同じような問題で困っている人がいたら、ぜひ使ってみてください。フィードバックや改善案も大歓迎です!
npm install mutt-config-core config-fs-utils