Claude-skill-registry esm-cjs-interop
Handle ESM/CJS module interoperability in the Orient monorepo. Use when encountering import errors, circular dependencies, module resolution issues, or "does not provide an export named" errors. Covers import patterns, re-export strategies, and avoiding circular dependency chains.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/esm-cjs-interop" ~/.claude/skills/majiayu000-claude-skill-registry-esm-cjs-interop && rm -rf "$T"
skills/data/esm-cjs-interop/SKILL.mdESM/CJS Interoperability
Quick Reference
# Check if package is ESM or CJS grep '"type"' packages/*/package.json # Find circular dependency issues npx madge --circular packages/*/src/index.ts
Module System in This Monorepo
This monorepo uses ESM (
"type": "module") for all packages:
// packages/*/package.json { "type": "module", "module": "NodeNext", "moduleResolution": "NodeNext" }
However, some legacy code in
src/ may still use CJS patterns, creating interop issues.
Required: .js Extensions for ESM
ESM requires explicit
.js extensions in import paths, even for TypeScript files:
// ✅ Correct - explicit .js extension import { loadConfig } from './config/index.js'; import { createLogger } from '@orientbot/core'; // ❌ Wrong - missing extension (works in CJS, fails in ESM) import { loadConfig } from './config/index';
Common Error: "Does not provide an export named"
This error occurs when CJS modules are re-exported incorrectly in ESM:
Error: The requested module '@orientbot/package' does not provide an export named 'SomeClass'
Solution: Use Default Import + Re-export
// ❌ Wrong - doesn't work with CJS modules export * from './some-cjs-module.js'; export { SomeClass } from './some-cjs-module.js'; // ✅ Correct - use default import first import SomeCjsModule from './some-cjs-module.js'; export const { SomeClass, someFunction } = SomeCjsModule; // Or re-export the whole module export default SomeCjsModule;
Circular Dependency Prevention
Circular dependencies cause runtime failures, especially when ESM and CJS mix.
Pattern: Circular Dependency Chain
@orientbot/mcp-tools └── imports @orientbot/agents (to get PromptService) └── imports @orientbot/mcp-tools (for tool definitions) └── CIRCULAR!
Solution: Import Lower-Level Package
Instead of importing a high-level package that re-exports, import the specific lower-level package:
// ❌ Creates circular dependency import { PromptService } from '@orientbot/agents'; // ✅ Import the actual implementation package import { MessageDatabase } from '@orientbot/database-services'; // Use MessageDatabase.setSystemPrompt directly
Identifying Circular Dependencies
- Build fails with cryptic ESM error - often circular deps
- Import works in tests but fails at runtime - module loading order differs
- "Cannot access before initialization" - circular import timing issue
Debug Command
# Check for circular dependencies npx madge --circular --extensions ts packages/*/src/index.ts # Visualize dependency graph npx madge --image deps.svg packages/mcp-tools/src/index.ts
Package Dependency Direction
Follow this dependency direction to avoid cycles:
┌─────────────────────────────────────────────────┐ │ Allowed Import Direction │ │ │ │ @orientbot/core │ │ ↓ │ │ @orientbot/database │ │ ↓ │ │ @orientbot/database-services │ │ ↓ │ │ @orientbot/integrations │ │ ↓ │ │ @orientbot/agents │ │ ↓ │ │ @orientbot/mcp-tools │ │ ↓ │ │ @orientbot/bot-whatsapp | @orientbot/bot-slack │ └─────────────────────────────────────────────────┘
Packages can only import from packages above them in this hierarchy.
Re-Export Patterns
Barrel File (index.ts) Pattern
// packages/*/src/index.ts // Re-export types (always safe) export type { ConfigOptions, LogLevel } from './types/index.js'; // Re-export ESM modules export { loadConfig } from './config/index.js'; export { createLogger } from './logger/index.js'; // Re-export default exports export { default as SomeClass } from './SomeClass.js';
Conditional ESM/CJS Exports
Use
package.json exports field for dual support:
{ "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs", "default": "./dist/index.js" } } }
Troubleshooting
Import Works in Tests, Fails in Runtime
Tests may use different module resolution (esbuild/vite transforms). Check:
- Is the import target an ESM or CJS module?
- Are you using
with a CJS module?export * - Is there a circular dependency in the runtime load order?
"Module not found" After Build
# Ensure dist/ exists ls packages/*/dist/ # Rebuild the package pnpm --filter @orientbot/package-name build # Check exports in package.json match dist/ structure
Dynamic Import for CJS Modules
When importing CJS modules dynamically:
// ESM dynamic import of CJS module const cjsModule = await import('./cjs-module.cjs'); const { someExport } = cjsModule.default || cjsModule;
Best Practices
- Use explicit
extensions in all import paths.js - Avoid
with modules that might be CJSexport * - Import from specific packages rather than re-export barrels
- Follow dependency hierarchy to prevent circular deps
- Test imports at runtime not just in vitest (different resolution)