T-CREATOR

Jest で ESM が通らない時の解決フロー:type: module/transform/resolver を総点検

Jest で ESM が通らない時の解決フロー:type: module/transform/resolver を総点検

Jest で ESM モジュールを使ったプロジェクトで「SyntaxError: Cannot use import statement outside a module」や「TypeError: Unknown file extension ".js"」といったエラーに遭遇したことはありませんか?

このようなエラーは、Jest が ES Modules(ESM)を完全にはサポートしていないことに起因しています。しかし、適切な設定を行うことで、これらの問題は確実に解決できます。

この記事では、Jest で ESM が通らない問題を体系的に解決するための完全なフローをご紹介します。package.jsontype: "module" 設定から、transform 設定、resolver の調整まで、段階的にトラブルシューティングを進めていきましょう。

ESM エラーの診断と分類

Jest で ESM を使用する際に発生するエラーは、主に以下のパターンに分類されます。まずは、どのエラーに該当するかを正確に把握しましょう。

よくあるエラーパターン

Jest ESM 関連のエラーは、その原因に応じて明確なパターンに分かれています。以下の表で主要なエラーを整理してみました。

#エラーコードエラーメッセージ例主な原因
1SyntaxErrorCannot use import statement outside a modulepackage.json の type 設定不備
2TypeErrorUnknown file extension ".js" for ESMtransform 設定の問題
3ERR_MODULE_NOT_FOUNDCannot find modulemoduleNameMapping 不備
4ReferenceErrorrequire is not definedCommonJS/ESM の混在問題

エラーメッセージから原因を特定する方法

エラーメッセージを読み解くことで、問題の所在を素早く特定できます。以下のフローチャートで診断を行ってみましょう。

mermaidflowchart TD
    error[エラー発生] --> syntax{SyntaxError?}
    syntax -->|Yes| import_error[import statement エラー]
    syntax -->|No| type_error{TypeError?}

    import_error --> check_type[package.json の type 確認]

    type_error -->|Yes| extension_error[Unknown file extension]
    type_error -->|No| module_error[Module not found]

    extension_error --> check_transform[transform 設定確認]
    module_error --> check_mapping[moduleNameMapping 確認]

    check_type --> solution_type[type: module 設定]
    check_transform --> solution_transform[transform 追加]
    check_mapping --> solution_mapping[パス設定修正]

このフローに従って原因を特定することで、効率的に問題を解決できます。

図で理解できる要点:

  • エラータイプごとに解決すべき設定項目が明確
  • 診断フローに沿って段階的にチェックが可能
  • 各エラーパターンに対応する設定箇所が一目瞭然

解決フローの全体像

Jest ESM 問題の解決は、以下の 4 つのステップで体系的に進めることが重要です。各ステップを順序立てて実行することで、確実に問題を解決できます。

Step 1: package.json の type: module 設定確認

最初に確認すべきは、プロジェクトが ESM を使用することを明示的に宣言しているかどうかです。

json{
  "name": "your-project",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "test": "jest"
  }
}

type: "module" の設定により、プロジェクト全体で ESM が有効になります。この設定がない場合、Jest は CommonJS として解釈するため、import 文でエラーが発生します。

Step 2: Jest 設定ファイルの transform 設定点検

Jest は標準では ESM をサポートしていないため、適切な transform 設定が必要です。以下の設定を確認しましょう。

javascript// jest.config.js
export default {
  preset: 'ts-jest/presets/default-esm',
  extensionsToTreatAsEsm: ['.ts', '.tsx'],
  globals: {
    'ts-jest': {
      useESM: true,
    },
  },
};

この設定により、TypeScript ファイルを ESM として正しく変換できます。

Step 3: moduleNameMapping と resolver の調整

外部パッケージや内部モジュールの解決には、moduleNameMapping の設定が重要です。

javascriptexport default {
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '^~/(.*)$': '<rootDir>/$1',
  },
  resolver: '<rootDir>/jest.resolver.js',
};

パスエイリアスを使用している場合は、Jest がモジュールを正しく解決できるよう設定を追加します。

Step 4: 外部パッケージの ESM 対応確認

Node.js の標準モジュールや外部ライブラリが ESM に対応していない場合は、transformIgnorePatterns の調整が必要です。

javascriptexport default {
  transformIgnorePatterns: [
    'node_modules/(?!(es6-package|another-esm-package)/)',
  ],
};

この設定により、特定のパッケージのみ transform の対象に含めることができます。

