既存のJavaScriptをTypeScript化してwebpackでビルドしてHeroku上でPM2経由でNode.jsサーバーを起動する

## はじめに

タイトルの長さ :innocent: 
とにかく、素のJavaScriptで書かれた個人開発プロジェクトをモダンな環境にして開発効率を上げるぞ！ :muscle: 

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">素のJavaScriptで書かれたnode.jsコードをTypeScript化＆webpack導入でDX向上させて、PM2導入でクラッシュ時/メモリ上限再起動も対応して、どんどん強くしてるぞ、、、たのしい！！！わからんこと多いけど新しいことにチャレンジできて嬉しい☺️</p>&mdash; にっしー👨‍💻岡山⇄東京 (@paranishian) <a href="https://twitter.com/paranishian/status/1167818678500356096?ref_src=twsrc%5Etfw">August 31, 2019</a></blockquote> 

## before

- 一つのファイルに書かれた大きなJavaScriptファイル（`app.js`）
- `node app.js`でNode.jsサーバーを起動
  - Node.jsサーバーがクラッシュしたら手動で再起動

rootに`app.js`が置かれている、実にシンプルな状態 :innocent: 

```sh
❯ tree -L 2
.
├── app.js
├── node_modules
│   └── ...
├── package-lock.json
└── package.json
```

## after

- TypeScript（`app.ts`）
- webpackでビルド（ファイル分割は今後少しずつやっていく）
- PM2でNode.jsサーバーを起動
  - クラッシュ時自動で再起動

`src`にあるtsファイルを`webpack`がビルドして`dist`に出力してくれます :relaxed:

```sh
❯ tree -L 2
.
├── dist
│   ├── bundle.js
│   └── bundle.js.map
├── ecosystem.config.js
├── node_modules
│   └── ...
├── package-lock.json
├── package.json
├── src
│   └── app.ts
├── tsconfig.json
└── webpack.config.js
```


## 手順

### TypeScriptの導入

`typescript`をインストールします。

```sh
npm install -D typescript
```

以下コマンドで`tsconfig.json`を作成し、編集します。

```sh
npx tsc --init
```

```json:tsconfig.json
{
  "compilerOptions": {
    "incremental": true,
    "target": "es5",
    "module": "commonjs",
    "lib": ["es2019"],
    "downlevelIteration": true,

    "strict": true,
    "noImplicitAny": false,

    "moduleResolution": "node",
    "esModuleInterop": true,

    "resolveJsonModule": true,   /* app.ts内で.jsonファイルを読み込んでたので必要 */
    "sourceMap": true            /* webpackでバンドルした際、エラー箇所が特定しやすいようにsourceMap追加 */
  }
}
```

