npmのCLIパッケージを作成する

初めてnpmのCLIパッケージを作成してみました。

コマンドライン引数の受け取りやプロンプトの表示など、難しそうなイメージだった部分も、ライブラリを使うことで手軽に実装できたので、それらを紹介したいと思います。

# 作成したパッケージ

利用しなくなったプロジェクトの `node_modules` や `dist` フォルダを探して削除するための `clean-node-projects` というパッケージを作成しました。

`npx clean-node-projects` とすることで、そのパスの配下のフォルダが検索され、どれを削除するか選ぶことができます。
![clean-node-projects_demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504515/76d8b414-2348-c662-ea7c-7078d6f5febf.gif)


https://github.com/K-kind/clean-node-projects

https://www.npmjs.com/package/clean-node-projects

# CLIパッケージの基本

コマンドラインから呼び出すアプリを作るためには、

まず `package.json` の `bin` という項目に、 `{ コマンド名: 実行ファイルパス }` を記載します。

```json
  ...
  "bin": {
    "test-cli": "./bin/test-cli"
  },
```

実行ファイルは、ファイル名を指定するだけで実行できるように、nodeのシェバンを書きます。

```js:bin/test-cli
#!/usr/bin/env node

console.log('Hello world');
```

このパッケージをnpmに公開すれば、 `test-cli` を実行することで `Hello world` と表示されるアプリができます。

# 便利なライブラリ

## yargs

https://github.com/yargs/yargs

yargsは、受け取ったコマンドライン引数を使いやすい形に整えたり、かっこいいヘルプメッセージを生成してくれるライブラリです。

```js:bin/test-cli
#!/usr/bin/env node

const yargs = require('yargs');

const slicedArgs = process.argv.slice(2); // 最初の2つは受け取った引数ではないので除外
const argv = yargs(slicedArgs)
  .option('option-a', {
    alias: 'a',
    description: 'オプションA',
    type: 'number'
  })
  .option('option-b', {
    alias: 'b',
    description: 'オプションB',
    type: 'array',
    default: ['foo', 'bar']
  })
  .help().argv;

console.log(argv.a);
console.log(argv.b);
```

上記のファイルを `test-cli --help` のように実行すると以下のようなメッセージが表示されます。

```
オプション:
      --version   バージョンを表示                                        [真偽]
  -a, --option-a  オプションA                                            [数値]
  -b, --option-b  オプションB                  [配列] [デフォルト: ["foo","bar"]]
      --help      ヘルプを表示                                            [真偽]
```

`test-cli -a 100` のように実行すると、指定した型で値を取得できることがわかります。

```js
console.log(argv.a); // => 100
console.log(argv.b); // => [ 'foo', 'bar' ] （デフォルト値）
```

## inquirer

https://github.com/SBoudrias/Inquirer.js

inquirerは、対話型のインターフェースを簡単に実装できるライブラリです。

チェックボックス選択式の例

```js:bin/test-cli
#!/usr/bin/env node

const inquirer = require('inquirer');

const prompt = async () => {
  const result = await inquirer.prompt({
    type: 'checkbox',
    name: 'folders',
    message: 'Select folders to remove',
    choices: ['aaa/bbb/ccc', 'aaa/bbb/ddd', 'aaa/bbb/eee', 'fff/ggg/hhh']
  });

  console.log(result.folders);
};

prompt();
```

上記を実行すると、以下のようにスペースで選択したり `a` で全選択したりできる入力受付状態になります。
![スクリーンショット 2022-01-07 22.14.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504515/22a0da66-5b53-1b7e-90e4-a363a3e7dde0.png)

```js
console.log(result.folders); // => [ 'aaa/bbb/ccc', 'aaa/bbb/ddd' ]
```

## columnify

https://github.com/timoxley/columnify

columnifyは、データを列を揃えて見やすく表示するためのライブラリです。

```js:bin/test-cli
#!/usr/bin/env node

const columnify = require('columnify');

const result = columnify([
  { path: '/aaa/bbb/ccc', 'last access': '2022/01/01', size: '20MB'},
  { path: '/aaa/bbb/ddd', 'last access': '2022/01/03', size: '40MB'},
  { path: '/aaa/bbb/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 'last access': '2022/01/07', size: '90MB'}
], { maxWidth: 40 });

console.log(result);
```

実行結果

```
PATH                                     LAST ACCESS SIZE
/aaa/bbb/ccc                             2022/01/01  20MB
/aaa/bbb/ddd                             2022/01/03  40MB
/aaa/bbb/eeeeeeeeeeeeeeeeeeeeeeeeeeeeee… 2022/01/07  90MB
eeeee                                                    
```

結果は文字列で得られるので、inquirerと組み合わせて綺麗に表示する際は色々工夫が必要でした。

# 感想

素晴らしいライブラリの製作者の方々に感謝です。

もしよければ [clean-node-projects](https://github.com/K-kind/clean-node-projects) も使ってみていただけると嬉しいです。

今後モチベーションが続けば、ちゃんとテストを書いて、ファイル読み込みを並列に実行するなど、改善できたら良いなと思っています。

# 参考

https://www.twilio.com/blog/how-to-build-a-cli-with-node-js-jp