CLOVER🍀

That was when it all began.

TypeScript+Node.jsプロジェクトを自動ビルドする(--watch、nodemon+ts-node)

これは、なにをしたくて書いたもの?

Node.js上で動作するTypeScriptでソースコードを書いていて、変更の度にnpx tscと実行するのが面倒だなぁと
思いまして。

自動的にビルドする方法はないかな?ということで調べてみました。

結論を先に書くと、ソースコードの変更を検知してビルドするだけなら

$ npx tsc --watch

で、変更時にアプリケーションを再起動する場合は

$ npx nodemon --watch 'src/**/*.ts' --exec 'ts-node' [アプリケーションのエントリーポイント].ts

という感じでしょうか。

環境とプロジェクトのセットアップ

今回の環境はこちら。

$ node --version
v16.13.0


$ npm --version
8.1.0

プロジェクトは、こんな感じで作成しました。

$ npm init -y
$ npm i -D typescript
$ npm i -D -E prettier
$ mkdir src

TypeScriptとPrettierのバージョン。

$ npx tsc --version
Version 4.5.2


$ npx prettier --version
2.5.0

各種設定。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

.prettierrc.json

{
  "singleQuote": true
}

サンプルアプリケーションを作成する

動作確認しつつ進めていこうと思うので、サンプルアプリケーションを作成してみましょう。

Expressを使って書くことにします。

$ npm i express
$ npm i -D @types/node @types/express

この時点での依存関係。

  "devDependencies": {
    "@types/express": "^4.17.13",
    "@types/node": "^16.11.10",
    "prettier": "2.5.0",
    "typescript": "^4.5.2"
  },
  "dependencies": {
    "express": "^4.17.1"
  }

ソースコードを作成します。

src/app.ts

import express from 'express';

const app = express();
const port = 3000;

app.get('/hello', (req, res) => {
  res.send('Hello World!!');
});

app.listen(port, () =>
  console.log(`[${new Date().toISOString()}] start server[${port}]`)
);

これで、サンプルアプリケーションはできました。

tscwatch modeを使う

最初は、tscwatch modeを使ってみます。

通常のビルドはこちらですが、

$ npx tsc

ソースコードの変更を検知してビルドする場合は、--watchオプション(watch mode)を使用します。

$ npx tsc --watch

ショートオプションでは-wですね。

$ npx tsc -w

--watch時に使うオプションはドキュメント(Watch Options)を見るか

TypeScript: Documentation - tsc CLI Options

tsc --allで確認できます。

$ npx tsc --all

こんな感じですね。

WATCH OPTIONS

