brand logo

ドキュメント

ESモジュール

NuxtはネイティブのESモジュールを使用します。

このガイドは、ESモジュールが何であるか、そしてNuxtアプリ(または上流のライブラリ)をESMに対応させる方法を説明します。

背景

CommonJSモジュール

CommonJS (CJS) は、Node.jsによって導入された形式で、分離されたJavaScriptモジュール間で機能を共有することを可能にします(詳細はこちら)。 この構文にすでに馴染みがあるかもしれません:

const a = require('./a')

module.exports.a = a

webpackやRollupのようなバンドラーはこの構文をサポートしており、CommonJSで書かれたモジュールをブラウザで使用することができます。

ESM構文

多くの場合、ESMとCJSの話をするとき、人々はモジュールを書くための異なる構文について話しています。

import a from './a'

export { a }

ECMAScriptモジュール(ESM)が標準になる前(それには10年以上かかりました!)、webpackのようなツールやTypeScriptのような言語でさえ、いわゆるESM構文をサポートし始めました。 しかし、実際の仕様とはいくつかの重要な違いがあります。こちらに役立つ説明があります。

「ネイティブ」ESMとは何か?

あなたは長い間、ESM構文を使ってアプリを書いてきたかもしれません。結局のところ、ブラウザによってネイティブにサポートされており、Nuxt 2では、あなたが書いたすべてのコードを適切な形式(サーバー用にはCJS、ブラウザ用にはESM)にコンパイルしていました。

パッケージにモジュールを追加するとき、状況は少し異なりました。サンプルライブラリはCJSとESMの両方のバージョンを公開し、どちらを使用するか選ばせてくれました:

{
  "name": "sample-library",
  "main": "dist/sample-library.cjs.js",
  "module": "dist/sample-library.esm.js"
}

したがって、Nuxt 2では、バンドラー(webpack)がサーバービルド用にCJSファイル('main')を取り込み、クライアントビルド用にESMファイル('module')を使用していました。

しかし、最近のNode.js LTSリリースでは、Node.js内でネイティブESMモジュールを使用することが可能になりました。つまり、Node.js自体がESM構文を使用してJavaScriptを処理できるようになったのです。ただし、デフォルトではそうしません。ESM構文を有効にする最も一般的な方法は次の2つです:

  • package.json内で"type": "module"を設定し、.js拡張子を使用し続ける
  • .mjsファイル拡張子を使用する(推奨)

これはNuxt Nitroで行っていることで、.output/server/index.mjsファイルを出力しています。これにより、Node.jsはこのファイルをネイティブESモジュールとして扱います。

Node.jsコンテキストでの有効なインポートとは?

モジュールをrequireするのではなくimportすると、Node.jsはそれを異なる方法で解決します。たとえば、sample-libraryをインポートすると、Node.jsはそのライブラリのpackage.json内のmainではなく、exportsまたはmoduleエントリを探します。

これは、const b = await import('sample-library')のような動的インポートにも当てはまります。

Nodeは次の種類のインポートをサポートしています(ドキュメントを参照):

  1. .mjsで終わるファイル - これらはESM構文を使用することが期待されます
  2. .cjsで終わるファイル - これらはCJS構文を使用することが期待されます
  3. .jsで終わるファイル - これらはpackage.json"type": "module"がない限りCJS構文を使用することが期待されます

どのような問題が発生する可能性がありますか?

長い間、モジュールの作成者はESM構文のビルドを生成していましたが、.esm.js.es.jsのような慣習を使用しており、それをpackage.jsonmoduleフィールドに追加していました。これは、webpackのようなバンドラーによってのみ使用されていたため、これまで問題にはなりませんでした。

しかし、Node.jsのESMコンテキストで.esm.jsファイルを持つパッケージをインポートしようとすると、次のようなエラーが発生します:

Terminal
(node:22145) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
/path/to/index.js:1

export default {}
^^^^^^

SyntaxError: Unexpected token 'export'
    at wrapSafe (internal/modules/cjs/loader.js:1001:16)
    at Module._compile (internal/modules/cjs/loader.js:1049:27)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
    ....
    at async Object.loadESM (internal/process/esm_loader.js:68:5)

