これは、なにをしたくて書いたもの?
以前、Node.jsの--inspect
オプションと組み合わせて、Node.jsアプリケーションをGoogle ChromeのDevToolsで
デバッグする方法を書いたことがあります。
Node.jsアプリケーションを、Google ChromeのDevToolsでデバッグする - CLOVER🍀
これ、ts-nodeを使うとTypeScriptでもできるのでは?と思って調べてみたら、できそうだったので試してみることに
しました。
Google Chrome DevToolsとNode.jsデバッガーの統合
Node.jsのDebuggerについてのドキュメントに、Google Chrome DevToolsとの統合について書かれています。
Debugger / V8 inspector integration for Node.js
簡単に書くと、--inspect
オプションを付与して起動することでDevToolsと接続できます。
また--inspect-brk
オプションを使うとアプリケーションの最初にブレークポイントが設置された状態で停止します。
ts-node
ts-nodeは、Node.js向けのTypeScript実行エンジンおよびREPLです。
GitHub - TypeStrong/ts-node: TypeScript execution and REPL for node.js
平たく言うと、ts-nodeを使うとTypeScriptファイルをプリコンパイルせずに直接実行できるようになります。
それで、機能紹介の中に以下のようにデバッガーなどと統合できることが書かれています。
Integrate with test runners, debuggers, and CLI tools
というか、Usageの中にまさに今回の内容が書かれています。
Note: If you need to use advanced node.js CLI arguments (e.g. --inspect), use them with node -r ts-node/register instead of ts-node's CLI.
今回はこちらを確認してみましょう。
また機能の中にこんな記述もあるので、こちらも軽く見ておきたいな、と。
Automatic sourcemaps in stack traces
スタックトレースが、TypeScriptファイルの表現で出力されるのは嬉しいですよね。
環境
今回の環境は、こちらです。
$ node --version v16.13.1 $ npm --version 8.1.2
プロジェクトの作成
サンプル用のプロジェクトを作成します。
$ npm init -y $ npm i -D typescript $ npm i -D -E prettier $ mkdir src
プログラムは、前に書いたエントリーをアレンジしたものにしましょう。
Node.jsの型宣言をインストール。
$ npm i -D @types/node@v16
ts-nodeをインストール。
$ npm i -D ts-node
バージョン。
$ npx ts-node --version v10.4.0
今回は、devDependencies
のみです。
"devDependencies": { "@types/node": "^16.11.17", "prettier": "2.5.1", "ts-node": "^10.4.0", "typescript": "^4.5.4" }
プログラムを作成する
サンプルプログラムを作成します。簡単な四則演算のHTTPサーバーです。
src/server.ts
import * as http from 'http'; const port = 3000; const logger = (fun: () => any) => console.log(`[${new Date().toISOString()}] ${fun.call(null)}`); const server = http.createServer((request, response) => { request.setEncoding('utf8'); request.on('data', (chunk) => { logger(() => `received data[${chunk}]`); const data = JSON.parse(chunk); const operator = data['operator']; const a = parseInt(data['a'], 10); const b = parseInt(data['b'], 10); const responseSender = (result: any) => response.end(JSON.stringify(result)); if (operator === '+') { responseSender({ result: a + b }); } else if (operator === '-') { responseSender({ result: a - b }); } else if (operator === '*') { responseSender({ result: a * b }); } else if (operator === '/') { responseSender({ result: a / b }); } else { logger(() => `Unknown operator[${operator}]`); console.error(new Error(`Unknown operator[${operator}]`)); response.statusCode = 400; responseSender({ message: `Unknown operator[${operator}]` }); } }); }); server.on('request', (request, response) => { const socket = request.socket; logger( () => `client connected[${socket.remoteAddress}:${socket.remotePort}] URL[${request.url} ${request.httpVersion}] Method[${request.method}]` ); }); server.listen(port, '0.0.0.0'); logger(() => 'Server startup');
軽く動作確認します。
ビルド。
$ npx tsc --project .
起動。
$ node dist/server.js [2022-01-01T06:13:34.138Z] Server startup
確認。
$ curl -XPOST localhost:3000 -d '{"operator": "+", "a": 1, "b": 2}' {"result":3} $ curl -XPOST localhost:3000 -d '{"operator": "-", "a": 5, "b": 1}' {"result":4} $ curl -XPOST localhost:3000 -d '{"operator": "*", "a": 3, "b": 2}' {"result":6} $ curl -XPOST localhost:3000 -d '{"operator": "/", "a": 4, "b": 2}' {"result":2} $ curl -XPOST -i localhost:3000 -d '{"operator": "?", "a": 1, "b": 1}' HTTP/1.1 400 Bad Request Date: Sat, 01 Jan 2022 06:14:05 GMT Connection: keep-alive Keep-Alive: timeout=5 Content-Length: 33 {"message":"Unknown operator[?]"}
この時のサーバーのログ。
[2022-01-01T06:13:52.886Z] client connected[127.0.0.1:54772] URL[/ 1.1] Method[POST] [2022-01-01T06:13:52.887Z] received data[{"operator": "+", "a": 1, "b": 2}] [2022-01-01T06:13:59.800Z] client connected[127.0.0.1:54774] URL[/ 1.1] Method[POST] [2022-01-01T06:13:59.800Z] received data[{"operator": "-", "a": 5, "b": 1}] [2022-01-01T06:14:02.144Z] client connected[127.0.0.1:54776] URL[/ 1.1] Method[POST] [2022-01-01T06:14:02.144Z] received data[{"operator": "*", "a": 3, "b": 2}] [2022-01-01T06:14:03.749Z] client connected[127.0.0.1:54778] URL[/ 1.1] Method[POST] [2022-01-01T06:14:03.749Z] received data[{"operator": "/", "a": 4, "b": 2}] [2022-01-01T06:14:05.066Z] client connected[127.0.0.1:54780] URL[/ 1.1] Method[POST] [2022-01-01T06:14:05.066Z] received data[{"operator": "?", "a": 1, "b": 1}] [2022-01-01T06:14:05.066Z] Unknown operator[?] Error: Unknown operator[?] at IncomingMessage.<anonymous> (/path/to/dist/server.js:48:27) at IncomingMessage.emit (node:events:390:28) at IncomingMessage.Readable.read (node:internal/streams/readable:527:10) at flow (node:internal/streams/readable:1012:34) at resume_ (node:internal/streams/readable:993:3) at processTicksAndRejections (node:internal/process/task_queues:83:21)
スタックトレースは、JavaScriptのものですね。当然ですが。
動作確認は終わったので、1度ビルド後のJavaScriptファイルを削除します。
$ rm -rf dist
ts-node+Google Chrome DevToolsでデバッグする
次は、アプリケーションをTypeScriptファイルのままts-nodeで起動してみましょう。
$ npx ts-node src/server.ts [2022-01-01T06:16:25.389Z] Server startup
確認。
$ curl -XPOST localhost:3000 -d '{"operator": "+", "a": 1, "b": 2}' {"result":3} $ curl -XPOST localhost:3000 -d '{"operator": "-", "a": 5, "b": 1}' {"result":4} $ curl -XPOST localhost:3000 -d '{"operator": "*", "a": 3, "b": 2}' {"result":6} $ curl -XPOST localhost:3000 -d '{"operator": "/", "a": 4, "b": 2}' {"result":2} $ curl -XPOST -i localhost:3000 -d '{"operator": "?", "a": 1, "b": 1}' HTTP/1.1 400 Bad Request Date: Sat, 01 Jan 2022 06:16:56 GMT Connection: keep-alive Keep-Alive: timeout=5 Content-Length: 33 {"message":"Unknown operator[?]"}
問題なく動いています。
[2022-01-01T06:16:40.511Z] client connected[127.0.0.1:54824] URL[/ 1.1] Method[POST] [2022-01-01T06:16:40.512Z] received data[{"operator": "+", "a": 1, "b": 2}] [2022-01-01T06:16:43.999Z] client connected[127.0.0.1:54826] URL[/ 1.1] Method[POST] [2022-01-01T06:16:43.999Z] received data[{"operator": "-", "a": 5, "b": 1}] [2022-01-01T06:16:47.333Z] client connected[127.0.0.1:54828] URL[/ 1.1] Method[POST] [2022-01-01T06:16:47.334Z] received data[{"operator": "*", "a": 3, "b": 2}] [2022-01-01T06:16:53.373Z] client connected[127.0.0.1:54830] URL[/ 1.1] Method[POST] [2022-01-01T06:16:53.374Z] received data[{"operator": "/", "a": 4, "b": 2}] [2022-01-01T06:16:56.394Z] client connected[127.0.0.1:54832] URL[/ 1.1] Method[POST] [2022-01-01T06:16:56.394Z] received data[{"operator": "?", "a": 1, "b": 1}] [2022-01-01T06:16:56.394Z] Unknown operator[?] Error: Unknown operator[?] at IncomingMessage.<anonymous> (/path/to/src/server.ts:33:21) at IncomingMessage.emit (node:events:390:28) at IncomingMessage.emit (node:domain:475:12) at IncomingMessage.Readable.read (node:internal/streams/readable:527:10) at flow (node:internal/streams/readable:1012:34) at resume_ (node:internal/streams/readable:993:3) at processTicksAndRejections (node:internal/process/task_queues:83:21)
ログを見ると、スタックトレースは確かにTypeScriptファイルのものになっていますね。
次に、--inspect
オプションを付与してみましょう。
ドキュメントを見ると、起動方法はts-node
からnode
と-r
オプションの組み合わせになるようです。
node -r ts-node/register
node -r ts-node/register/transpile-only
transpile-onlyについては今回は扱いませんが、こちらのドキュメントを見ると良さそうです。
Third-party transpilers | ts-node
では、試してみます。
$ node --inspect -r ts-node/register src/server.ts Debugger listening on ws://127.0.0.1:9229/e7007673-d38d-43cd-a64c-31d625be729a For help, see: https://nodejs.org/en/docs/inspector [2022-01-01T06:23:12.034Z] Server startup
この状態でGoogle Chromeの新しいタブを開き、chrome://inspect
と入力します。
デバッグ可能なNode.jsのプロセスが表示されるので、対象のプロセスのinspect
リンクを選択。
デバッガーが開きますが、ソースコードが見えません。
「Open file」で開けば良さそうです。
こんな感じで絞り込めます。
今回作成したソースコードが2つ表示されていますが、[sm]
の方はSource Mapを意味しているみたいですね。
なにもついてない方を選ぶと「Source map detected.」と言われます。というか、中身が.js
ファイルでした。
ソースコードを選択した後は、ブレークポイントを配置して好きにデバッグしましょう。
次に、--inspect
を--inspect-brk
に変えてみます。
$ node --inspect-brk -r ts-node/register src/server.ts Debugger listening on ws://127.0.0.1:9229/d28263c8-1801-4bc9-a7aa-c33b19fc168b For help, see: https://nodejs.org/en/docs/inspector
こちらの場合は、トランスパイル後の方のファイルにブレークポイントが付けられてしまうようです。
ですが、ステップ実行していくと途中からTypeScriptファイルの方に移ってくるので、実質問題にはならないかもですね。
確認したいことは、だいたい見れたのでOKです。
オマケ
chrome://inspect
から開いていくのが面倒な場合は、NIMを使うとよいでしょう。
NIM (Node Inspector Manager) - Chrome ウェブストア
まとめ
TypeScriptファイルのままデバッグができないかな?と思って、以前調べたGoogle Chrome DevToolsとの統合を
ts-nodeを使うことで同じようにできることを確認できました。
ts-nodeのインストールは必要になりますが、覚えておくと便利そうですね。