この記事は、株式会社ビットキー Developer Advent Calendar 2023 19日目の記事です。
tl;dr
ISUCON13で実際に遭遇した問題を題材に、tsconfig.jsonのlib
に設定を追加してECMA Scriptの新しい機能を追加する方法と、Node.jsをNative ES Modulesとして動かすために、必要な設定変更と修正が必要なアプリケーションコードについてまとめました。
あらすじ
先月、ISUCON13に同僚とチームを組んで参加して来ました。実装言語は日々の業務で利用しているNode.jsを選択したのですが、構成の変更中にTypeScriptのコンパイルとES Modulesに関連した問題が発生し、苦戦しつつも解決できたので、備忘録的に共有します。
なお、この記事ではISUCON自体の解説はしませんので、ISUCONが何かを知りたい方はISUCON公式ホームページを参照してください。
ISUCON13 Node.js実装の解説
ISUCON13のNode.js実装はTypeScriptを事前にコンパイル1しない!
ISUCON13のNode.jsは、tscによるコンパイルを行わず、tsxによる実行時の変換でサーバーが実行されていました。
(参考)サーバー上でNode.jsのプロセスをps auxfオプションで表示したもの
isucon 1331 0.1 1.5 1095596 57520 ? Ssl 01:20 0:00 npm run start
isucon 1346 0.0 0.0 2888 1664 ? S 01:20 0:00 \_ sh -c tsx watch src/main.ts
isucon 1347 0.1 1.3 11476996 52428 ? Sl 01:20 0:00 \_ node /home/isucon/webapp/node/node_modules/.bin/tsx watch src/main.ts
isucon 1358 0.3 1.9 65146028 72836 ? Sl 01:20 0:01 \_ /home/isucon/local/node/bin/node --require /home/isucon/webapp/node/node_modules/tsx/dist/preflight.cjs --import file:///home/isucon/webapp/node/node_modules/tsx/dist/loader.mjs src/main.ts
isucon 1378 0.0 0.3 721044 12168 ? Sl 01:20 0:00 \_ /home/isucon/webapp/node/node_modules/esbuild/lib/downloaded-@esbuild-linux-x64-esbuild --service=0.18.20 --ping
JavaScriptにコンパイルするぞ!
一般的には事前にJavaScriptにコンパイルした方が、サーバーのパフォーマンスが良くなるので、初手でTypeScriptのコードをJavaScriptにコンパイルする修正を入れることにしました。2
以下、実行環境は下記のバージョンを前提とします。
Node.js 20.9.0
TypeScript 5.3.2
遭遇したエラーと解決方法
エラーその1:error TS2551: Property 'toReversed' does not exist on type '{ username: string; score: number; }[]'. Did you mean 'reverse'?
TypeScriptでコンパイルするためにおもむろにtsconfig.jsonを開きます。
{
"extends": "@tsconfig/recommended/tsconfig.json",
"compilerOptions": {
"types": ["bun-types"]
}
}
記述はこれだけですが、"extends": "@tsconfig/recommended/tsconfig.json”
は以下の設定を取り込みます。
extendsを展開した上で、"types": ["bun-types"]
はbunを使わないため削除、出力先の指定に"outDir": "dist/"
を追加してtscを実行します。
{
"compilerOptions": {
"target": "es2015",
+ "outDir": "dist/",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
- "types": ["bun-types"]
}
}
$ npx tsc
src/handlers/stats-handler.ts:83:31 - error TS2551: Property 'toReversed' does not exist on type '{ username: string; score: number; }[]'. Did you mean 'reverse'?
83 for (const r of ranking.toReversed()) {
~~~~~~~~~~
node_modules/typescript/lib/lib.es5.d.ts:1365:5
1365 reverse(): T[];
~~~~~~~~~~~~~~~
'reverse' is declared here.
src/handlers/stats-handler.ts:244:31 - error TS2551: Property 'toReversed' does not exist on type '{ livestreamId: number; title: string; score: number; }[]'. Did you mean 'reverse'?
244 for (const r of ranking.toReversed()) {
~~~~~~~~~~
node_modules/typescript/lib/lib.es5.d.ts:1365:5
1365 reverse(): T[];
~~~~~~~~~~~~~~~
'reverse' is declared here.
Found 2 errors in the same file, starting at: src/handlers/stats-handler.ts:83
error TS2551: Property 'toReversed' does not exist on type '{ username: string; score: number; }[]'. Did you mean 'reverse'?
Property 'toReversed' does not exist on type
のエラーが発生しました。
toReserved
はECMAScript Proposalで2023年1月にStage 4に到達した、配列要素を非破壊的に変更するメソッド群のひとつです。
コンパイラのターゲットで指定したECMA Scriptのバージョンにまだ追加されていない新しい機能を利用したい場合、tsconfig.jsonのlib
に追加の設定を書くことができます。
{
"compilerOptions": {
"target": "es2015",
+ "lib": ["ES2023.Array"],
"outDir": "dist/",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
これでJavaScriptにコンパイルできました。
# エラーにならない場合はコンソールに何も出力されない
$ npx tsc
エラーその2:Error [ERR_REQUIRE_ESM]
無事にコンパイルできたので、サーバーを実行します。
# 実際のサーバー起動コマンドはsystemdで管理されていますが説明のため簡略化します
$ node dist/main.js
/home/isucon/webapp/node/node_modules/hono-sessions/script/deps.js:27
var async_1 = require("nanoid/async");
^
Error [ERR_REQUIRE_ESM]: require() of ES Module /home/isucon/webapp/node/node_modules/nanoid/async/index.js from /home/isucon/webapp/node/node_modules/hono-sessions/script/deps.js not supported.
Instead change the require of index.js in /home/isucon/webapp/node/node_modules/hono-sessions/script/deps.js to a dynamic import() which is available in all CommonJS modules.
at Object.<anonymous> (/home/isucon/webapp/node/node_modules/hono-sessions/script/deps.js:27:15) {
code: 'ERR_REQUIRE_ESM'
}
Node.js v20.9.0
Error [ERR_REQUIRE_ESM]
これは依存しているnpmパッケージの内、hono-sessionsはNative ES Modulesのみに対応しているため、デフォルトでNode.jsでは実行できません。
Node.jsのNative ES Modules対応の問題や背景はすでに様々な記事で解説がなされているので、記事末にまとめるにとどめ、ここでは深入りしません。
少なくともこの状態ではサーバーが実行できないため、サーバーもNative ES Modulesで実行されるように修正していきます。
まずは、package.jsonに”type”: “modules”
を追加します。
これによって、拡張子がmts、mjsでなくてもES Modulesとして解釈されるようになります。
{
"name": "isupipe",
// 中略
+ "type": "module"
}
次に、tsconfig.jsonのmodule
をNode16
に変更します。
package.jsonが"type": "module"
の場合、ES Modulesとして解釈します。
{
"compilerOptions": {
"target": "es2015",
"lib": ["ES2023.Array"],
"outDir": "dist/",
+ "module": "Node16",
- "module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
この状態でコンパイルするとimportの構文エラーが発生します。
ES Modulesはimport宣言で拡張子をつける必要があります。
$ npx tsc
src/handlers/livecomment-handler.ts:3:33 - error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean '../types/application.js'?
3 import { HonoEnvironment } from '../types/application'
# 以下、他にもたくさん
修正例
import { Context } from 'hono'
import { RowDataPacket, ResultSetHeader } from 'mysql2/promise'
+ import { HonoEnvironment } from '../types/application.js'
- import { HonoEnvironment } from '../types/application'
// 後略
また、ES Modulesでは__dirname
, __filename
がそのままでは利用できないため次の行を追加します。
+ import { fileURLToPath } from "url";
+ import path from "path";
+ const __filename = fileURLToPath(import.meta.url);
+ const __dirname = path.dirname(__filename);
これでNative ES Modulesでサーバーを実行できるようになりました!
まとめ
ISUCON13で実際に遭遇した問題を題材に、TypeScriptでECMA Scriptの新しい機能を追加する方法とNode.jsをNative ES Modulesで動かす方法を解説しました。
日常の業務では、何となくでしか理解していなかった、tsconfig.jsonについて振り返る良い機会になったと同時に、競技中は素早く対応できなかったため実力不足を思い知らされました。3
20日は @takuuuuuuu777 が担当します。お楽しみに!
Appendix:Node.jsのモジュールシステムの複雑さを解説する偉大な先人の記事