-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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"
inpackage.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:
- Create a new Node.js ESM project:
mkdir cube-esm-test
cd cube-esm-test
npm init -y
npm install @cubejs-client/core tsx
- Update
package.json
:
{
"type": "module"
}
- Create
test.ts
:
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);
- Run with
tsx
:
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:
// 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
:
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:
# After tsc compilation, add .js extensions
npx fix-esm-import-path dist/src
Or use a bundler that handles this automatically:
// 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:
{
"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:
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: Provide a way to add the '.js' file extension to the end of module specifiers microsoft/TypeScript#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: