これは、なにをしたくて書いたもの?
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}]`) );
これで、サンプルアプリケーションはできました。
tscのwatch 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で自動ビルド+再起動
tscのwatch 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は通常は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種類試してみました。
どちらを使うかはケースバイケースな気はしますが、覚えておくと損はないかなと思います。