また、Node.jsがCJSと考えるESM構文ビルドから名前付きインポートを行った場合にも、このエラーが発生する可能性があります:

Terminal
file:///path/to/index.mjs:5
import { named } from 'sample-library'
         ^^^^^
SyntaxError: Named export 'named' not found. The requested module 'sample-library' is a CommonJS module, which may not support all module.exports as named exports.

CommonJS modules can always be imported via the default export, for example using:

import pkg from 'sample-library';
const { named } = pkg;

    at ModuleJob._instantiate (internal/modules/esm/module_job.js:120:21)
    at async ModuleJob.run (internal/modules/esm/module_job.js:165:5)
    at async Loader.import (internal/modules/esm/loader.js:177:24)
    at async Object.loadESM (internal/process/esm_loader.js:68:5)

ESMの問題のトラブルシューティング

これらのエラーに遭遇した場合、問題はほぼ確実に上流のライブラリにあります。彼らはNodeによってインポートされることをサポートするためにライブラリを修正する必要があります。

ライブラリのトランスパイル

その間、これらのライブラリをインポートしないようにNuxtに指示することができます。build.transpileに追加してください:

export default defineNuxtConfig({
  build: {
    transpile: ['sample-library']
  }
})

これらのライブラリによってインポートされている他のパッケージも追加する必要があるかもしれません。

ライブラリのエイリアス

場合によっては、ライブラリをCJSバージョンに手動でエイリアスする必要があるかもしれません。例えば:

export default defineNuxtConfig({
  alias: {
    'sample-library': 'sample-library/dist/sample-library.cjs.js'
  }
})

デフォルトエクスポート

CommonJS形式の依存関係は、module.exportsまたはexportsを使用してデフォルトエクスポートを提供できます:

node_modules/cjs-pkg/index.js
module.exports = { test: 123 }
// または
exports.test = 123

このような依存関係をrequireすると通常はうまく動作します:

test.cjs
const pkg = require('cjs-pkg')

console.log(pkg) // { test: 123 }

ネイティブESMモードのNode.jsesModuleInteropを有効にしたTypeScript、およびwebpackのようなバンドラーは、このようなライブラリをデフォルトインポートできる互換性メカニズムを提供します。 このメカニズムはしばしば「interop require default」と呼ばれます:

import pkg from 'cjs-pkg'

console.log(pkg) // { test: 123 }

しかし、構文検出の複雑さや異なるバンドル形式のため、interop defaultが失敗し、次のような結果になる可能性があります:

import pkg from 'cjs-pkg'

console.log(pkg) // { default: { test: 123 } }

また、動的インポート構文を使用する場合(CJSおよびESMファイルの両方で)、常にこの状況が発生します:

import('cjs-pkg').then(console.log) // [Module: null prototype] { default: { test: '123' } }

この場合、デフォルトエクスポートを手動でinteropする必要があります:

// 静的インポート
import { default as pkg } from 'cjs-pkg'

// 動的インポート
import('cjs-pkg').then(m => m.default || m).then(console.log)

より複雑な状況を処理し、より安全にするために、Nuxtではmllyを内部的に使用しており、名前付きエクスポートを保持できます。

import { interopDefault } from 'mlly'

// 形状が { default: { foo: 'bar' }, baz: 'qux' } であると仮定
import myModule from 'my-module'

console.log(interopDefault(myModule)) // { foo: 'bar', baz: 'qux' }

ライブラリ作成者ガイド

良いニュースは、ESM互換性の問題を修正するのは比較的簡単だということです。主に2つのオプションがあります:

  1. ESMファイルの名前を.mjsで終わるように変更することができます。

    これは推奨される最も簡単なアプローチです。 ライブラリの依存関係やビルドシステムに関する問題を解決する必要があるかもしれませんが、ほとんどの場合、これで問題は解決します。また、CJSファイルの名前を.cjsで終わるように変更することも推奨されます。これにより、より明示的になります。

  2. ライブラリ全体をESM専用にすることを選択できます。

    これは、package.json"type": "module"を設定し、ビルドされたライブラリがESM構文を使用することを確認することを意味します。ただし、依存関係に問題が発生する可能性があり、このアプローチではライブラリがESMコンテキストでのみ消費されることを意味します。

