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 '@orient/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 '@orient/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
@orient/mcp-tools
└── imports @orient/agents (to get PromptService)
└── imports @orient/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 '@orient/agents';
// ✅ Import the actual implementation package
import { MessageDatabase } from '@orient/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 │
│ │
│ @orient/core │
│ ↓ │
│ @orient/database │
│ ↓ │
│ @orient/database-services │
│ ↓ │
│ @orient/integrations │
│ ↓ │
│ @orient/agents │
│ ↓ │
│ @orient/mcp-tools │
│ ↓ │
│ @orient/bot-whatsapp | @orient/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 @orient/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)
chat Comments (0)
Sign in to join the discussion and leave a comment.
Skill Details
Related Skills
Build your own?
Join 12,000+ developers contributing to the Claude ecosystem.
No comments yet. Be the first to share your thoughts!