本記事は、mediba Advent Calendar 2024 20日目の記事です。
本記事の内容は個人的な見解であり、会社を代表するものではありません。
はじめに
今年はNode.jsの競合であるDenoやBunも頻繁にアップデートされ、より実用的になってきた年だったように思います。
特にDenoは、先日Deno 2がリリースされ、package.jsonのサポートなどNode.jsプロジェクトとの互換性も大幅に強化されました。
Node.jsにはないTypeScriptネイティブサポートやフォーマッタやリンターの標準搭載などの強みもあり、
将来的には業務でもNode.jsからDenoへ移行する選択肢が出てくる場合もあるのではないかと考えています。
とはいえ、すべてのNode.js用のコードがそのままDenoで動作するわけではありません。
今回は個人的に、できるだけDeno環境でも動作するように、Node.js開発の時点でやっておくとよいことを模索してみたので、5点ほど書きたいと思います。
やっておくとよいこと
1. ES Modules(ESM)にする
DenoではESMを推奨しており、CommonJS(CJS)も動かなくはないですが、ひと手間必要です。
Node.jsもデフォルトではCJSですが、ESMをサポートしています。ESMを採用しましょう。
Node.jsではpackage.json に以下の記述を加えることでESMとして動作するようになります。
{
"name": "project-name",
"version": "1.0.0",
+ "type": "module",
...
}
ESMにするとCJSの場合に使えていたrequire()
や__filename
,__dirname
が使えなくなりますが、必要な場合は同様の変数を簡単に定義できます
import { createRequire } from "node:module";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
2. TypeScriptでコーディングする
DenoはTypeScriptを標準でサポートしています。
Node.js開発においてもTypeScriptでコーディングし、JSにトランスパイルするのが主流になっているかと思います。
Node.jsはまだ標準ではTypeScriptコードを実行できないので、esbuildやtsxなどのツールを入れておきます。
npm install -D @types/node esbuild tsx typescript
tsconfig.jsonでは、
ESMの機能を有効化するために“module”:“es2022”
にします。
strict
などの厳格なルールを有効化して開発をおこなっておくとDenoなどに移行してもトラブルが起きにくいでしょう。
以下は設定した tsconfig.json の一例です。
{
"compilerOptions": {
"target": "es2022",
"lib": ["es2022"],
"module": "es2022",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"noEmit": true,
"allowJs": true,
"checkJs": false,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
"skipLibCheck": true
}
}
※Denoはtsconfig.jsonを参照しない点は注意が必要です。Denoのデフォルトと異なるcompilerOptionsを設定したい場合は、deno.jsonに記述する必要があります。
3. import文の拡張子は.tsにする
ESMでは、import文のパスに拡張子を含めたファイル名まで記述する必要があります(CJSのように拡張子を省略できません)。
Node.js+TypeScriptコンパイラ(tsc)開発の場合は、初期設定ではトランスパイル後のファイル名(拡張子.js)で記述しなければなりません。
しかし、そうするとDenoでは動作しません。ソースコードのファイル名(拡張子.ts)で記述する必要があります。
- import { hoge } from "./hoge.js";
+ import { hoge } from "./hoge.ts";
import文を拡張子.tsで記述を可能にする設定
TypeScriptのcompilerOptionsでallowImportingTsExtensions
をtrue
にすることで、
Node.js開発でも拡張子.tsでimportパスを記述可能になります。
ただし、allowImportingTsExtensions
を true
にする場合、noEmit
またemitDeclarationOnly
を有効にする必要があるので、トランスパイルはesbuildなどで行うことになります。
{
"compilerOptions": {
"target": "es2022",
"lib": ["es2022"],
"module": "es2022",
"moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "noEmit": true,
...
}
}
※先日リリースされた TypeScript 5.7 で rewriteRelativeImportExtensions
というオプションが追加され、tscでもトランスパイルさせることができるようになりましたが、このオプションにもいくつかの制約があるようです。
4. Node.js組み込みパッケージのimportは node:
プレフィックスを付ける
- import { resolve } from "path";
+ import { resolve } from "node:path";
Node.jsでは、組み込みパッケージはパッケージ名のみでimportできますが、Denoではエラーになります。
Node.js組み込みパッケージをimportする際は、パッケージ名に node:
プレフィクスを付けます。
Node.jsでも引き続き問題なく動作します。
すべてのNode.js組み込みパッケージが利用できるわけではありません、Denoに実装されているNode.js組み込みAPIの互換パッケージはこちらをご参照ください。
5. Node.jsグローバルオブジェクトは明示的にimportする
+ import { Buffer } from "node:buffer";
+ import process from "node:process";
const base64 = Buffer.from("Hello,World!").toString("base64");
process.exit(0);
process
パッケージやBuffer
オブジェクトなど、Node.jsではグローバルオブジェクトとしてimport不要で使用できますが、Denoではグローバルではないため利用できません。(console
などDenoでもグローバルに存在するものもあります)
明示的にimportすることでDenoでも動作します。
おわりに
最近はNode.js本体にも、watch実行・.envファイルサポート・テストランナーが組み込まれたり、
TypeScriptコードを直接実行できるようなオプションや、シングルバイナリにビルドする方法が実験されていたり、
開発環境は日進月歩の勢いで変わっています。
これからも日々キャッチアップを続け、社内外にも共有していければと思っています。