-
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/srcOr 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: