シェルにも使える!nodemon を使ってコードに変更があった場合にホットリロード(自動再起動)させる

2022-02-28Bash,JavaScript,PHP

はじめに

Node.js でアプリケーション開発をしている際に、コード変更が行われたら自動的に再起動したいと思いました。
ちょうど GraphQL を使ってアプリケーションを開発していたのですが、 GraphQL のスキーマ変更は( Apollo Server の) 再起動を行わないと変更が反映されません。

nodemon というツールを使うことで簡単に実現できました。

作業環境

$ node --version
v15.12.0

「nodemon」 ってなに?

  • 2021-04-09 現在で 1500 万を超えるプロジェクトで利用されているツール
  • ソースコードの変更を監視している
  • 変更があったら自動定期にサーバプロセスを再起動してくれる

「nodemon」 の使い方は?

とても簡単です。
node コマンドの代わりに nodemon コマンドを使います。ただそれだけ。
それだけで、コードの変更を検知したら自動的に再起動してくれます。

「nodemon」 コマンドの特徴

ほぼこれを使えば困ることがないぐらい、機能豊富です。
特徴を上げてみます。

  • アプリケーションを自動的に再起動してくれます
  • いくつかの拡張子に該当するファイルを自動的に探して監視してくれます
    • .js
    • .mjs
    • .coffee
    • .litcoffee
    • .json
  • デフォルト挙動では node コマンド + 監視機能 として動作しますが、 python コマンド + 監視機能 や、 ruby コマンド、 make など、任意のコマンド実行に切替可能です
  • 指定したファイルやディレクトリを監視対象から外すこともできます
  • 指定したディレクトリだけを監視させることができます
  • 自作のスクリプトから呼びだあすことも可能

インストール方法

npm コマンドが使えれば簡単にインストールできます。

$ npm install -g nodemon

「システムのグローバル領域にインストールしたくない!」、「バージョン依存の問題を回避するためプロジェクトディレクトリにインストールしたい」といった場合は以下のコマンドでも OK。

$ npm install --save-dev nodemon

プロジェクトディレクトリにインストールされた nodemon を使いたい場合、 npx nodemon コマンドを実行するのが良いでしょう。

使い方

nodemon コマンドは node コマンドのラッパーです。
node コマンドで利用できる引数はそのまま利用できます。

例えば node -c でコードチェックを行えますが、 nodemon -c でもコードチェックを行えます。

// index.js
console.log("hello1");
console.log("hello2" // ← わざと括弧を閉じていない
# node コマンドでチェック。 括弧が閉じられていないエラーが発生
$ node -c index.js
/private/tmp/work/index.js:3
console.log("hello2"
            ^^^^^^^^

SyntaxError: missing ) after argument list
    at Object.compileFunction (node:vm:355:18)
    at wrapSafe (node:internal/modules/cjs/loader:1022:15)
    at checkSyntax (node:internal/main/check_syntax:66:3)
    at node:internal/main/check_syntax:39:3

# nodemon コマンドでチェック。 括弧が閉じられていないエラーが発生
$ nodemon -c index.js
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node -c index.js`
/private/tmp/work/index.js:3
console.log("hello2"
            ^^^^^^^^

SyntaxError: missing ) after argument list
    at Object.compileFunction (node:vm:355:18)
    at wrapSafe (node:internal/modules/cjs/loader:1022:15)
    at checkSyntax (node:internal/main/check_syntax:66:3)
    at node:internal/main/check_syntax:39:3
[nodemon] app crashed - waiting for file changes before starting...

nodemon コマンドはチェックを実行しても、コマンドが終了しません。
この状態で index.js を正しく修正し保存をすると、再度チェックが実行されます。
今度は以下のように何も表示されません。( コードチェックの結果、問題がなかった

[nodemon] restarting due to changes...
[nodemon] starting `node -c index.js`
[nodemon] clean exit - waiting for changes before restart

出力されているログの見方

先程実行した nodemon -c コマンドのログを見てみます。

$ nodemon -c index.js
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node -c index.js`
/private/tmp/work/index.js:3
console.log("hello2"
            ^^^^^^^^

SyntaxError: missing ) after argument list
    at Object.compileFunction (node:vm:355:18)
    at wrapSafe (node:internal/modules/cjs/loader:1022:15)
    at checkSyntax (node:internal/main/check_syntax:66:3)
    at node:internal/main/check_syntax:39:3

[nodemon] という文言が前方についている行とついていない行があります。
[nodemon] という文言がついている行は、 nodemon コマンド自体が出力した情報となります。
[nodemon] という文言がついていない行は、 nodemon コマンドに指定したスクリプト(ここでは index.js ですね)が出力したログになります。

これを知っていれば、ログの見方がわかります。

nodemon は CLI ツール開発時にも利用できる

nodemon はもともとハングしたサーバプロセス(例えば Web サーバ)を再起動するために作られたようです。

では、実行したらすぐに終了するようなプログラムの場合は使えないのか?というとそういうわけでは有りません。

試してみます。以下のようなスクリプトを作成します。

// date.js
console.log("---");
console.log(new Date());
console.log("---");

このスクリプトを nodemon コマンドで実行します。

$ nodemon date.js
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node date.js`
---
2021-04-08T23:14:25.262Z
---
[nodemon] clean exit - waiting for changes before restart

ここで、スクリプトの内容を変更します。

// date.js
console.log("---");
console.log("*****");
console.log(new Date());
console.log("*****");
console.log("---");

保存するとすぐにスクリプトが自動的に再実行されます。

$ nodemon date.js
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node date.js`
---
2021-04-08T23:14:25.262Z
---
[nodemon] clean exit - waiting for changes before restart
[nodemon] restarting due to changes...
[nodemon] starting `node date.js`
---
*****
2021-04-08T23:16:09.721Z
*****
---
[nodemon] clean exit - waiting for changes before restart

ちょっとしたスクリプトを開発しているときでもこれは便利です。

nodemon を明示的に再起動する

実行中の nodemon コマンドですが、再起動したい場合は nodemon の停止nodemon の起動 を行う方法とは別の方法も用意されています。

実行中のターミナルで rs とタイプし、最期に ENTER を入力します。

---
*****
2021-04-08T23:21:11.221Z
*****
---
[nodemon] clean exit - waiting for changes before restart
rs # <<<<< ここでrs<ENTER>を入力している
[nodemon] starting `node date.js`
---
*****
2021-04-08T23:21:13.482Z
*****
---
[nodemon] clean exit - waiting for changes before restart

PHP コードを監視させてみる

PHP でちょっとした CLI ツールを作成してみたりしている時に使えるかもためしてみました。

まずは index.php を作成します。以下のコード

# phpがインストールされていることを確認
$ php --version
PHP 7.2.34 (cli) (built: Feb 27 2021 17:44:53) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.34, Copyright (c) 1999-2018, by Zend Technologies

# phpスクリプトを作成
cat <<'EOF' >index.php
#!/usr/bin/env php
<?php

echo "Hello, World", PHP_EOL;
EOF

このファイルを nodemon で実行してみます。

$ nodemon index.php
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: php,json
[nodemon] starting `node index.php`
/private/tmp/work/index.php:2
<?php
^

SyntaxError: Unexpected token '<'
    at Object.compileFunction (node:vm:355:18)
    at wrapSafe (node:internal/modules/cjs/loader:1022:15)
    at Module._compile (node:internal/modules/cjs/loader:1056:27)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1121:10)
    at Module.load (node:internal/modules/cjs/loader:972:32)
    at Function.Module._load (node:internal/modules/cjs/loader:813:14)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12)
    at node:internal/main/run_main_module:17:47