設定内容はgfxさんのこの記事を参考にしました :pray: 
[TypeScript再入門「がんばらないTypeScript」で、JavaScriptを“柔らかい”静的型付き言語に（gfx執筆）](https://employment.en-japan.com/engineerhub/entry/2019/04/16/103000)

今後開発していく中で様子を見ながら変更していくと思います。

### webpackの導入

`webpack`と周辺パッケージをインストールします。

```sh
npm install -D webpack webpack-cli webpack-dev-server ts-loader webpack-node-externals
```

`webpack-node-externals`はwarningの抑制で使っています。

以下コマンドで`webpack.config.js`を作成し、編集します。

```sh
npx webpack init
```

```js:webpack.config.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
    target: 'node',
    devtool: 'inline-source-map',
    entry: {
        bundle: './src/app.ts'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },
    externals: [
        nodeExternals()
    ],
    resolve: {
        extensions: ['.ts', '.js'],
        modules: [
            "node_modules",
            path.resolve(__dirname, "src")
        ]
    },
    // .tsファイルをts-loader
    module: {
        rules: [
            {
                test: /\.ts$/,
                exclude: /node_modules/,
                loader: 'ts-loader'
            }
        ]
    }
}
```

### いざ、TypeScript化

`app.js`をリネームして`src`に移動します。

```sh
mkdir src
mv app.js src/app.ts
```

毎回`webpack`コマンドを叩くのは面倒なので`webpack-dev-server`に動いてもらいます。

```sh
npx webpack --mode development --watch
```

ここでまず大量にエラー出てくるので、自分で型を定義したり、ライブラリの型定義をインストールしたりしてひたすらエラーをつぶしていきます。
（e.g. `npm install -D @types/moment`）

TypeScript化に成功したら、Node.jsサーバーを起動してみます。

```sh
node dist/bundle.js
```

起動に成功したら、Herokuへのデプロイに移りたいところなんですが、今回から`node`コマンドではなくPM2というNode.jsのプロセスマネージャーを使って起動するようにします。

### PM2でNode.jsサーバーを起動する

PM2には便利な機能がたくさんあるんですが、今回はサーバークラッシュ時に自動で再起動させることが主な目的なので特に複雑なことはまだしてません。

PM2にHerokuのインテグレーションも載ってます。ありがたい。
[PM2 \- Heroku Integration](http://pm2.keymetrics.io/docs/integrations/heroku/#prepare-your-app)

```sh
npm install pm2
npx pm2 init
```

`ecosystem.config.js`ファイルが出力されるので、編集します。

```js:ecosystem.config.js
module.exports = {
  apps: [{
    name: 'development',         // 開発環境
    script: './dist/bundle.js',
    instances: 1,
    autorestart: true,
    watch: ['dist'],             // distディレクトリの変更を監視して再起動させる
    env: {
      NODE_ENV: 'development'
    },
  },
  {
    name: 'production',          // 本番環境
    script: './dist/bundle.js',
    instances: 1,
    autorestart: true,
    watch: false,                // ファイル変更の監視は不要
    max_memory_restart: '800M',　// メモリ800Mで再起動させる
    env: {
      NODE_ENV: 'production'
    }
  }]
};
```

開発環境と本番環境で設定を変えたかったので、`name`で分けてます。
今思えば、そもそも設定ファイルを分けてしまってもいいと思います :ok_woman: 

### package.jsonの編集

あとは、`package.json`を編集してエイリアスを追加していきます。

```json:package.json
...
  "scripts": {
    "start": "pm2-runtime start ecosystem.config.js --only production",
    "start:dev": "pm2-runtime start ecosystem.config.js --only development",
    "heroku-postbuild": "npm run build",
    "build": "webpack --mode production",
    "build:dev": "webpack --mode development --watch"
  },
...
```

- `npm start`で本番環境のNode.jsサーバーを起動
- `npm run start:dev`で開発環境のNode.jsサーバーを起動
- `npm run build`で本番環境のwebpackビルド
- `npm run build:dev`で開発環境のwebpackビルド（ファイル変更監視）

といった感じです。
また、`heroku-postbuild`はHerokuへのデプロイ時に走るコマンドです。

つまり、Herokuへデプロイすると、

- まず依存パッケージのインストール（`npm install`）が実行される
    - Herokuではbuild時に`dependencies`と`devDependencies`両方インストールされる
- `npm run heroku-postbuild`が実行される
    - つまり`npm run build`が実行される
    - つまり`webpack --mode production`が実行される
- `npm start`が実行される
    - つまり`pm2-runtime start ecosystem.config.js --only production`が実行される

という流れでNode.jsサーバーが起動します。

## おわりに

`import/export`を使ってファイルを分けたり、`webpack.config.js`の環境ごとに用意したり、まだまだやりたいことは多いですが、ひとまず以前よりはだいぶ開発効率が上がったのでこれからがんばるぞい！ :muscle: 

以上です :smile:  :wave: 

## 参考
- [Heroku Node\.js Support \| Heroku Dev Center](https://devcenter.heroku.com/articles/nodejs-support)
- [Get started Express \+ TypeScript する \- Qiita](https://qiita.com/IgnorantCoder/items/c9b79dbab8c1a34b769f)
- [PM2 \- Advanced Node\.js process manager](http://pm2.keymetrics.io/)
- [TypeScript再入門「がんばらないTypeScript」で、JavaScriptを“柔らかい”静的型付き言語に（gfx執筆）](https://employment.en-japan.com/engineerhub/entry/2019/04/16/103000)