以下の図は、Jest ESM 解決フローの全体像を示しています。

mermaidflowchart LR
    start[プロジェクト開始] --> step1[Step1: type module設定]
    step1 --> step2[Step2: transform設定]
    step2 --> step3[Step3: resolver設定]
    step3 --> step4[Step4: 外部パッケージ対応]
    step4 --> test[テスト実行]
    test --> success[成功]
    test --> error[エラー]
    error --> debug[デバッグ]
    debug --> step1

このフローに従うことで、段階的かつ確実に問題を解決できます。

各解決手順の詳細実装

ここからは、各ステップの具体的な実装方法を詳しく見ていきましょう。実際のコードサンプルと共に解説します。

type: module 設定の最適化

package.json での ESM 宣言は、プロジェクト全体の モジュール解決方法を決定する重要な設定です。

json{
  "name": "jest-esm-project",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  }
}

exports フィールドを使用することで、ESM と CommonJS の両方をサポートできます。これにより、他のプロジェクトからの利用時にも互換性を保てます。

また、Jest 専用の設定も追加しておくと安全です。

json{
  "scripts": {
    "test": "node --experimental-vm-modules node_modules/.bin/jest",
    "test:watch": "node --experimental-vm-modules node_modules/.bin/jest --watch"
  }
}

--experimental-vm-modules フラグにより、Node.js の ESM サポートを有効にします。

transform 設定のトラブルシューティング

TypeScript プロジェクトでの Jest ESM 対応には、ts-jest の ESM モードを使用します。

javascript// jest.config.js
export default {
  preset: 'ts-jest/presets/default-esm',

  // ESM として扱うファイル拡張子を指定
  extensionsToTreatAsEsm: ['.ts', '.tsx', '.jsx'],

  // ts-jest の ESM 設定
  globals: {
    'ts-jest': {
      useESM: true,
      tsconfig: {
        module: 'es6',
        target: 'es2020',
      },
    },
  },

  // テスト環境の設定
  testEnvironment: 'node',
};

JavaScript のみのプロジェクトの場合は、Babel を使用します。

javascriptexport default {
  transform: {
    '^.+\\.jsx?$': 'babel-jest',
  },

  // Babel 設定
  transformIgnorePatterns: [
    'node_modules/(?!(@babel|es6-module)/)',
  ],
};

対応する .babelrc.json の設定も必要です。

json{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": { "node": "current" },
        "modules": false
      }
    ]
  ],
  "env": {
    "test": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "targets": { "node": "current" }
          }
        ]
      ]
    }
  }
}

resolver 設定による依存関係解決

カスタム resolver を作成することで、複雑なモジュール解決ルールに対応できます。

javascript// jest.resolver.js
export default (request, options) => {
  // パスエイリアスの解決
  if (request.startsWith('@/')) {
    return options.defaultResolver(
      request.replace('@/', './src/'),
      options
    );
  }

  // 外部モジュールの ESM バージョンを優先
  if (
    !request.startsWith('.') &&
    !request.startsWith('/')
  ) {
    try {
      return options.defaultResolver(
        request + '/esm',
        options
      );
    } catch {
      // フォールバック
    }
  }

  return options.defaultResolver(request, options);
};

このカスタム resolver により、プロジェクト特有のモジュール解決ルールを実装できます。

moduleNameMapping との組み合わせも効果的です。

javascriptexport default {
  moduleNameMapping: {
    // パスエイリアス
    '^@/(.*)$': '<rootDir>/src/$1',
    '^~/(.*)$': '<rootDir>/$1',

    // 外部ライブラリの ESM バージョンを指定
    '^lodash$': 'lodash-es',
    '^react-router$': 'react-router/esm',
  },

  resolver: '<rootDir>/jest.resolver.js',
};

実践的なトラブルシューティング事例

実際のプロジェクトで頻繁に遭遇する問題パターンと、その具体的な解決方法をケース別に解説します。

ケース別解決方法

ケース 1: Next.js プロジェクトでの ESM エラー

Next.js プロジェクトでは、以下のような設定が必要です。

javascript// jest.config.js
const nextJest = require('next/jest');

const createJestConfig = nextJest({
  dir: './',
});

const customJestConfig = {
  preset: 'ts-jest/presets/default-esm',
  extensionsToTreatAsEsm: ['.ts', '.tsx'],

  globals: {
    'ts-jest': {
      useESM: true,
    },
  },

  moduleNameMapping: {
    '^@/components/(.*)$': '<rootDir>/components/$1',
    '^@/pages/(.*)$': '<rootDir>/pages/$1',
  },

  testEnvironment: 'jsdom',
};

