Visual Studio Code でモジュールバンドラの watch モードとデバッガの起動終了を連動させる launch.json

スタッフブログ

皆様どうも、こんにちは!
こまりのフロントエンドとかのエンジニア、桑木です。

先に断っておくと、この方法でデバッガを起動した場合でも、HMRした時にブレークポイントやステップ実行などのデバッガの機能に不具合が出ることがあります。

HMR時にsource mapまで更新しないことが原因のようです。webpackであればinline source mapで対処できる、というような情報もありますが特に試していません。

とりあえず、ブラウザ側でページ全体をリロードして対処していますが、なにか情報があれば教えてください。

クライアントのウェブアプリはlaunch.jsonを設定しなくてもデバッグできる

そもそも、最近のブラウザには優秀なJavaScriptのデバッガが搭載されているので、VSCodeのデバッガを利用しなくても開発には困りません。

webpackなどのモジュールバンドラを利用している場合も、ビルドオプションでmapファイルを生成しておけば、ブラウザのデバッガが利用できます。

手間があるとしても、ターミナルからnpm startとかでwatchモードでバンドラを起動して、ブラウザでローカルホストのページを開くだけです。

とはいえ、せっかくだからそれも自動化するぜ

実は先日、モジュールバンドラをwebpackからParcelに乗り換えました。webpackに比べればできることに制限があるとは言われるものの、複雑な設定が不要で明快に使えるのは精神的にとても良いですね。

Parcelに乗り換えてすっきりしたついでに、長年(半年くらい)放置していたクライアント側のデバッグもlaunch.jsonに設定しました。

ここから本番

とりあえず次のようなディレクトリ構造だとします。

  • プロジェクトルート
    • .vscode
      • launch.json
      • tasks.json
    • app
      • src
      • public
      • dist
    • server
      • bin
      • src

プロジェクトルートの下に、appserverの2つのnpmパッケージディレクトリがあります。

Chromeと連携させるため、VSCodeには拡張機能の"Debugger for Chrome"をインストールしておきます。この拡張機能を入れることで、Chromeで動作しているJavaScriptにアタッチすることができるようになります。

そして、launch.jsonとtasks.jsonはこちら。

// launch.json
{
    // IntelliSense を使用して利用可能な属性を学べます。
    // 既存の属性の説明をホバーして表示します。
    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "server",
            "cwd": "${workspaceFolder}/server",
            "program": "${workspaceFolder}/server/bin/www",
            "envFile": "${workspaceFolder}/server/.env",
            "env": {
                "NODE_ENV": "development"
            },
            "outputCapture": "std"
        },
        {
            "type": "chrome",
            "request": "launch",
            "name": "app",
            "url": "http://localhost:3000",
            "webRoot": "${workspaceFolder}/app/dist",
            "preLaunchTask": "app start",
            "postDebugTask": "app stop"
        }
    ],
    "compounds": [
        {
            "name": "all",
            "configurations": ["server", "app"]
        }
    ]
}
// tasks.json
{
    // tasks.json 形式の詳細についての資料は、
    // https://go.microsoft.com/fwlink/?LinkId=733558 をご覧ください
    "version": "2.0.0",
    "tasks": [
        {
            "label": "app start",
            "type": "npm",
            "script": "start",
            "path": "app/",
            "isBackground": true,
            "problemMatcher": {
                "pattern": [
                    {
                        "regexp": ".",
                        "file": 1,
                        "location": 2,
                        "message": 3
                    }
                ],
                "background": {
                    "activeOnStart": true,
                    "beginsPattern": "^.*",
                    "endsPattern": "^.*Built in.*",
                }
            }
        },
        {
            "label": "app stop",
            "type": "shell",
            "command": "echo ${input:terminate}"
        }
    ],
    "inputs": [
        {
            "id": "terminate",
            "type": "command",
            "command": "workbench.action.tasks.terminate",
            "args": "app start"
        }
    ]
}

appではモジュールバンドラとしてParcelを使用していて、npm scriptsのstartにはparcel watch public/index.htmlが指定されています。これでnpm startを実行すればParcelがwatchモードで起動します。

出力ディレクトリを特に指定していないので、この場合はapp/distにビルド結果が出力されます。

serverはexpress-generatorで生成したExpressアプリで、server/bin/wwwがエントリーポイントになっていて、起動後はhttp://localhost:3000で接続を待ち受けます。また、express.staticapp/distを指定しています。

設定解説

launch.json

preLaunchTaskpostDebugTaskを指定しておけば、それぞれデバッガの起動前と終了後に指定したtaskを実行させることができます。今回は、preLaunchTaskにParcelをwatchモードで起動させるタスク、postDebugTaskにそのタスクを強制終了させるタスクを指定しています。

タスク自体の設定はtasks.jsonに記述して、label名をこのプロパティに指定して参照します。(label名による参照でなく、直接タスクの内容をこのプロパティに記述することもできますが、すべてのタスク機能には対応していないので専用のtasks.jsonを使用したほうが良いでしょう)

デバッガ自体は、Chromeの新しいウィンドウでローカルホストのページを開く設定にしています。

また、compoundsを設定することで複数デバッガの同時起動をさせることができます。今回はクライアントのウェブアプリだけでなく、Expressサーバも同時に起動するように設定しました。

launch.jsonの詳しいようであまり詳しくない説明は、VSCode公式サイトの"Debugging in Visual Studio Code"のページ書かれているので参照してください。

tasks.json

app start

app/npm run startするように設定したタスクです。前述のpreLaunchTaskから参照しています。

preLaunchTaskは名前の通りデバッガの起動前に実行するタスクですが、そのタスクが終了するまでデバッガは起動されません。これはコンパイルなどで実行ファイルを生成してから起動させるための挙動です。

しかしwatchモードの場合、コンパイルが完了してもプロセスは終わらないのでデバッガが起動しないことになります。そこでisBackgroundtrueを指定してタスクのバックグラウンド動作を有効にします。

ただし、バックグラウンドへ移行させるにはproblemMatcherも指定する必要があります。このプロパティはコンパイラが出力するエラー情報をVSCodeで取得するためのものですが、今回はバックグラウンドへの移行に必要なproblemMatcher.background.endsPatternだけ適切に設定します。ここに指定した正規表現にマッチすれば、タスクがバックグラウンドへ移行します。

その他のプロパティも指定しておかないとproblemMatcherとして認識しないので、問題の起こらない範囲で適当に指定しておきます。

tasks.jsonの公式の説明は"Tasks in Visual Studio Code"を参照してください。

app stop

前項で起動しっぱなしのタスクを強制終了させるタスクです。これを前述のpostDebugTaskから参照することで、デバッガの終了と連動できます。

しかし、タスク自身の機能で他のタスクを終了させるような直接的な機能はありません。

そこで、VSCodeの内部コマンドworkbench.action.tasks.terminatecommandタイプのinput変数を参照することで実行します。

内部コマンドを実行する変数としてcommand変数もありますが、command変数には引数を指定できません。workbench.action.tasks.terminateは引数に終了させるタスク名を指定する必要があるので、引数を指定できるinput変数のcommandタイプを利用します。

このinput変数は、タスク実行時にユーザーの入力や選択を求めることもできる特殊な変数です。詳しい説明は公式サイトの"Visual Studio Code Variables Reference"を参照してください。

まぁまぁ実用的にはなったかな

以上でモジュールバンドラとデバッガが連動するようになったと思います。F5一発でサーバとクライアントが起動するのは楽ですね。

VSCodeは機能が盛り沢山なので、皆さんもドキュメントを見て便利な機能を見つけてみてください。