name: esm-cjs-interop description: 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.
ESM/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
export *with a CJS module? - 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
.jsextensions in all import paths - Avoid
export *with modules that might be CJS - 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)