[nodemon] app crashed - waiting for file changes before starting...

エラーが発生してしまいました。
これは監視対象の index.php スクリプトを node コマンドで実行しようとしたためです。
php コマンドを使って実行するように指定します。 --exec php というオプションを付与します。

$ nodemon index.php --exec php
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: php,json
[nodemon] starting `php index.php`
Hello, World

うまくいきました。

ここで index.php を少し編集してみます。

#!/usr/bin/env php
<?php

echo "Hello, World", PHP_EOL;
echo "Hello, World again", PHP_EOL;

保存すると即座に再実行されます。
追加されたロジックが反映されていますね。

[nodemon] restarting due to changes...
[nodemon] starting `php index.php`
Hello, World
Hello, World again
[nodemon] clean exit - waiting for changes before restart

実行時オプションを設定ファイルに保存する

毎回実行するようなオプションは設定ファイルに書き出して置くことができます。
特に拡張仕事に実行するコマンドはほぼ決まっていると思いますので、拡張子と実行コマンドのマッピングを事前に行っておくと便利です。

設定ファイルはプロジェクトごとのディレクトリに配置してもいいですし、ユーザのホームディレクトリに配置しても構いませんが、 nodemon.json という名前にしておく必要があります。

先程の PHP スクリプト実行時に --exec php オプションを付け忘れてしまいましたが、忘れていても正常に動作するようにしてみます。

nodemon.json を作成します。

{
  "execMap": {
    "php": "php"
  }
}

たったこれだけです。 "php": "php" の記述ですが、左がファイルの拡張子を表しています。右がその拡張子のファイルを実行する際に利用するコマンドです。

では先程の index.php を実行してみます。

$ nodemon index.php
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: php,json
[nodemon] starting `php index.php`
Hello, World
Hello, World again
[nodemon] clean exit - waiting for changes before restart

正しく実行されました。

シェルスクリプトも自動実行させてみる

シェルスクリプトも自動実行させてみたいと思います。
nodemon.json を修正します。

{
  "execMap": {
    "php": "php",
    "sh": "bash"
  }
}

先程のファイルに一行追加しました。

さらに適当なスクリプトを作成し、 nodemon に監視させます。

$ touch main.sh

$ nodemon main.sh
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: sh,json
[nodemon] starting `bash main.sh`
[nodemon] clean exit - waiting for changes before restart

main.sh を修正します。

#!/usr/bin/env bash

echo "*****"
date
echo "*****"

保存すると、 nodemon 実行ログに出力内容が追加されます。

[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: sh,json
[nodemon] starting `bash main.sh`
[nodemon] clean exit - waiting for changes before restart
[nodemon] restarting due to changes...
[nodemon] starting `bash main.sh`
*****
金  4  9 08:40:25 JST 2021
*****
[nodemon] clean exit - waiting for changes before restart

ひとこと

シェルスクリプト開発時に重宝しそうですね。
JavaScript 界隈は進化が早いのでおじさんには生きにくい世界です。 (^_^;)

2022-02-28Bash,JavaScript,PHP