← 記事一覧に戻る

Node.js ESM で __dirname / __filename が "ReferenceError: __dirname is not defined" になる原因と解決法【type: module 移行】

はじめに

Node.js プロジェクトで ESM(ECMAScript Modules)に移行した、あるいは新規プロジェクトで "type": "module"package.json に追加したとたん、以下のエラーが出て詰まった経験はないでしょうか。

ReferenceError: __dirname is not defined in ES module scope

または

ReferenceError: __filename is not defined in ES module scope

このエラーはいきなり全く見慣れない場所で出ることが多く、「さっきまで動いていたのになぜ?」となりがちです。この記事では原因と複数の解決策を体系的にまとめます。


なぜ起きるのか:CommonJS と ESM の根本的な違い

Node.js が従来使ってきた CommonJS(CJS) では、各ファイルはモジュールラッパー関数で包まれて実行されます。このラッパー関数が __dirname__filename などの変数を引数として注入していました。

// Node.js が内部的にやっていること(CJS の場合)
(function(exports, require, module, __filename, __dirname) {
  // あなたのコードはここに入る
});

一方、ESM ではこのラッパー関数が存在しません。ESM はブラウザとの互換性を重視した仕様であり、ファイルシステムの概念はブラウザには存在しないため、__dirname__filename は ESM のスコープでは グローバル変数として存在しない のです。

ESM になる条件

以下のいずれかに該当するファイルは ESM として扱われます。

  • package.json"type": "module" がある場合、拡張子 .js のファイル
  • 拡張子が .mjs のファイル
  • import / export 構文を使っているファイル(TypeScript でトランスパイルする場合は設定次第)

解決策1(推奨): import.meta.url + fileURLToPath で再現する

ESM では __dirname の代わりに import.meta.url が使えます。これは現在のモジュールファイルの URL(file:///path/to/file.js 形式)を返します。

import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

console.log(__dirname)  // => /path/to/your/project/src

これを各ファイルの先頭に書くことで、CJS と同じように __dirname を使えます。

URL コンストラクタを使う(パス結合)

path.join(__dirname, '..', 'data') のような相対パス結合も、URL ベースで書き直せます。

import { fileURLToPath } from 'node:url'

// CJS での書き方
// const configPath = path.join(__dirname, '..', 'config.json')

// ESM での書き方
const configPath = fileURLToPath(new URL('../config.json', import.meta.url))
console.log(configPath)  // => /path/to/your/project/config.json

new URL(相対パス, import.meta.url) を使うと、__dirname を経由せずに相対パス解決ができます。パス1つを解決するだけなら、こちらのほうがシンプルです。


解決策2: ユーティリティファイルにまとめて再利用する

複数ファイルで __dirname が必要な場合、毎回同じコードを書くのは面倒です。ユーティリティとして切り出すのがおすすめです。

// src/utils/esm-path.js
import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path'

export function getDirname(importMetaUrl) {
  return dirname(fileURLToPath(importMetaUrl))
}

export function getFilename(importMetaUrl) {
  return fileURLToPath(importMetaUrl)
}
// src/some-module.js
import { getDirname } from './utils/esm-path.js'

const __dirname = getDirname(import.meta.url)

解決策3: require も使いたい場合は createRequire

ESM では require も使えません。require を使いたい場合(require.resolve を使うライブラリを呼び出す場合など)は createRequire で復活させられます。

import { createRequire } from 'node:module'

const require = createRequire(import.meta.url)

// これで require が使える
const data = require('./data.json')

ただし、JSON のインポートについては後述の方法の方がシンプルです。


よくある追加ハマりポイント

ハマりポイント1: JSON ファイルが import できない

ESM で JSON を直接 import すると、Node.js 22 以前では以下のエラーになります。

TypeError [ERR_IMPORT_ASSERTION_TYPE_MISSING]: Module "file:///path/to/data.json"
needs an import attribute of "type: json"

解決策: インポートアサーション(Node.js 18+)または Import Attributes(Node.js 22+)を使う。

// Node.js 18〜21: assert 構文(非推奨になりつつある)
import data from './data.json' assert { type: 'json' }

// Node.js 22+: with 構文(推奨)
import data from './data.json' with { type: 'json' }

または createRequirefs.readFileSync + JSON.parse を使う方法もあります。

import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'

const jsonPath = fileURLToPath(new URL('./data.json', import.meta.url))
const data = JSON.parse(readFileSync(jsonPath, 'utf-8'))

ハマりポイント2: サードパーティライブラリが __dirname を使っていて動かない

内部で __dirname を使っているサードパーティライブラリを ESM から呼び出すと、そのライブラリ自体がエラーになることがあります。

確認方法: エラースタックトレースを見て、node_modules 内のファイルが __dirname を参照していないか確認する。

解決策:

  1. そのライブラリの ESM 対応バージョンがあればアップデートする
  2. createRequire で CJS として読み込む
  3. 自分のコードだけを ESM にして、ライブラリは CJS のまま動かす(.mjs 拡張子を使い、package.json"type": "module" を外す)

ハマりポイント3: TypeScript プロジェクトでの注意点

TypeScript で ESM を使う場合、tsconfig.json の設定が重要です。

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler",  // または "NodeNext"
    "target": "ES2022"
  }
}

"module": "CommonJS"(デフォルト)のままだと、TypeScript はトランスパイル時に importrequire に変換するため、__dirname が CJS として注入されます。この場合はエラーが出ません。

つまり TypeScript でエラーが出ていないのに Node.js 実行時にエラーが出る場合、TypeScript が CJS に変換しているが package.json"type": "module" によって Node.js が ESM として解釈しようとしている、という設定の不一致が原因であることが多いです。

// package.json の確認
{
  "type": "module"  // ← これがあると .js は ESM として扱われる
}
// tsconfig.json の確認
{
  "compilerOptions": {
    "module": "CommonJS"  // ← これだと TypeScript は CJS を出力する(矛盾が起きる)
  }
}

TypeScript + ESM の組み合わせでは、modulemoduleResolutionNodeNextESNext/bundler に統一しましょう。

// TypeScript + ESM の推奨設定
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist"
  }
}

また、NodeNext モードでは .ts ファイルを import するときに拡張子を .js で書く必要があります(トランスパイル後のファイルを指定する)。

// NG: 拡張子なし(NodeNext モードではエラー)
import { something } from './utils'

// OK: .js 拡張子を付ける(.ts ファイルを import していても .js と書く)
import { something } from './utils.js'

まとめ

問題 解決策
__dirname is not defined fileURLToPath(import.meta.url) + dirname()
__filename is not defined fileURLToPath(import.meta.url)
相対パス解決 new URL('../relative/path', import.meta.url)
require is not defined createRequire(import.meta.url)
JSON import できない with { type: 'json' } または JSON.parse(readFileSync(...))
TypeScript との不一致 module: "NodeNext" に統一

ESM への移行は最初に多くのエラーが出ますが、パターンを把握してしまえば対処は難しくありません。import.meta.url を起点にファイルシステム操作を再構築する、というのが ESM の基本的な考え方です。

段階的に移行する場合は、.mjs 拡張子を使って ESM ファイルと CJS ファイルを共存させる方法も有効です。プロジェクト全体を一度に書き換えるよりも、ファイル単位で少しずつ移行するほうがリスクが低いです。