Deno 2.0移行ガイド - Node.jsプロジェクトをDenoに移行する実践手順
Deno 2.0への移行を検討すべき理由
Deno 2.0は、Node.jsとの互換性を大幅に向上させ、既存のNode.jsエコシステムをそのまま活用できるようになりました。以下のような利点があります:
Deno 2.0の主な利点
- npm完全サポート: package.jsonとnode_modulesがそのまま使える
- TypeScript標準: 設定不要でTypeScriptが動く
- セキュリティ: デフォルトで権限制御
- 標準ライブラリ: 高品質な標準ライブラリ
- Webプラットフォーム互換: fetch、WebSocket、Web Crypto API
- パフォーマンス: V8エンジンの最適化
- シングルバイナリ: 依存関係なしで動作
移行の前提条件
Denoのインストール
# macOS / Linux
curl -fsSL https://deno.land/install.sh | sh
# Homebrew (macOS)
brew install deno
# Windows (PowerShell)
irm https://deno.land/install.ps1 | iex
# バージョン確認
deno --version
Node.jsプロジェクトの評価
移行前に、プロジェクトの状態を確認します:
# プロジェクトの依存関係を確認
npm list --depth=0
# TypeScript設定を確認
cat tsconfig.json
# ビルド・テストスクリプトを確認
cat package.json | jq '.scripts'
段階的移行戦略
フェーズ1: Deno互換性チェック
# プロジェクトディレクトリで実行
deno check --node-modules-dir src/**/*.ts
# npmパッケージの互換性チェック
deno info npm:express
deno info npm:react
フェーズ2: package.jsonの準備
Deno 2.0では、既存のpackage.jsonをそのまま使用できます。
// package.json
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "deno run --allow-all --node-modules-dir src/server.ts",
"build": "deno run --allow-all build.ts",
"test": "deno test --allow-all"
},
"dependencies": {
"express": "^4.18.2",
"dotenv": "^16.0.3"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/node": "^20.0.0"
}
}
フェーズ3: deno.jsonの作成
Deno固有の設定を追加します。
// deno.json
{
"compilerOptions": {
"allowJs": true,
"lib": ["deno.window"],
"strict": true
},
"nodeModulesDir": true,
"imports": {
"@/": "./src/",
"@shared/": "./shared/"
},
"tasks": {
"dev": "deno run --allow-all --watch src/server.ts",
"build": "deno run --allow-all build.ts",
"test": "deno test --allow-all"
},
"lint": {
"include": ["src/"],
"rules": {
"tags": ["recommended"]
}
},
"fmt": {
"useTabs": false,
"lineWidth": 100,
"indentWidth": 2,
"semiColons": true,
"singleQuote": true,
"proseWrap": "preserve"
}
}
Expressアプリの移行例
Node.js版 (移行前)
// src/server.ts (Node.js)
import express from 'express';
import dotenv from 'dotenv';
import { router } from './routes/index.js';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.use('/api', router);
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Deno 2.0版 (移行後)
// src/server.ts (Deno)
import express from 'npm:express@^4.18.2';
import { load } from 'https://deno.land/std@0.224.0/dotenv/mod.ts';
import { router } from './routes/index.ts';
// 環境変数読み込み
const env = await load();
const PORT = Number(env.PORT || 3000);
const app = express();
app.use(express.json());
app.use('/api', router);
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
実行:
# 開発モード(ホットリロード付き)
deno run --allow-net --allow-read --allow-env --watch src/server.ts
# または deno.jsonのタスク使用
deno task dev
npm依存関係の扱い
方法1: npm: スキーマ(推奨)
// 直接npmから読み込む
import express from 'npm:express@^4.18.2';
import lodash from 'npm:lodash@^4.17.21';
// 型定義も自動解決
const result = lodash.chunk([1, 2, 3, 4], 2);
方法2: package.jsonを使用
# node_modulesディレクトリを生成
deno install
# または実行時に生成
deno run --node-modules-dir --allow-all src/server.ts
// Node.js互換の読み込み
import express from 'express';
import lodash from 'lodash';
方法3: Import Mapsを使用
// deno.json
{
"imports": {
"express": "npm:express@^4.18.2",
"lodash": "npm:lodash@^4.17.21",
"@/": "./src/"
}
}
// すっきりした読み込み
import express from 'express';
import lodash from 'lodash';
import { config } from '@/config.ts';
データベース接続の移行
PostgreSQL (Node.js → Deno)
Node.js版:
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
export async function query(text: string, params?: any[]) {
const result = await pool.query(text, params);
return result.rows;
}
Deno版 (PostgreSQL native):
import { Client } from 'https://deno.land/x/postgres@v0.17.0/mod.ts';
const client = new Client({
user: Deno.env.get('DB_USER'),
database: Deno.env.get('DB_NAME'),
hostname: Deno.env.get('DB_HOST'),
password: Deno.env.get('DB_PASSWORD'),
port: Number(Deno.env.get('DB_PORT')),
});
await client.connect();
export async function query(text: string, params?: any[]) {
const result = await client.queryObject(text, params);
return result.rows;
}
Deno版 (npm:pg使用):
import pg from 'npm:pg@^8.11.0';
const pool = new pg.Pool({
connectionString: Deno.env.get('DATABASE_URL'),
});
export async function query(text: string, params?: any[]) {
const result = await pool.query(text, params);
return result.rows;
}
ファイルシステム操作の移行
Node.js版
import fs from 'fs/promises';
import path from 'path';
export async function readConfig() {
const configPath = path.join(__dirname, 'config.json');
const data = await fs.readFile(configPath, 'utf-8');
return JSON.parse(data);
}
export async function writeLog(message: string) {
const logPath = path.join(__dirname, 'logs', 'app.log');
await fs.appendFile(logPath, `${new Date().toISOString()}: ${message}\n`);
}
Deno版
import { join } from 'https://deno.land/std@0.224.0/path/mod.ts';
export async function readConfig() {
const configPath = join(Deno.cwd(), 'config.json');
const data = await Deno.readTextFile(configPath);
return JSON.parse(data);
}
export async function writeLog(message: string) {
const logPath = join(Deno.cwd(), 'logs', 'app.log');
const logMessage = `${new Date().toISOString()}: ${message}\n`;
await Deno.writeTextFile(logPath, logMessage, { append: true });
}
環境変数の扱い
Node.js版
import dotenv from 'dotenv';
dotenv.config();
const config = {
port: process.env.PORT || 3000,
dbUrl: process.env.DATABASE_URL,
apiKey: process.env.API_KEY,
};
Deno版
import { load } from 'https://deno.land/std@0.224.0/dotenv/mod.ts';
const env = await load();
const config = {
port: Number(env.PORT || 3000),
dbUrl: env.DATABASE_URL,
apiKey: env.API_KEY,
};
// または直接Deno.envを使用
const port = Number(Deno.env.get('PORT') || 3000);
テストの移行
Node.js (Jest)
// sum.test.ts
import { sum } from './sum';
describe('sum', () => {
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
});
Deno Test
// sum_test.ts
import { assertEquals } from 'https://deno.land/std@0.224.0/assert/mod.ts';
import { sum } from './sum.ts';
Deno.test('sum adds 1 + 2 to equal 3', () => {
assertEquals(sum(1, 2), 3);
});
Deno.test({
name: 'sum with negative numbers',
fn() {
assertEquals(sum(-1, -2), -3);
},
});
// 非同期テスト
Deno.test('async operation', async () => {
const result = await fetchData();
assertEquals(result.status, 'success');
});
実行:
# すべてのテストを実行
deno test
# 特定のファイルのみ
deno test src/utils/sum_test.ts
# カバレッジ取得
deno test --coverage=coverage
deno coverage coverage
TypeScript設定の移行
tsconfig.json (Node.js)
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
deno.json (Deno)
Denoではほとんどの設定がデフォルトで適切です。
{
"compilerOptions": {
"allowJs": true,
"lib": ["deno.window"],
"strict": true
}
}
ビルド・バンドル
Node.js (webpack/esbuild)
npm run build
Deno Bundle
# 単一ファイルにバンドル
deno bundle src/server.ts dist/server.js
# または deno compile で実行可能バイナリを生成
deno compile --allow-net --allow-read --allow-env --output ./bin/server src/server.ts
# クロスコンパイル
deno compile --target x86_64-unknown-linux-gnu --output ./bin/server-linux src/server.ts
deno compile --target x86_64-pc-windows-msvc --output ./bin/server.exe src/server.ts
deno compile --target aarch64-apple-darwin --output ./bin/server-mac-arm src/server.ts
Docker化
Dockerfile (Deno)
# Dockerfile
FROM denoland/deno:1.42.0
WORKDIR /app
# 依存関係のキャッシュ
COPY deno.json deno.lock ./
RUN deno cache src/server.ts
# ソースコードをコピー
COPY . .
# アプリケーションを実行
EXPOSE 3000
CMD ["deno", "run", "--allow-net", "--allow-read", "--allow-env", "src/server.ts"]
docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- '3000:3000'
environment:
- PORT=3000
- DATABASE_URL=postgres://user:pass@db:5432/mydb
volumes:
- .:/app
command: deno run --allow-all --watch src/server.ts
db:
image: postgres:15
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
よくある移行の問題と解決策
1. __dirname / __filenameが使えない
問題:
// Node.jsではOK、Denoではエラー
const configPath = path.join(__dirname, 'config.json');
解決策:
import { dirname, fromFileUrl, join } from 'https://deno.land/std@0.224.0/path/mod.ts';
const __dirname = dirname(fromFileUrl(import.meta.url));
const configPath = join(__dirname, 'config.json');
2. CommonJSモジュールの読み込み
問題:
// require()は使えない
const express = require('express');
解決策:
// ESMを使用
import express from 'npm:express@^4.18.2';
// またはnpm:プレフィックス
import express from 'npm:express';
3. グローバルなprocessオブジェクト
問題:
// Node.jsグローバルは使えない
console.log(process.env.PORT);
解決策:
// Deno.envを使用
console.log(Deno.env.get('PORT'));
// またはNode互換モードで実行
deno run --node-modules-dir --compat script.ts
パフォーマンス比較
簡単なベンチマーク:
// benchmark.ts
import { performance } from 'node:perf_hooks';
const iterations = 1000000;
console.time('Array operations');
const arr = Array.from({ length: iterations }, (_, i) => i);
const doubled = arr.map(x => x * 2).filter(x => x % 2 === 0);
console.timeEnd('Array operations');
console.time('File I/O');
for (let i = 0; i < 100; i++) {
await Deno.writeTextFile(`/tmp/test-${i}.txt`, 'Hello, Deno!');
await Deno.readTextFile(`/tmp/test-${i}.txt`);
}
console.timeEnd('File I/O');
# Node.jsで実行
node benchmark.ts
# Denoで実行
deno run --allow-read --allow-write benchmark.ts
まとめ
Deno 2.0への移行は、以下の順序で進めるとスムーズです:
- 互換性チェック: npm依存関係とコードの確認
- 段階的移行: まずは小さなスクリプトから
- テスト: Deno Testへの移行
- 本番環境: Dockerまたはバイナリでデプロイ
Deno 2.0の利点:
- ✅ TypeScript標準
- ✅ セキュリティファースト
- ✅ npm完全互換
- ✅ 標準ライブラリ充実
- ✅ シングルバイナリ配布
Node.jsからの移行は思ったより簡単です。ぜひDeno 2.0を試してみてください!