module.exports = createJestConfig(customJestConfig);

ケース 2: React + Vite プロジェクトでのテスト設定

javascript// jest.config.js
export default {
  preset: 'ts-jest/presets/default-esm',
  testEnvironment: 'jsdom',

  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],

  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
  },

  transform: {
    '^.+\\.tsx?$': ['ts-jest', { useESM: true }],
    '^.+\\.jsx?$': 'babel-jest',
  },

  extensionsToTreatAsEsm: ['.ts', '.tsx'],
};

ケース 3: Monorepo での設定

javascript// packages/shared/jest.config.js
export default {
  preset: 'ts-jest/presets/default-esm',

  moduleNameMapping: {
    '^@shared/(.*)$': '<rootDir>/src/$1',
    '^@utils/(.*)$': '<rootDir>/../utils/src/$1',
  },

  // 他のワークスペースパッケージを transform 対象に含める
  transformIgnorePatterns: [
    'node_modules/(?!(@your-org)/)',
  ],
};

設定ファイルの完全版サンプル

以下は、最も汎用的で安定した Jest ESM 設定の完全版です。

javascript// jest.config.js
export default {
  // 基本設定
  preset: 'ts-jest/presets/default-esm',
  testEnvironment: 'node',

  // ESM 対応設定
  extensionsToTreatAsEsm: ['.ts', '.tsx', '.jsx'],

  // ts-jest 設定
  globals: {
    'ts-jest': {
      useESM: true,
      tsconfig: {
        module: 'es6',
        target: 'es2020',
        moduleResolution: 'node',
      },
    },
  },

  // モジュール解決設定
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '^~/(.*)$': '<rootDir>/$1',
    '^tests/(.*)$': '<rootDir>/tests/$1',
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|gif|svg)$':
      '<rootDir>/__mocks__/fileMock.js',
  },

  // Transform 設定
  transform: {
    '^.+\\.tsx?$': ['ts-jest', { useESM: true }],
    '^.+\\.jsx?$': 'babel-jest',
  },

  // 外部モジュールの transform 設定
  transformIgnorePatterns: [
    'node_modules/(?!(es6-package|@babel|lodash-es)/)',
  ],

  // テストファイルの設定
  testMatch: [
    '<rootDir>/src/**/__tests__/**/*.{ts,tsx,js,jsx}',
    '<rootDir>/src/**/*.{test,spec}.{ts,tsx,js,jsx}',
  ],

  // セットアップファイル
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],

  // カバレッジ設定
  collectCoverageFrom: [
    'src/**/*.{ts,tsx,js,jsx}',
    '!src/**/*.d.ts',
    '!src/**/index.{ts,tsx,js,jsx}',
  ],

  // カスタム resolver
  resolver: '<rootDir>/jest.resolver.js',
};

対応するセットアップファイルも作成します。

javascript// jest.setup.js
import { jest } from '@jest/globals';

// グローバルモックの設定
global.jest = jest;

// fetch のモック(Node.js 18未満の場合)
if (!global.fetch) {
  global.fetch = jest.fn();
}

// その他の必要なセットアップ
beforeEach(() => {
  jest.clearAllMocks();
});

この設定により、ほとんどの ESM 関連問題を解決できます。プロジェクトの要件に応じて、必要な部分をカスタマイズしてご利用ください。

まとめ

Jest で ESM が通らない問題は、体系的なアプローチにより確実に解決できます。

本記事で解説した 4 つのステップを順序立てて実行することで、複雑に見える ESM 問題も段階的に解決できることがお分かりいただけたでしょう。特に重要なポイントは以下の通りです。

解決の鍵となる設定項目:

  • package.json の type: "module" 宣言
  • Jest 設定での transform と extensionsToTreatAsEsm
  • moduleNameMapping によるパス解決
  • 外部パッケージの transformIgnorePatterns 調整

エラーメッセージを正確に読み解き、適切な診断フローに従うことで、問題の根本原因を素早く特定できます。また、プロジェクトの特性(Next.js、React + Vite、Monorepo など)に応じた設定調整も重要です。

Jest ESM 対応は一度設定すれば安定して動作します。この記事の設定を参考に、ぜひ快適な ESM テスト環境を構築してください。

関連リンク