TypeScriptでCLIツールを作って、npmパッケージにする

## 環境

- node 9.4.0
- TypeScript 2.9.2
- npm 6.1.0
- webpack 4.16.3
- Visual Studio Code 1.25.1

予め[tj/n: Node version management](https://github.com/tj/n)等を使って、Node.jsをインストールしておきましょう。

## 実践
適当な名前でプロジェクトディレクトリを作っておきます。ココでは`ts-sample`とします。

```
$ mkdir ts-sample
$ cd ts-sample/
$ git init
$ curl https://raw.githubusercontent.com/github/gitignore/master/Node.gitignore > .gitignore
```

### webpackでTypeScriptのBuild環境構築
まず`package.json`を作成します。

```
$ npm init
```
適当に答えていくと、こんな感じになります。

```json:package.json
{
  "name": "ts-sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT"
}
```
TypeScriptと、ビルドするために必要なパッケージを、`devDependencies`でインストールします。
今回はwebpackを使うため、`ts-loader`もインストールします。

```
$ npm install --save-dev webpack webpack-cli typescript ts-loader
```
TypeScriptの設定である`tsconfig.json`を作成します。
手動で作成しても良いですが、`$ tsc --init`で自動生成することもできます。

```
$ ./node_modules/typescript/bin/tsc --init
```

webpackの設定である`webpack.config.js`を作成します。

```js:webpack.config.js
module.exports = {
  entry: './src/index.ts',
  target: 'node',
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: ['ts-loader'],
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: [ '.tsx', '.ts', '.js' ]
  }
};
```
#### npmパッケージとして公開しない場合の設定
もしnpmパッケージとして公開する予定がない場合は、package.jsonに`"private": true`を付けておきましょう。誤って`$ npm publish`で公開してしまう事を回避できます。

また、このとき`description`など「公開しないならば不要な設定項目」は一緒に消してしまっても構いません。(`private`でない場合、package.jsonが正しく設定されていないと各種警告が表示されます)

```diff:package.json
 {
-  "name": "ts-sample",
-  "version": "1.0.0",
-  "description": "",
+  "private": true,
   "main": "index.js",
```
参考: [package\.json \| npm Documentation](https://docs.npmjs.com/files/package.json#private)

### ソースコードの記述
TypeScriptのコーディングを始める前に、Node.js自体の型定義ファイルをインストールしておきます。

```
$ npm install --save-dev @types/node
```
実際のソースコードを書いていきます。

```
$ mkdir src
$ code src/index.ts
```
今回は例として、コマンドライン引数から数字を受け取って、[Fizz Buzz](https://ja.wikipedia.org/wiki/Fizz_Buzz)で返すだけのCLIプログラムを書いてみます。

```ts:src/index.ts
const num : number = +process.argv[2];
console.log(fizzbuzz(num));

function fizzbuzz(num : number) : string {
  if (num % 15 == 0) {
    return "FizzBuzz";
  } else if (num % 3 == 0) {
    return "Fizz";
  } else if (num % 5 == 0) {
    return "Buzz";
  }
  return num.toString();
}

```

### ビルドして実行
webpackでビルドするnpm scriptを記述します。

```diff:package.json
   "scripts": {
+    "build": "webpack",
```
ビルドします。

```
$ npm run build
```
実行してみます

```
$ node dist/main.js 1
1
$ node dist/main.js 2
2
$ node dist/main.js 3
Fizz
```
うまく動きました。

### npm経由でコマンドとして実行
作成したプログラムを、CLIアプリケーションとして実行できるように設定していきます。

まずpackage.jsonに`bin`の設定を追加します。

```diff:package.json
+  "bin": {
+    "ts-sample": "./bin/ts-sample.js"
+  },
```
参考: [package\.json \| npm Documentation](https://docs.npmjs.com/files/package.json)

設定した`bin/ts-sample.js`ファイルからプログラムを実行できるようにします。

```js:bin/ts-sample.js
#!/usr/bin/env node
require('../dist/main.js');
```
`$ npm link`コマンドを使うと、このプロジェクトに対してシンボリックリンクを貼り、ローカルでnpm moduleをglobal installしたかのように使えるようになります。

```
$ npm link
```
参考: [link \| npm Documentation](https://docs.npmjs.com/cli/link)

こうして`package.json`内で`bin`に指定したキーで、コマンドが実行になります。

```
$ ts-sample 15
FizzBuzz
```

### 開発/本番の各ビルドステージの設定
現在出力された`js:dist/main.js`は、プロダクションビルドの設定になっています。
中身はminifyされており単体では読むことができません。

```js:dist/main.js
!function(e){var r={};function t(n){if(r[n])return r[n].exports;var o=r[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:n})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,r){if(1&r&&(e=t(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(t.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var o in e)t.d(n,o,function(r){return e[r]}.bind(null,o));return n},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=0)}([function(e,r,t){"use strict";var n=+process.argv[2];console.log(function(e){if(e%15==0)return"FizzBuzz";if(e%3==0)return"Fizz";if(e%5==0)return"Buzz";return e.toString()}(n))}]);
```
また、開発時はビルド用になんどもコマンドを打つのも面倒です。

- 開発時にはファイルの変更を監視したい
- 開発時にはソースマップを有効にしたい
- 本番ビルドではソースマップを無効にしたい
- 後に`npm publish`する直前に自動で本番ビルドしたい

これらの問題を解決するため、webpackの設定を開発用と本番用で分離します。
`webpack.dev.js`を作成し、`webpack.config.js`を読み込んで設定をmergeします。

まずmergeするためのライブラリを読み込みます。

```
$ npm install webpack-merge --save-dev
```

`webpack.dev.js`を作成し、下記の記述をします。

```js:webpack.dev.js
const merge = require('webpack-merge'); // v5以上は { merge } = require('webpack-merge') とする
const common = require('./webpack.config.js');
module.exports = merge(common, {
  mode: 'development',
  devtool: 'inline-source-map',
});
```

`package.json`に、それぞれのビルド用のコマンドを追記します

```diff:package.json
   "scripts": {
+    "prepare": "webpack --config webpack.config.js",
+    "watch": "webpack --config webpack.dev.js --watch"
   },
```
参考: [Production - webpack](https://webpack.js.org/guides/production/)

こうして、開発時は`npm run watch`することでファイル変更時に自動でビルドされるようになりました。

```
$ npm run watch
```
このときは`dist/main.js`の中身も読みやすいものになっています。

```js:dist/main.js(抜粋)
var num = +process.argv[2];
console.log(fizzbuzz(num));
function fizzbuzz(num) {
    if (num % 15 == 0) {
        return "FizzBuzz";
    }
    else if (num % 3 == 0) {
        return "Fizz";
    }
    else if (num % 5 == 0) {
        return "Buzz";
    }
    return num.toString();
}
```

また`prepare`は、`$ npm publish`する前などに自動的に実行されます。なので、公開時には自動的に本番の設定でビルドされたものがpublishされます。もちろん、`$ npm run prepare`で手動実行することもできます。

参考: [scripts \| npm Documentation](https://docs.npmjs.com/misc/scripts)

#### VSCodeでWatchする
ここではVisual Studio Codeのnpm連携機能を使ってみます。

コマンドパレットから`>Tasks: Configure Task`→`npm: watch`すると、下記JSONが生成されます。

```json:tasks.json
{
  // See https://go.microsoft.com/fwlink/?LinkId=733558
  // for the documentation about the tasks.json format
  "version": "2.0.0",
  "tasks": [
    {
      "type": "npm",
      "script": "watch",
      "problemMatcher": []``
    }
  ]
}
```
コマンドパレットから`task npm: watch`すると、ターミナルウィンドウがひとつ自動的に立ち上がり`$ npm watch`を実行します。

また、VSCodeの設定で`npm.enableScriptExplore`を有効にすると、`package.json`内のnpm scriptsを読み取ってサイドバーに表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/50391/960d9426-ce9b-0b6a-3f8d-f68944702663.png)
ここでwatchをクリックすることでも`$ npm watch`を実行できます。

### npmignoreの設定
ここでは、npmパッケージとして公開したい場合を考えます。

- gitリポジトリには、**`dist/`以下のビルド済JavaScriptファイルは無視**して、ビルド前のTypeScriptソースコードのみを登録したい
- npmパッケージとしては、**`src/`以下のTypeScriptソースコードは無視**して、ビルド済のJavaScriptファイルのみを登録したい

これらを実現するため、`.gitignore`に下記追記します

```:.gitignore
dist/
```

そして`.npmignore`を作成し、下記追記します

```:.npmignore
src/
```
すると、gitリポジトリにはTypeScriptコードのみが含まれ、npmパッケージにはビルド済JavaScriptファイルのみが含まれるようになります。

参考: [developers \| npm Documentation](https://docs.npmjs.com/misc/developers#keeping-files-out-of-your-package)

### 利用
GitHubに上がっていれば、`npm install`コマンドでインストールすることが可能です。

```
$ npm install --global github_user_name/project_name
```

そしてnpmパッケージとして`$ npm publish`済であれば、下記コマンドでnpmからインストールできます。

```
$ npm install --global project_name
```

その他の詳細は、公式ドキュメントが詳しいです。
https://docs.npmjs.com/cli/install

## 参考

- [React & Webpack · TypeScript](https://www.typescriptlang.org/docs/handbook/react-&-webpack.html)
- [How to create and publish an npm module in TypeScript](https://codeburst.io/https-chidume-nnamdi-com-npm-module-in-typescript-12b3b22f0724)
- [Node\.jsでCLIアプリ · JavaScriptの入門書 \#jsprimer](https://asciidwango.github.io/js-primer/use-case/nodecli/)

また、こうしてできたCLIツールが下記になります。
[s2terminal/i\_read\_u: Extracting commands from README markdown file](https://github.com/s2terminal/i_read_u)