ESM/CSJ compatibility issue with @cubejs-client/core cube client library
- Dominant language
- Rust
- Stars
- 20.8k
- Forks
- 2.1k
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 181
Description
## Describe the bug
The `@cubejs-client/core` ESM build (`dist/src/index.js`) contains extensionless relative imports (e.g., `import ResultSet from './ResultSet'`) which fail in Node.js ESM strict mode.
Node.js ESM specification requires explicit file extensions for relative imports. When running in strict ESM environments (using `tsx`, `ts-node --esm`, Vite, or native Node.js ESM), the module resolution fails because Node cannot resolve `./ResultSet` without the `.js` extension.
This affects any project using:
- `"type": "module"` in `package.json`
- TypeScript loaders in ESM mode (`tsx`, `ts-node`)
- Modern build tools with strict ESM (Vite, esbuild)
- Node.js with native ESM imports
## To Reproduce
**Steps to reproduce:**
1. Create a new Node.js ESM project:
```bash
mkdir cube-esm-test
cd cube-esm-test
npm init -y
npm install @cubejs-client/core tsx
```
2. Update `package.json`:
```json
{
"type": "module"
}
```
3. Create `test.ts`:
```typescript
import cubejs from '@cubejs-client/core';
const client = cubejs('token', {
apiUrl: 'http://localhost:4000/cubejs-api/v1'
});
const meta = await client.meta();
console.log(meta);
```
4. Run with `tsx`:
```bash
npx tsx test.ts
```
**Error:**
```
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/path/to/node_modules/@cubejs-client/core/dist/src/ResultSet' imported from /path/to/node_modules/@cubejs-client/core/dist/src/index.js
```
## Expected behavior
The ESM build should use explicit `.js` extensions for all relative imports, allowing Node.js to properly resolve modules:
```javascript
// Current (broken)
import ResultSet from './ResultSet';
// Expected (working)
import ResultSet from './ResultSet.js';
```
## Root Cause
Looking at `node_modules/@cubejs-client/core/dist/src/index.js`:
```javascript
import ResultSet from './ResultSet'; // ❌ Missing .js extension
import SqlQuery from './SqlQuery'; // ❌ Missing .js extension
import Meta from './Meta'; // ❌ Missing .js extension
// ... etc
```
The TypeScript compilation doesn't automatically add `.js` extensions to emitted ESM output, and the build process doesn't have a post-processing step to add them.
## Suggested Solutions
### Option 1: Fix TypeScript Build (Recommended)
Add a post-build step to rewrite import paths:
```bash
# After tsc compilation, add .js extensions
npx fix-esm-import-path dist/src
```
Or use a bundler that handles this automatically:
```javascript
// tsup.config.ts
export default {
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
// tsup automatically fixes import extensions
}
```
### Option 2: Expose CJS in Exports (Interim)
Update `package.json` to provide a CJS fallback:
```json
{
"exports": {
".": {
"import": "./dist/src/index.js",
"require": "./dist/cubejs-client-core.cjs.js",
"default": "./dist/src/index.js"
}
}
}
```
This allows consumers to fall back to the working CJS bundle when ESM fails.
## Workaround
Currently, users must implement a dual ESM/CJS loader:
```typescript
async function loadCubeFactory() {
try {
// Try ESM
const esm = await import("@cubejs-client/core");
return esm.default ?? esm;
} catch {
// Fallback to CJS via createRequire
const { createRequire } = await import("node:module");
const require = createRequire(import.meta.url);
const cjs = require("@cubejs-client/core");
return cjs.default ?? cjs;
}
}
const factory = await loadCubeFactory();
const client = factory('token', { apiUrl: 'http://localhost:4000/cubejs-api/v1' });
```
## Version
- `@cubejs-client/core`: 1.3.73 (latest)
- Node.js: 20.x, 22.x, 24.x (all affected)
- TypeScript: 5.x
## Additional Context
This is a common issue in the TypeScript/Node.js ESM ecosystem. Similar issues have been fixed by other libraries:
- https://github.com/nodejs/node/blob/main/doc/api/esm.md#mandatory-file-extensions
- TypeScript tracking issue: https://github.com/microsoft/TypeScript/issues/16577
The ESM specification requires explicit file extensions for relative imports to avoid ambiguity and improve resolution performance. Many modern tools (Vite, Next.js 13+, Remix, etc.) enforce this strictly.
Related discussions:
- https://nodejs.org/api/esm.html#esm_mandatory_file_extensions
- https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
Contributor guide
Assessment
This issue has not been assessed yet.