ts-jest與ES模塊互操作:處理CommonJS依賴的最佳實踐

在TypeScript項目中使用Jest進行測試時,ES模塊(ESM)與CommonJS模塊的互操作性常常帶來挑戰。本文將系統介紹如何使用ts-jest實現ES模塊與CommonJS依賴的無縫協作,解決常見的模塊解析問題和兼容性錯誤。

ESM與CommonJS互操作的核心挑戰

當TypeScript項目配置為生成ES模塊(如設置"module": "ES2022"或"NodeNext")時,導入CommonJS依賴可能導致多種錯誤,例如SyntaxError: Cannot use import statement outside a module或require is not defined。這是因為:

ES模塊使用import/export語法,而CommonJS使用require/module.exports

Node.js對兩種模塊系統的處理方式存在根本差異

TypeScript的模塊解析邏輯與Jest的轉換流程可能衝突

官方文檔詳細説明了這些兼容性問題:ESM Support。

配置基礎:tsconfig與Jest的協同設置

正確配置TypeScript編譯器選項是解決模塊互操作問題的第一步。根據項目需求選擇以下兩種配置策略之一:

策略1:使用純ES模塊模式

{
  "compilerOptions": {
    "module": "ES2022",
    "target": "ESNext",
    "esModuleInterop": true,
    "moduleResolution": "NodeNext"
  }
}

策略2:使用混合模塊模式(Node16/NodeNext)

{
  "compilerOptions": {
    "module": "NodeNext",
    "target": "ESNext",
    "esModuleInterop": true,
    "isolatedModules": true
  }
}

混合模塊模式要求在package.json中設置"type": "module",並對.mts和.cts文件使用不同的處理策略。詳細説明見Hybrid Node module。

Jest配置的關鍵調整

使用ESM預設快速配置

ts-jest提供了預設函數簡化ESM配置,推薦在jest.config.ts中使用:

import type { Config } from 'jest'
import { createDefaultEsmPreset } from 'ts-jest'

export default {
  displayName: 'ts-only',
  ...createDefaultEsmPreset({
    tsconfig: 'tsconfig-esm.json',
  }),
} satisfies Config

這個預設會自動配置必要的轉換規則和文件擴展名處理。預設實現代碼可查看create-jest-preset.ts。

手動配置轉換規則

如果需要更精細的控制,可以手動配置Jest轉換選項:

import type { Config } from 'jest'
import { TS_EXT_TO_TREAT_AS_ESM, ESM_TS_TRANSFORM_PATTERN } from 'ts-jest'

export default {
  extensionsToTreatAsEsm: [...TS_EXT_TO_TREAT_AS_ESM],
  transform: {
    [ESM_TS_TRANSFORM_PATTERN]: [
      'ts-jest',
      {
        useESM: true,
        tsconfig: 'tsconfig-esm.json'
      },
    ],
  },
} satisfies Config

處理CommonJS依賴的實戰技巧

1. 使用動態import()加載CommonJS模塊

對於僅提供CommonJS版本的依賴,使用動態import替代靜態import:

// 替代 import { someFunction } from 'commonjs-package'
const { someFunction } = await import('commonjs-package')

2. 配置moduleNameMapper解決別名問題

當第三方庫使用非標準模塊路徑時,在Jest配置中添加映射規則:

export default {
  // ...其他配置
  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
    '^commonjs-package$': '<rootDir>/node_modules/commonjs-package/dist/index.cjs'
  }
}

3. 創建自定義解析器處理複雜場景

對於複雜的模塊解析需求,可以實現自定義Jest解析器:

import type { SyncResolver } from 'jest-resolve'

const resolver: SyncResolver = (path, options) => {
  // 處理.mjs/.cjs文件的解析邏輯
  if (path.endsWith('.mjs')) {
    return options.defaultResolver(path.replace('.mjs', '.mts'), options)
  }
  return options.defaultResolver(path, options)
}

export default resolver

然後在Jest配置中引用:

export default {
  // ...其他配置
  resolver: '<rootDir>/jest-resolver.ts'
}

運行與調試

使用以下命令啓動Jest測試,確保啓用Node.js的ESM支持:

node --experimental-vm-modules node_modules/jest/bin/jest.js

對於Yarn用户,可以使用:

yarn node --experimental-vm-modules $(yarn bin jest)

注意:Jest的ESM支持仍在完善中,某些高級功能如自動模擬可能受限。跟蹤最新進展可關注Jest ESM支持進度。

常見問題解決方案

錯誤場景

解決方案

參考文檔

SyntaxError: Cannot use import statement outside a module

確保package.json設置了"type": "module"並使用正確的文件擴展名

ESM Support

Error [ERR_REQUIRE_ESM]: require() of ES Module

require調用替換為動態import()

動態import()

Cannot find module 'xxx' or its corresponding type declarations

檢查moduleResolution設置,確保包含types目錄

模塊解析

項目示例參考

ts-jest倉庫提供了多個ESM配置示例,可直接參考或複用:

  • ts-only示例:純TypeScript項目的ESM配置
  • esm-features示例:展示各種ESM特性的測試用例
  • react-app示例:React+TypeScript項目的ESM配置

這些示例包含完整的package.json、tsconfig.json和jest.config.ts配置文件,可作為實際項目的模板。

通過以上策略,大多數ts-jest與ES模塊的互操作問題都能得到有效解決。關鍵是理解TypeScript、Jest和Node.js在模塊處理上的差異,並正確配置轉換流程。對於複雜場景,可結合自定義解析器和動態導入實現靈活的模塊處理邏輯。