Skip to content

ESM/CSJ compatibility issue with @cubejs-client/core cube client library #10041

@dmeshkov-brex

Description

@dmeshkov-brex

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:
mkdir cube-esm-test
cd cube-esm-test
npm init -y
npm install @cubejs-client/core tsx
  1. Update package.json:
{
  "type": "module"
}
  1. 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);
  1. 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:

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:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions