Node.js(TypeScript)导入Keycloak Admin Client遇ESM/CJS兼容问题
问题背景
在Node.js(TypeScript)项目中使用@keycloak/keycloak-admin-client时,遭遇ES6/CommonJS模块兼容错误:代码中明明用了ES模块的import语法,运行时却提示require()加载ES模块不支持。尝试添加"type": "module"到package.json后,又出现TypeError: Unknown file extension ".ts"错误,动态导入类名时也频繁出现编译语法错误。
代码片段
import [... whatever ...] import KcAdminClient from '@keycloak/keycloak-admin-client'; import { Credentials } from '@keycloak/keycloak-admin-client/lib/utils/auth'; export class Controller { private kcAdminClient = new KcAdminClient(); [...] }
错误信息
Error [ERR_REQUIRE_ESM]: require() of ES Module .../node_modules/@keycloak/keycloak-admin-client/lib/index.js from .../server/logic/auth/users.ts not supported. Instead change the require of index.js in .../server/logic/auth/users.ts to a dynamic import() which is available in all CommonJS modules. at Object.<anonymous> (.../server/logic/auth/users.ts:10:49) at m._compile (.../node_modules/ts-node/dist/index.js:791:29) at require.extensions.<computed> [as .ts] (.../node_modules/ts-node/dist/index.js:793:16) { code: 'ERR_REQUIRE_ESM' }
核心疑问解答
1. “这类库”指什么?
这类库是纯ES模块库:即库的package.json中声明了"type": "module",或所有输出文件为.mjs格式,只能被ES模块加载,无法被CommonJS的require()直接调用。@keycloak/keycloak-admin-client就属于这类库,它的构建产物是纯ES模块,不兼容CommonJS的同步加载逻辑。
你的代码虽然写了import,但如果项目默认是CommonJS环境(比如package.json无"type": "module"、TypeScript配置module设为CommonJS),TypeScript会把import编译成require(),这就触发了错误——用require()加载纯ES模块。
解决方案
方案一:将项目切换为ES模块环境
这是最彻底的解决方式,让项目整体与目标库的模块类型保持一致:
- 在项目根目录
package.json中添加:"type": "module" - 修改
tsconfig.json配置,确保输出为ES模块:{ "compilerOptions": { "module": "ESNext", // 或ES2020及以上版本 "moduleResolution": "NodeNext", "target": "ES2020", "esModuleInterop": true, "skipLibCheck": true }, "include": ["src/**/*"] } - 解决
.ts文件扩展名错误:- 若用
ts-node,需安装ts-node-esm,修改启动命令为:ts-node-esm your-entry-file.ts - 或改用
tsx替代ts-node,它原生支持ES模块和TypeScript,启动命令更简洁:tsx your-entry-file.ts
- 若用
方案二:在CommonJS环境中使用动态导入
如果不想切换项目模块类型,可通过动态导入(import())加载纯ES模块,注意动态导入是异步操作,需调整代码结构:
import [... whatever ...] export class Controller { // 先声明类型,避免类型报错 private kcAdminClient: Awaited<typeof import('@keycloak/keycloak-admin-client').default>; private credentialsType: typeof import('@keycloak/keycloak-admin-client/lib/utils/auth').Credentials; constructor() { this.initKeycloak(); } private async initKeycloak() { // 动态导入库及类型 const KcAdminClient = (await import('@keycloak/keycloak-admin-client')).default; const { Credentials } = await import('@keycloak/keycloak-admin-client/lib/utils/auth'); this.kcAdminClient = new KcAdminClient(); this.credentialsType = Credentials; // 后续初始化逻辑,比如配置Keycloak连接 await this.kcAdminClient.setConfig({ baseUrl: 'http://your-keycloak-url', realm: 'master', }); } // 所有依赖kcAdminClient的方法都要改为异步 async someBusinessMethod() { await this.kcAdminClient.auth({ clientId: 'admin-cli', username: 'admin', password: 'admin', grantType: 'password', }); // 调用Admin Client的其他方法 } }
注意:因动态导入是异步的,所有依赖kcAdminClient的操作必须等待初始化完成,避免出现undefined调用错误。
内容的提问来源于stack exchange,提问作者Lucio Crusca

