CLOVER🍀

That was when it all began.

ts-node+Google Chrome DevToolsで、TypeScript+Node.js環境のデバッグ(node --inspect)を行う

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

以前、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です。

ts-node | ts-node

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

平たく言うと、ts-nodeを使うとTypeScriptファイルをプリコンパイルせずに直接実行できるようになります。

それで、機能紹介の中に以下のようにデバッガーなどと統合できることが書かれています。

Integrate with test runners, debuggers, and CLI tools

Overview / Features

というか、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.

Usage / Programmatic

今回はこちらを確認してみましょう。

また機能の中にこんな記述もあるので、こちらも軽く見ておきたいな、と。

Automatic sourcemaps in stack traces

Overview / Features

スタックトレースが、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

Usage / Programmatic

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リンクを選択。

f:id:Kazuhira:20220101152537p:plain

デバッガーが開きますが、ソースコードが見えません。

f:id:Kazuhira:20220101153005p:plain

「Open file」で開けば良さそうです。

f:id:Kazuhira:20220101153532p:plain

こんな感じで絞り込めます。

f:id:Kazuhira:20220101153739p:plain

今回作成したソースコードが2つ表示されていますが、[sm]の方はSource Mapを意味しているみたいですね。
なにもついてない方を選ぶと「Source map detected.」と言われます。というか、中身が.jsファイルでした。

ソースコードを選択した後は、ブレークポイントを配置して好きにデバッグしましょう。

f:id:Kazuhira:20220101154158p:plain

次に、--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

こちらの場合は、トランスパイル後の方のファイルにブレークポイントが付けられてしまうようです。

f:id:Kazuhira:20220101155149p:plain

ですが、ステップ実行していくと途中からTypeScriptファイルの方に移ってくるので、実質問題にはならないかもですね。

f:id:Kazuhira:20220101155439p:plain

確認したいことは、だいたい見れたのでOKです。

オマケ

chrome://inspectから開いていくのが面倒な場合は、NIMを使うとよいでしょう。

NIM (Node Inspector Manager) - Chrome ウェブストア

まとめ

TypeScriptファイルのままデバッグができないかな?と思って、以前調べたGoogle Chrome DevToolsとの統合を
ts-nodeを使うことで同じようにできることを確認できました。

ts-nodeのインストールは必要になりますが、覚えておくと便利そうですね。