移行

CJSからESMへの最初のステップは、requireの使用をimportに更新することです:

module.exports = ...

exports.hello = ...
const myLib = require('my-lib')

ESMモジュールでは、CJSとは異なり、requirerequire.resolve__filename__dirnameのグローバルは使用できず、import()およびimport.meta.filenameに置き換える必要があります。

import { join } from 'path'

const newDir = join(__dirname, 'new-dir')
const someFile = require.resolve('./lib/foo.js')

ベストプラクティス

  • デフォルトエクスポートよりも名前付きエクスポートを優先してください。これにより、CJSの競合が減少します。(デフォルトエクスポートセクションを参照)

  • Nitroポリフィルを必要とせずにブラウザやエッジワーカーでライブラリを使用できるようにするため、Node.jsのビルトインやCommonJSまたはNode.js専用の依存関係にできるだけ依存しないようにしてください。

  • 条件付きエクスポートを使用して新しいexportsフィールドを使用してください。(詳細はこちら

{
  "exports": {
    ".": {
      "import": "./dist/mymodule.mjs"
    }
  }
}

tips

このセクションは公式ドキュメントの翻訳ではなく、本サイト独自の補足記事です。

はじめに:ESモジュール対応で得られるメリットと解決できる課題

Nuxtは最新のJavaScript標準であるESモジュール(ESM)をネイティブに活用することで、モジュールの読み込みや依存関係の解決をより効率的かつ明確に行えるようになりました。これにより、開発者は以下のような課題を解決できます。

  • サーバーサイドとクライアントサイドでのモジュール管理の一貫性向上
  • ビルドやバンドル時のパフォーマンス改善
  • 依存ライブラリのモジュール形式の違いによるトラブルの軽減

しかし、ESM対応はNode.jsの仕様や既存のCommonJS(CJS)モジュールとの互換性問題など、理解しておくべきポイントも多くあります。本記事ではNuxtにおけるESMの基本から、実務での活用例、注意点までを詳しく解説します。


まず結論:NuxtでのESM対応の要点まとめ

  • NuxtはNode.jsのネイティブESMを活用し、サーバー・クライアント双方でモジュールを効率的に扱う
  • ESMはimport/export構文を使い、CommonJSのrequire/module.exportsとは異なる
  • Node.jsでESMを使うにはpackage.json"type": "module"設定か.mjs拡張子が必要
  • 依存ライブラリがCJS形式の場合、build.transpileやエイリアス設定で対応可能
  • ESMとCJSの混在による名前付きエクスポートの問題に注意し、必要に応じてinterop処理を行う

いつ使うべきか、使わない方がよいケース

使うべきケース

  • 最新のNode.js環境でネイティブESMを活用し、ビルドや実行のパフォーマンスを最大化したい場合
  • 依存ライブラリがESM対応済みで、モジュールのツリーシェイキングや静的解析を活かしたい場合
  • Nuxt Nitroなどのサーバーレス環境で軽量かつ高速なモジュール解決を実現したい場合

使わない方がよいケース

  • 依存ライブラリがCJSのみでESM対応が不十分な場合(特に名前付きエクスポートの互換性問題が頻発する場合)
  • Node.jsのバージョンが古く、ネイティブESMを十分にサポートしていない環境
  • プロジェクトのビルド設定やCI/CD環境がESM対応に未対応でトラブルが多発する場合

実務でよくあるユースケースとサンプルコード

1. 依存ライブラリのESM対応状況に応じた設定

ESM対応済みのライブラリはそのままimportで利用可能ですが、CJS形式のライブラリはbuild.transpileでトランスパイル対象に指定することが多いです。

export default defineNuxtConfig({
  build: {
    transpile: ['legacy-cjs-lib']
  }
})

これにより、Nuxtのビルド時にCJSライブラリをESM互換に変換し、サーバー・クライアント双方で問題なく動作させられます。

2. ライブラリのエイリアスでCJS版を明示的に指定

特定のライブラリがESM版で問題を起こす場合、CJS版を直接指定して回避することもあります。

export default defineNuxtConfig({
  alias: {
    'problematic-lib': 'problematic-lib/dist/problematic-lib.cjs.js'
  }
})

3. 動的インポート時のデフォルトエクスポートの取り扱い

CJSモジュールを動的インポートすると、名前付きエクスポートがうまく解決できず、defaultプロパティにラップされることがあります。これを安全に扱う例:

const mod = await import('cjs-lib')
const actualModule = mod.default || mod
console.log(actualModule)

よくある落とし穴・注意点

SSRとCSRでのモジュール解決の違い

Nuxtはサーバーサイドレンダリング(SSR)とクライアントサイドレンダリング(CSR)で異なるモジュール解決を行います。サーバーはNode.jsのESM対応に依存し、クライアントはブラウザのESMを利用します。依存ライブラリが両環境で異なる形式を持つ場合、ビルド設定での調整が必要です。

Hydration時の不整合

ESMとCJSの混在により、サーバーで生成されたHTMLとクライアントのJavaScriptの状態がずれることがあります。特に名前付きエクスポートの扱いに注意し、interop処理を適切に行いましょう。

パフォーマンスへの影響

ESMは静的解析が可能なため、ツリーシェイキングや遅延読み込みが効果的に働きますが、CJSモジュールを多用するとこれらの恩恵が減少します。可能な限りESM対応済みのライブラリを選び、build.transpileで適切に処理しましょう。

Node.jsのバージョン依存

ネイティブESMはNode.js 12以降でサポートされていますが、安定的に使うには14以上が推奨されます。古いバージョンでは動作しないか、設定が複雑になるため注意が必要です。


まとめ

NuxtにおけるESモジュール対応は、最新のJavaScript標準を活かしつつ、サーバーとクライアント双方で効率的なモジュール管理を実現します。実務では依存ライブラリの形式に応じたビルド設定やエイリアスの活用、動的インポート時のinterop処理が重要です。Node.jsのバージョンや環境に注意しながら、ESMのメリットを最大限に活かした開発を目指しましょう。


ESMとCJSの違いは一見複雑ですが、Nuxtのbuild.transpilealias設定を活用することで多くの問題は解決可能です。依存ライブラリのアップデート情報も定期的にチェックしましょう。

Node.jsのネイティブESM対応は進化中のため、NuxtのバージョンアップやNode.jsのアップデートに伴い挙動が変わることがあります。公式ドキュメントやリリースノートをこまめに確認することをおすすめします。


title: 'NuxtでのESM対応ライブラリ作成のポイントと実務的注意点' description: 'NuxtプロジェクトでESM対応ライブラリを作成・移行する際の基本的な考え方と実務での活用例、よくある落とし穴を丁寧に解説します。ESMとCJSの違いや移行手順、パフォーマンス面の注意点も含めて解説。'

NuxtでのESM対応ライブラリ作成のポイントと実務的注意点

Nuxtを使った開発において、外部ライブラリや自作モジュールをESM(ECMAScript Modules)対応にすることは、最新のJavaScriptエコシステムに適応し、パフォーマンスや互換性を高める上で非常に重要です。特にNuxt 3やNitro環境ではESMが標準となっているため、CommonJS(CJS)形式のままだと動作しない、またはビルド時にエラーが発生することがあります。

本記事では、Nuxt公式ドキュメントの内容を補足し、ESM対応ライブラリの作成・移行に関する実務的なポイントや注意点を詳しく解説します。これにより、Nuxtプロジェクトでのライブラリ利用や開発がスムーズになり、将来的なメンテナンス性も向上します。


まず結論:ESM対応ライブラリ作成の要点

  • ファイル拡張子を.mjs(ESM)と.cjs(CJS)に分けるのが最も簡単で推奨される方法
    → NuxtやNode.jsがモジュール形式を明確に認識しやすくなる。

  • package.json"type": "module"を設定してライブラリ全体をESM化も可能だが依存関係に注意
    → ESM専用になるため、CJS依存のライブラリとの互換性問題が起こりやすい。

  • requireからimportへの書き換えが必須
    → ESMではrequireが使えないため、動的インポートや名前付きインポートに置き換える。

  • Node.jsのグローバル変数(__dirname__filename)は使えず、import.meta.urlなどに置き換える必要がある

  • 名前付きエクスポートを優先し、デフォルトエクスポートは控えめにするのがベストプラクティス


いつ使うべきか・使わない方がよいケース

使うべきケース

  • Nuxt 3やNitroで動作する最新のライブラリを作成・公開したい場合
  • ブラウザやエッジ環境での利用を想定し、Node.js固有の依存を減らしたい場合
  • 将来的にメンテナンスしやすく、モダンなJavaScript標準に準拠したい場合

使わない方がよいケース

  • 既存のCJS依存ライブラリが多く、移行コストが高すぎる場合
  • Node.jsの特定バージョンや環境に強く依存したコードを使い続ける必要がある場合
  • すぐに動作保証が必要で、ESM対応のテストや調整に時間を割けない場合

実務でよくあるユースケースとサンプルコード

1. ESM対応ライブラリの基本的な書き換え

// Before (CommonJS)
const utils = require('./utils')
module.exports = {
  greet: () => console.log('Hello')
}
// After (ESM)
import * as utils from './utils.js'
export function greet() {
  console.log('Hello')
}

2. 動的インポートを使った依存関係の遅延読み込み

// ESMでは動的にモジュールを読み込む場合
export async function loadFeature() {
  const feature = await import('./feature.js')
  feature.init()
}

3. Node.jsの__dirnameの代替

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

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

const dataPath = join(__dirname, 'data.json')

よくある落とし穴・注意点

SSRとCSRの違いによる影響

Nuxtはサーバーサイドレンダリング(SSR)とクライアントサイドレンダリング(CSR)を組み合わせて動作します。ESM対応ライブラリがNode.js固有のAPIに依存していると、クライアント側でエラーになることがあります。ブラウザで動作しないNode.js専用コードは分離するか、条件付きで読み込む工夫が必要です。

Hydrationエラーの原因に

ESM対応の不備やモジュールの読み込み順序の違いが原因で、Hydration(サーバーでレンダリングしたHTMLにクライアント側でイベントを紐付ける処理)時に不整合が起きることがあります。特に動的インポートのタイミングや副作用のあるモジュールは注意が必要です。

パフォーマンス面の注意

ESMは静的解析が可能なため、ツリーシェイキング(未使用コードの除去)が効率的に行えます。しかし、動的インポートを多用しすぎると初期ロードが遅くなることもあります。必要なモジュールはできるだけ静的にインポートし、動的インポートは遅延読み込みが効果的なケースに限定しましょう。

依存関係の互換性問題

"type": "module"を設定したライブラリは、CJS依存のパッケージと相性が悪い場合があります。特に古いライブラリやNode.jsのビルトインモジュールの使い方に注意が必要です。可能な限り依存関係を最新化し、条件付きエクスポートを活用して環境に応じたモジュールを提供しましょう。


まとめ

NuxtでESM対応ライブラリを作成・移行することは、将来的な互換性とパフォーマンス向上に不可欠です。ファイル拡張子の適切な設定やimportへの書き換え、Node.js固有APIの代替など基本的なポイントを押さえつつ、SSR/CSRの違いや依存関係の問題にも注意しましょう。実務では動的インポートの使いどころやHydrationエラーの回避が重要です。これらを理解し適切に対応することで、Nuxtプロジェクトの品質と開発効率が大きく向上します。

ESM対応の移行は一度に全てを変えるのではなく、段階的に進めるのがおすすめです。まずはファイル拡張子の変更やimportへの書き換えから始め、動作確認をしながら徐々に依存関係を整理しましょう。