自分のライブラリのダウンロードが気になる…けど。
npmパッケージのダウンロード数を可視化するCLIツールを作った
はじめに
自分が公開しているnpmパッケージのダウンロード数を手軽に確認したいと思い、CLIツール「npm-dl-stats」を作成しました。
- npm: https://www.npmjs.com/package/npm-dl-stats
- GitHub: https://github.com/a-lost-social-misfit/npm-dl-stats
作ったもの
npmパッケージのダウンロード統計を美しく表示するCLIツールです。
主な機能
- 📊 ダウンロード数でソート・色分け表示
- 🎯 対話的なパッケージ選択機能
- 💾 パッケージリストの保存・管理
- 🎨 chalkによる見やすい色付き出力
- ⚡ 軽量(8.8kB、依存2つのみ)
デモ
# インストール
npm install -g npm-dl-stats
# 自分のパッケージを登録
npm-dl-stats init me
# ダウンロード数を確認
npm-dl-stats me
出力例:
📦 npm downloads (last-month)
react 237,228,276 # 1位は太字緑
vue 28,122,368 # 2位は緑
svelte 8,583,371 # 3位は黄色
------ ------------
Total 273,934,015
なぜ作ったか
きっかけ
自分のnpmパッケージが何回ダウンロードされているか気になったのですが、毎回npmのサイトを開いて確認するのが面倒でした。
また、複数パッケージをまとめて確認したり、比較したりする良いツールがなかったため、自分で作ることにしました。
要件定義
以下の要件を満たすツールを目指しました:
- シンプル - 難しい設定不要、すぐ使える
- 軽量 - 依存を最小限に
- 見やすい - 色付け、ソート機能
- 実用的 - 自分が毎日使いたくなるもの
技術選定
基本構成
- 言語: TypeScript
- ランタイム: Node.js 18+
- モジュール: ESM
- 依存: minimist(CLI引数)、chalk(色付け)
なぜTypeScript?
型安全性とエディタ補完の恩恵を受けるためです。特にnpm APIのレスポンス型を定義することで、実行時エラーを防げました。
export type SearchApiResponse = {
objects: Array<{
package: {
name: string
version: string
description?: string
}
}>
total: number
}
なぜESM?
- Node.js 18+で標準サポート
- モダンなJavaScript
- top-level awaitが使える
依存を最小限にした理由
- セキュリティ - 依存が少ないほど脆弱性リスクが低い
- メンテナンス - 依存更新の手間が少ない
- 軽量性 - インストールが速い
設計
ディレクトリ構造
src/
├── types.ts # 型定義
├── config.ts # 純粋関数(設定操作)
├── io/
│ ├── fs.ts # ファイルIO(副作用)
│ └── fetch.ts # HTTP通信(副作用)
├── commands/
│ ├── init.ts # initコマンド
│ ├── config.ts # configコマンド
│ └── show.ts # showコマンド
└── index.ts # エントリーポイント
設計原則
1. 副作用の分離
純粋関数と副作用を明確に分離しました。
純粋関数(config.ts):
// 設定にパッケージを追加(純粋関数)
export const addPackagesToList = (
config: Config,
listName: string,
packages: string[]
): Config => {
const existingPackages = config.lists[listName] || []
const uniquePackages = [...new Set([...existingPackages, ...packages])]
return {
...config,
lists: {
...config.lists,
[listName]: uniquePackages
}
}
}
副作用層(io/fs.ts):
// ファイルの読み書きは副作用層に隔離
export const loadConfig = async (): Promise<Config> => {
try {
if (!configExists()) {
return defaultConfig()
}
const content = await readFile(CONFIG_PATH, 'utf-8')
return parseConfig(content) // 純粋関数を呼ぶ
} catch {
return defaultConfig()
}
}
メリット:
- テストが容易
- ロジックの再利用性が高い
- 予測可能な動作
2. 型安全性
npm APIのレスポンスは unknown として扱い、型アサーションで安全に変換します。
// response.json() は unknown を返す
const data = (await response.json()) as SearchApiResponse
型定義があることで、以下のようなミスを防げます:
// ✅ 型エラーで気づける
data.objects.map(obj => obj.package.nam) // 'nam' → 'name'
3. エラーハンドリング
APIエラーやファイルIOエラーに対して適切に対応します。
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Search API error: ${response.status}`)
}
// ...
} catch (error) {
throw new Error(`Failed to search packages: ${error}`)
}
実装で工夫した点
1. 単一パッケージと複数パッケージのAPI形式の違い
npm Downloads APIは、単一パッケージと複数パッケージでレスポンス形式が異なります。
単一パッケージ:
{
"downloads": 237228276,
"package": "react",
"start": "2025-12-27",
"end": "2026-01-25"
}
複数パッケージ:
{
"react": {
"downloads": 237228276,
"package": "react"
},
"vue": {
"downloads": 28122368,
"package": "vue"
}
}
この違いを吸収する実装:
if (packages.length === 1) {
// 単一パッケージ
const singleData = data as { downloads: number; package: string }
result[singleData.package] = singleData.downloads ?? 0
} else {
// 複数パッケージ
const bulkData = data as BulkDownloadsApiResponse
for (const [pkg, info] of Object.entries(bulkData)) {
result[pkg] = info?.downloads ?? 0
}
}
2. 色付けとランキング
トップ3を強調表示することで、視覚的にわかりやすくしました。
entries.forEach(([pkg, count], index) => {
if (index === 0) {
console.log(chalk.bold.green(paddedPkg), chalk.bold.green(formattedCount))
} else if (index === 1) {
console.log(chalk.green(paddedPkg), chalk.green(formattedCount))
} else if (index === 2) {
console.log(chalk.yellow(paddedPkg), chalk.yellow(formattedCount))
} else {
console.log(paddedPkg, chalk.gray(formattedCount))
}
})
3. 対話的なパッケージ選択
--pick オプションで、リストからパッケージを選択できるようにしました。
const pickPackages = async (packages: string[]): Promise<string[]> => {
console.log(chalk.cyan("\n📋 Select packages:\n"))
packages.forEach((pkg, i) => {
console.log(` ${chalk.gray(`${i + 1}.`)} ${pkg}`)
})
console.log(` ${chalk.gray(`${packages.length + 1}.`)} ${chalk.bold("ALL")}`)
const rl = createInterface({ input, output })
const answer = await rl.question(
chalk.yellow("\nSelect (1-" + (packages.length + 1) + " or comma-separated): ")
)
rl.close()
// カンマ区切りの番号を処理
const indices = answer.trim()
.split(",")
.map(s => parseInt(s.trim()))
.filter(n => !isNaN(n) && n >= 1 && n <= packages.length)
return indices.map(i => packages[i - 1])
}
4. 設定ファイルの保存場所
XDG Base Directory仕様に従い、~/.config/ 配下に保存します。
const CONFIG_DIR = join(homedir(), '.config', 'npm-dl-stats')
const CONFIG_PATH = join(CONFIG_DIR, 'config.json')
config コマンドで場所を確認できるようにしました:
$ npm-dl-stats config
⚙️ Configuration
Location: /Users/username/.config/npm-dl-stats/config.json
Lists:
me (4 packages)
- md-blog-core
- create-neomutt-gmail
- mutt-config-core
- config-fs-utils
開発の流れ
1. プロトタイプ
まず最小構成で動くものを作りました:
- npm APIを叩く
- 結果を表示
2. 機能追加
段階的に機能を追加:
- 設定ファイル保存
- 色付け
- ソート
- 選択機能
3. 公開準備
- README作成
- LICENSE追加
- package.json調整
- GitHubリポジトリ作成
- npm publish
ハマったポイント
1. 型アサーションの必要性
最初は型注釈 : Type で書いていましたが、TypeScriptのstrict modeでエラーになりました。
// ❌ エラー
const data: SearchApiResponse = await response.json()
// ✅ OK
const data = (await response.json()) as SearchApiResponse
response.json() は Promise<any> ではなく Promise<unknown> を返すため、型アサーションが必要でした。
2. ESMでのファイル拡張子
ESMでは、importに .js 拡張子が必要です(.ts ではない)。
// ✅ 正しい
import { runInit } from './commands/init.js'
// ❌ 間違い
import { runInit } from './commands/init.ts'
TypeScriptがトランスパイル時に拡張子を変換しないため、.js と書く必要があります。
3. グローバルコマンドの実行権限
npm link でグローバルインストールする際、実行権限が必要です。
chmod +x dist/index.js
また、shebang(#!/usr/bin/env node)も必須です。
パフォーマンス
バンドルサイズ
- パッケージサイズ: 8.8 kB
- 展開後: 25.1 kB
- 依存: 2つのみ(minimist, chalk)
実行速度
ローカルキャッシュがあれば、コマンド実行は即座に完了します。 API通信も数百ミリ秒で完了するため、ストレスなく使えます。
今後の展望
追加したい機能
- グラフ表示(Sparkline)
- JSONエクスポート
- 過去データとの比較
- GitHub READMEバッジ生成
- 週次レポート(cron対応)
改善点
- テストコードの追加
- CI/CDの構築
- より詳細なエラーメッセージ
まとめ
「自分が欲しいツール」を作るというシンプルな動機から始まり、実際に使えるものができました。
学んだこと:
- 副作用の分離による設計の重要性
- 型安全性がもたらす恩恵
- 依存を減らすことの価値
- ユーザー体験(色付け、ソート)の重要性
結果:
- 約3時間で実装からnpm公開まで完了
- 軽量で高速なCLIツール
- 毎日使いたくなるツール
同じようなツールを作りたい方の参考になれば幸いです。
リンク
- npm: https://www.npmjs.com/package/npm-dl-stats
- GitHub: https://github.com/a-lost-social-misfit/npm-dl-stats
- 作者: @a-lost-social-misfit
インストール
npm install -g npm-dl-stats
ぜひ使ってみてください!