Including --watch, -w will start watching the current project for the file changes. Once set, you can config watch mode with:

                  --watchFile  Specify how the TypeScript watch mode works.
                      one of:  fixedpollinginterval, prioritypollinginterval, dynamicprioritypolling, fixedchunksizepolling, usefsevents, usefseventsonparentdirectory

             --watchDirectory  Specify how directories are watched on systems that lack recursive file-watching functionality.
                      one of:  usefsevents, fixedpollinginterval, dynamicprioritypolling, fixedchunksizepolling

            --fallbackPolling  Specify what approach the watcher should use if the system runs out of native file watchers.
                      one of:  fixedinterval, priorityinterval, dynamicpriority, fixedchunksize

  --synchronousWatchDirectory  Synchronously call callbacks and update the state of directory watchers on platforms that don`t support recursive watching natively.
                        type:  boolean
                     default:  false

         --excludeDirectories  Remove a list of directories from the watch process.

               --excludeFiles  Remove a list of files from the watch mode's processing.

watch modeでtscを起動すると、watch modeで起動したことを表すメッセージが表示され

[20:58:08] Starting compilation in watch mode...

ソースコードを自動的にコンパイルします。

[20:58:08] Starting compilation in watch mode...

[20:58:12] Found 0 errors. Watching for file changes.

ここでコンパイルエラーになるようなソースコードの変更をすると

app.get('/hello', (req, res) => {
  res.send('Hello World!!';
});

コンソールにコンパイルエラーが表示されます。

[21:00:06] File change detected. Starting incremental compilation...

src/app.ts:7:27 - error TS1005: ')' expected.

7   res.send('Hello World!!';
                            ~

[21:00:06] Found 1 error. Watching for file changes.

ファイルが増えても、監視は追従してくれるようです。

たとえば、先ほどのソースコードを2つのファイルに分割してみても問題なくビルドしてくれました。

src/app.ts

import express from 'express';

export const app = express();

app.get('/hello', (req, res) => {
  res.send('Hello World!!');
});

src/start.ts

import { app } from './app';

const port = 3000;

app.listen(port, () =>
  console.log(`[${new Date().toISOString()}] start server[${port}]`)
);

なんとなく、動作確認してみましょう。

$ node dist/start.js
[2021-11-28T12:05:28.360Z] start server[3000]

OKですね。

$ curl localhost:3000/hello
Hello World!!

package.jsonに登録して使うと良いのかな、と思います。

  "scripts": {
    "build": "tsc",
    "build:watch": "tsc --watch"
  },

こんな感じですね。

$ npm run build:watch

nodemon+ts-nodeで自動ビルド+再起動

tscwatch modeを試しましたが、アプリケーションを実行中にソースコードを変更しても

app.get('/hello', (req, res) => {
  res.send('Hello World!?');
});

ビルドは行われるものの起動しているアプリケーションには反映されないので、

$ curl localhost:3000/hello
Hello World!!

アプリケーション自体の再起動が必要になります。

ここも自動化しようと思うと、nodemonoとts-nodeを使えば良さそうです。

$ npm i -D ts-node nodemon

バージョンはこうなりました。

  "devDependencies": {
    "@types/express": "^4.17.13",
    "@types/node": "^16.11.10",
    "nodemon": "^2.0.15",
    "prettier": "2.5.0",
    "ts-node": "^10.4.0",
    "typescript": "^4.5.2"
  },
  "dependencies": {
    "express": "^4.17.1"
  }

ts-nodeは、Node.js用のTypeScript実行エンジン+REPLで、TypeScriptファイルを直接実行することができます。

GitHub - TypeStrong/ts-node: TypeScript execution and REPL for node.js

nodemonは、ソースコードの変更を監視して、サーバーを自動的に再起動できるツールです。

nodemon

GitHub - remy/nodemon: Monitor for any changes in your node.js application and automatically restart the server - perfect for development

nodemonは通常はJavaScriptファイルを指定するのですが、他のプログラムを起動する場合は--execオプションを
使います。

nodemon / Running non-node scripts

なので、今回はこんな感じに。

$ npx nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/start.ts

実行するとこのように表示され

[nodemon] 2.0.15
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src/**/*.ts
[nodemon] watching extensions: ts,json
[nodemon] starting `ts-node src/start.ts`
[2021-11-28T12:10:58.892Z] start server[3000]

ts-nodeを使ってアプリケーションが起動します。

$ curl localhost:3000/hello
Hello World!?

ここで、ソースコードを変更すると

app.get('/hello', (req, res) => {
  res.send('Hello World!!');
});

nodemonがソースコードの変更を検知して、その後にts-nodeで動作しているアプリケーションが再起動されます。

[nodemon] restarting due to changes...
[nodemon] starting `ts-node src/start.ts`
[2021-11-28T12:12:07.254Z] start server[3000]

変更が反映されていますね。

$ curl localhost:3000/hello
Hello World!!

ちなみに、この方法ではTypeScriptファイルをJavaScriptファイルに出力せずに実行しているため、ビルド結果を得たい
場合はtscコマンドを実行する必要があります。

また、コンパイルに失敗するような変更をするとこういう状態になり、アプリケーションも停止しますが

[nodemon] starting `ts-node src/start.ts`
/path/to/node_modules/ts-node/src/index.ts:750
    return new TSError(diagnosticText, diagnosticCodes);
           ^
TSError: ⨯ Unable to compile TypeScript:
src/app.ts:6:27 - error TS1005: ')' expected.

6   res.send('Hello World!!';
                            ~

    at createTSError (/path/to/node_modules/ts-node/src/index.ts:750:12)
    at reportTSError (/path/to/node_modules/ts-node/src/index.ts:754:19)
    at getOutput (/path/to/node_modules/ts-node/src/index.ts:941:36)
    at Object.compile (/path/to/node_modules/ts-node/src/index.ts:1243:30)
    at Module.m._compile (/path/to/node_modules/ts-node/src/index.ts:1370:30)
    at Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Object.require.extensions.<computed> [as .ts] (/path/to/node_modules/ts-node/src/index.ts:1374:12)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Module.require (node:internal/modules/cjs/loader:1005:19) {
  diagnosticText: "\x1B[96msrc/app.ts\x1B[0m:\x1B[93m6\x1B[0m:\x1B[93m27\x1B[0m - \x1B[91merror\x1B[0m\x1B[90m TS1005: \x1B[0m')' expected.\n" +
    '\n' +
    "\x1B[7m6\x1B[0m   res.send('Hello World!!';\n" +
    '\x1B[7m \x1B[0m \x1B[91m                          ~\x1B[0m\n',
  diagnosticCodes: [ 1005 ]
}
[nodemon] app crashed - waiting for file changes before starting...

この後、コンパイルできるソースコードに修正すると、正常に戻ります。

[nodemon] restarting due to changes...
[nodemon] starting `ts-node src/start.ts`
[2021-11-28T13:06:51.599Z] start server[3000]

package.jsonには、こんな感じに登録したら良いでしょうか。

  "scripts": {
    "build": "tsc",
    "build:watch": "tsc --watch",
    "build:live": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/start.ts"
  }

実行。

$ npm run build:live

まとめ

TypeScript+Node.jsのプロジェクトで、自動ビルドする方法を2種類試してみました。

どちらを使うかはケースバイケースな気はしますが、覚えておくと損はないかなと思います。