Node.jsアプリケーションを単一exeファイルにする

社内で [NestJS](https://nestjs.com/) を利用したアプリケーションを構築した後
exe ファイルへの変換と Windows サービスへの登録が必要になりました。

あまり情報がなかったので備忘録的に書き残します。

NestJS だけでなく Node.js を利用したアプリケーション全般に適用できる内容かと思います。

# 環境
- Windows10 64bit
- Node.js 14.18.1

# [NestJS](https://nestjs.com/) とは(一応)

- Node.js Web フレームワーク
- TypeScript の完全なサポート
- ORM(TypeORM, Sequelize)のサポート
- GraphQL のサポート
- Express を超える抽象化
- Angular のアーキテクチャから影響を受けている
- Guards を利用した認証
- WebSockets のサポート

など…Express で後々必要になる機能が最初から用意されており、ドキュメントも充実しています。
なにより初学者は TypeScript の環境作成だけで苦労するので、最初から ts-node を利用した環境が用意されるのはありがたいです。
Module, Controller, Provider(Service)の概念を理解するのに少し苦労しますが Angular を使用したことがある方なら特に問題はないでしょう。

また、とっつきにくかった GraphQL をコードファーストで定義することができ、理解するのにとても役立ちました。
実装とスキーマ定義を同期させながら開発ができることも魅力でした。(一般的にはスキーマファーストで開発するのが主だとは思いますが)
例えば lerna + yarn workspaces + NestJS + NextJS の構成にしてフロント、バックエンドで同じスキーマ定義ファイルを参照し開発を進められます。

# 概要

簡単な NestJS プロジェクトの作成と exe ファイルへの変換の手順を示します。

# プロジェクトの作成

Nest CLI をインストール

```bash
npm i -g @nestjs/cli
```

プロジェクトの作成

```bash
nest new <project-name>
```

パッケージマネージャの指定はお好みで。この時点でサーバーを立てられる状態になっています。

```bash
cd <project-name>
npm run start
```

http://localhost:3000/ に GET リクエストを投げれば `Hello World!` が返ってきます。

# ビルド

```bash
npm run build
```

`dist`フォルダに js ファイルが作成され、以下コマンドで起動できます。

```bash
node dist/main
# or "npm run start:prod"
```

この `dist/main.js` をエントリポイントとする exe ファイルの作成を行います。

# [nexe](https://github.com/nexe/nexe) のインストール

[nexe](https://github.com/nexe/nexe) は Node.js アプリケーションを単一 exe ファイルにコンパイルしてくれるツールです。

>同様なツールに [pkg](https://github.com/vercel/pkg) がありこちらの方が手軽だったのですがアイコンの変更やバージョン情報の書き換えを行うと起動できなくなったのでやめました。

NestJS プロジェクトの devDependencies に追加します。

```bash
npm i nexe --save-dev
```

# exe ファイルの作成

以下コマンドを実行します。

```bash
./node_modules/.bin/nexe dist/main.js --target windows-x64-14.15.3 --output NestJS.exe
```

`--target` を省略すると Windows にインストールされている Node.js 環境のダウンロードを試みるのですが、https://github.com/nexe/nexe/releases にないバージョンだと

```
Error: https://github.com/nexe/nexe/releases/download/v3.3.3/windows-x64-14.18.1 is not available, create it using the --build flag
```

と表示されるので `windows-x64-14.15.3` を直接指定しています。

# アイコンの作成・変更

#### アイコンの作成

[ImageMagick](https://imagemagick.org/index.php)を使用して`svg`ファイルから`ico`ファイルを作成します。

とりあえず [NestJS アイコン](https://camo.githubusercontent.com/5f54c0817521724a2deae8dedf0c280a589fd0aa9bffd7f19fa6254bb52e996a/68747470733a2f2f6e6573746a732e636f6d2f696d672f6c6f676f2d736d616c6c2e737667)の `svg`ファイル を使用して `ico`ファイルを作成することにします。

```bash
./magick.exe convert icon.svg -transparent white -define icon:auto-resize icon.ico
```

#### アイコンの削除

[Resource Hacker](http://www.angusj.com/resourcehacker/) を使用して初期アイコンを削除します。

```bash
./ResourceHacker.exe -open NestJS.exe -save NestJS.exe -action delete -mask ICONGROUP,1,1033
```

```
Current Directory:
D:\Node.js\Nest\executable

Commandline:
"D:\Node.js\Nest\executable\ResourceHacker.exe" -open NestJS.exe -save NestJS.exe -action delete -mask ICONGROUP,1,1033

Open    : D:\Node.js\Nest\executable\NestJS.exe
Save    : D:\Node.js\Nest\executable\NestJS.exe

  Deleted: ICONGROUP,1,1033

Success!
```

#### アイコンの追加

```bash
./ResourceHacker.exe -open NestJS.exe -save NestJS.exe -action addoverwrite -resource <ico FilePath> -mask ICONGROUP,MAINICON,0
```

```
Current Directory:
D:\Node.js\Nest\executable

Commandline:
"D:\Node.js\Nest\executable\ResourceHacker.exe" -open NestJS.exe -save NestJS.exe -action addoverwrite -resource icon.ico -mask ICONGROUP,MAINICON,0

Open    : D:\Node.js\Nest\executable\NestJS.exe
Save    : D:\Node.js\Nest\executable\NestJS.exe
Resource: D:\Node.js\Nest\executable\icon.ico

  Added: ICONGROUP,MAINICON,0

Success!
```

# バージョン情報の編集

#### Rc ファイル作成

以下の内容のファイルを `utf-16` で `VersionInfo.rc` として保存します。

```text:VersionInfo.rc
1 VERSIONINFO
FILEVERSION 1,0,0,0
PRODUCTVERSION 1,0,0,0
FILEOS 0x4
FILETYPE 0x1
{
  BLOCK "StringFileInfo"
  {
    BLOCK "041103a4"
    {
      VALUE "CompanyName", "(C) xxx CORPORATION"
      VALUE "FileDescription", "NestJS"
      VALUE "FileVersion", "1.0.0.0"
      VALUE "InternalName", "NestJS.exe"
      VALUE "LegalCopyright", "(C) xxx CORPORATION"
      VALUE "OriginalFilename", "NestJS.exe"
      VALUE "PrivateBuild", "0000"
      VALUE "ProductName", "NestJS"
      VALUE "ProductVersion", "1.0.0.0"
      VALUE "SpecialBuild", "0000"
    }
  }
  BLOCK "VarFileInfo"
  {
    VALUE "Translation", 0x0411 0x03A4
  }
}
```

#### Res ファイル作成

```bash
./ResourceHacker.exe -open VersionInfo.rc -save VersionInfo.res -action compile
```

```
Current Directory:
D:\Node.js\Nest\executable

Commandline:
"D:\Node.js\Nest\executable\ResourceHacker.exe" -open VersionInfo.rc -save VersionInfo.res -action compile

Open    : D:\Node.js\Nest\executable\VersionInfo.rc
Save    : D:\Node.js\Nest\executable\VersionInfo.res

Compiling: D:\Node.js\Nest\executable\VersionInfo.rc
Success!
```

#### 初期バージョン情報の削除

```bash
./ResourceHacker.exe -open NestJS.exe -save NestJS.exe -action delete -mask VERSIONINFO,1,1033
```

```
Current Directory:
D:\Node.js\Nest\executable

Commandline:
"D:\Node.js\Nest\executable\ResourceHacker.exe" -open NestJS.exe -save NestJS.exe -action delete -mask VERSIONINFO,1,1033

Open    : D:\Node.js\Nest\executable\NestJS.exe
Save    : D:\Node.js\Nest\executable\NestJS.exe

  Deleted: VERSIONINFO,1,1033

Success!
```

#### バージョン情報の追加

```bash
./ResourceHacker.exe -open NestJS.exe -save NestJS.exe -action addoverwrite -resource VersionInfo.res
```

```
Current Directory:
D:\Node.js\Nest\executable

Commandline:
"D:\Node.js\Nest\executable\ResourceHacker.exe" -open NestJS.exe -save NestJS.exe -action addoverwrite -resource VersionInfo.res

Open    : D:\Node.js\Nest\executable\NestJS.exe
Save    : D:\Node.js\Nest\executable\NestJS.exe
Resource: D:\Node.js\Nest\executable\VersionInfo.res

  Added: VERSIONINFO,1,1041
```

これでアイコンとバージョン情報を設定した exe ファイルが作成できました。

もし.env など参照ファイルがあれば exe ファイルと同じ場所に配置してください。

モジュールが見つからないとエラーが表示された場合は、該当のモジュールのみ含む node_module フォルダを作成して exe ファイルと同じ場所に配置してください。

```
Cannot find module 'xxx' or its corresponding type declarations.
```

# Windows サービスへの登録

[NSSM](https://nssm.cc/) を使用し、作成した exe ファイルを Windows サービスとして登録します。
使い方は公式を参照すれば問題ないと思いますが、管理者権限が必要なことに注意してください。

サービスへの登録はこんな感じ。

```bash
./nssm.exe install NestJSService NestJS.exe
./nssm.exe set NestJSService Description "NestJSアプリケーション"
./nssm.exe set NestJSService AppNoConsole 1
./nssm.exe set NestJSService AppRestartDelay 1000
./nssm.exe start NestJSService
```

```
Service "NestJSService" installed successfully!
Set parameter "Description" for service "NestJSService".
Set parameter "AppNoConsole" for service "NestJSService".
Set parameter "AppRestartDelay" for service "NestJSService".
NestJSService: START: この操作を正しく終了しました。
```

サービスから削除する場合はこう。

```bash
./nssm.exe stop NestJSService
./nssm.exe remove NestJSService confirm
```

```
NestJSService: STOP: この操作を正しく終了しました。
Service "NestJSService" removed successfully!
```

# 終わりに

「製品の NestJS のコードを見られないように隠蔽したい」という要望から調査を始めましたが、
かなり特殊な手順になってしましました。nexe のメンテナンスも現在は停滞しているようで、いつまでこの手順が使えるのかは分かりません。(Electronはどうやってるんだろう)

素直にexeファイルへのコンパイルがサポートされている言語で開発した方がよかったと思います。

# 追記 (2024/01/05)
公式でexe化がサポートされました。まだ実験段階っぽいですが、可能ならこちらを利用した方が良いと思います。
[Single executable applications](https://nodejs.org/api/single-executable-applications.html)