Skip to content

feat: Support named ESM exports across all drivers - #11838

Merged
ovr merged 1 commit into
masterfrom
feat/drivers-esm-support
Sep 10, 2026
Merged

feat: Support named ESM exports across all drivers#11838
ovr merged 1 commit into
masterfrom
feat/drivers-esm-support

Conversation

@ovr

@ovr ovr commented Sep 10, 2026

Copy link
Copy Markdown
Member

import { PostgresDriver } from '@cubejs-backend/postgres-driver' threw does not provide an export named 'PostgresDriver'. Node discovers a CJS module's named exports with cjs-module-lexer, a static source scan, and the hand-written root index.js shim hides them behind a runtime loop:

const toExport = PostgresDriver;
for (const [key, module] of Object.entries(fromExports)) {
  toExport[key] = module;
}
module.exports = toExport;

so ESM saw only a synthetic default. 27 of the 28 drivers in DriverDependencies were affected, in three shapes:

Shape Packages Change
Root index.js shim (TS) 20 exports map routing import at the tsc output

The tsc-emitted dist/src/index.js is already lexer-friendly, so the 20 shim drivers need no wrapper file — only a conditional exports map, matching the shape cubejs-client-core already uses. The plain-JS one-liner is the idiom DremioDriver.js already used for applyParams.

CommonJS still resolves to the same index.js, and both conditions load one underlying module instance, so require(pkg) === (await import(pkg)).XDriver continues to hold.

Note: on the 20 shim drivers import X from '@cubejs-backend/x-driver' now yields the module namespace rather than the driver class, because Node always sets a CJS module's ESM default to module.exports and ignores __esModule. This is not treated as a breaking change: the ESM surface of these packages was not usable before — no export could be imported by name, and the class only arrived as default as a side effect of the shim assigning it to module.exports, never as a designed entry point. Nothing in the repo or the docs imports a driver that way; every documented sample uses require, which is unaffected. Named imports are now the one consistent path across all 28 drivers.

Extension-less deep imports (.../dist/src/PostgresDriver) also stop resolving now that subpaths are explicit; ./dist/* keeps the extension-ful form working, and nothing in the repo, docs, or examples deep-imports a driver.

Verified by probing every driver in its own subprocess, deriving the expected class name from CommonJS and asserting ESM exposes the same object: 27 of 28 packages fail before this change, all 28 pass after.

`import { PostgresDriver } from '@cubejs-backend/postgres-driver'` threw
`does not provide an export named 'PostgresDriver'`. Node discovers a CJS
module's named exports with cjs-module-lexer, a static source scan, and the
hand-written root `index.js` shim hides them behind a runtime loop:

    const toExport = PostgresDriver;
    for (const [key, module] of Object.entries(fromExports)) {
      toExport[key] = module;
    }
    module.exports = toExport;

so ESM saw only a synthetic `default`. 27 of the 28 drivers in
DriverDependencies were affected, in three shapes:

| Shape | Packages | Change |
| --- | --- | --- |
| Root `index.js` shim (TS) | 20 | `exports` map routing `import` at the tsc output |
| `main` at dist, no named re-export | 1 (druid) | `export { DruidDriver };` |
| Plain JS, bare `module.exports = Class` | 6 | `module.exports.XDriver = XDriver;` |
| Already correct | 1 (databricks-jdbc) | none |

The tsc-emitted `dist/src/index.js` is already lexer-friendly, so the 20 shim
drivers need no wrapper file — only a conditional `exports` map, matching the
shape cubejs-client-core already uses. The plain-JS one-liner is the idiom
DremioDriver.js already used for `applyParams`.

CommonJS still resolves to the same `index.js`, and both conditions load one
underlying module instance, so `require(pkg) === (await import(pkg)).XDriver`
continues to hold.

Note: on the 20 shim drivers `import X from '@cubejs-backend/x-driver'` now
yields the module namespace rather than the driver class, because Node always
sets a CJS module's ESM `default` to `module.exports` and ignores `__esModule`.
This is not treated as a breaking change: the ESM surface of these packages was
not usable before — no export could be imported by name, and the class only
arrived as `default` as a side effect of the shim assigning it to
`module.exports`, never as a designed entry point. Nothing in the repo or the
docs imports a driver that way; every documented sample uses `require`, which is
unaffected. Named imports are now the one consistent path across all 28 drivers.

Extension-less deep imports (`.../dist/src/PostgresDriver`) also stop resolving
now that subpaths are explicit; `./dist/*` keeps the extension-ful form working,
and nothing in the repo, docs, or examples deep-imports a driver.

Verified by probing every driver in its own subprocess, deriving the expected
class name from CommonJS and asserting ESM exposes the same object: 27 of 28
packages fail before this change, all 28 pass after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ovr
ovr requested a review from a team as a code owner September 10, 2026 15:55
@github-actions github-actions Bot added driver:mongodb Issues relating to the MongoBI driver driver:redshift Issues relating to the Redshift driver driver:mysql Issues relating to the MySQL/MariaDB driver driver:bigquery Issues related to the BigQuery driver driver:snowflake Issues relating to the Snowflake driver driver:clickhouse Issues related to the ClickHouse driver driver:athena Issues related to the AWS Athena driver driver:mssql Issues relating to the MSSQL driver driver:oracle Issues relating to the Oracle driver driver:prestodb Issues relating to the PrestoDB driver driver:postgres Issues relating to the Postgres driver driver:hive Issues relating to the Apache Hive driver driver:mysql-aurora-serverless Issues relating to the AWS MySQL Aurora Serverless driver driver:sqlite Issues relating to the SQLite driver driver:druid Issues relating to the Druid driver driver:crate driver:dremio driver:questdb javascript Pull requests that update Javascript code driver:firebolt data source driver driver:duckdb driver:trino Issues relating to the Trino driver driver:materialize driver:pinot labels Sep 10, 2026
@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @ovr's task in 4m 57s —— View job


Review complete — sound approach, 1 medium + 2 low. 2 inline comments posted.

Full review
  • Gather context (read diff + all 27 changed files in place)
  • Verify exports maps against each package's main, files, typings, and actual on-disk layout
  • Check all 28 DriverDependencies entries are covered, and check the shape of each of the three fixes
  • Check side effects: deep imports, CJS resolution, files/publish, type resolution
  • Post review

Verdict

The diagnosis is correct and the fix is the right one. The runtime Object.entries loop in the root index.js shim is invisible to cjs-module-lexer, so Node's CJS→ESM named-export detection finds nothing; routing the import condition straight at the tsc-emitted dist/src/index.js, whose __exportStar reexports the lexer does follow, is the minimal correct answer and avoids 20 new wrapper files.

I verified the mechanical parts against the tree rather than the diff:

Check Result
All 28 DriverDependencies entries accounted for ✅ 20 + 1 + 6 + databricks-jdbc = 28
The 6 plain-JS drivers' main points at the file that was edited ✅ all 6 (src/HiveDriver.js, driver/OracleDriver.js, driver/SqliteDriver.js, src/VerticaDriver.js, driver/AuroraServerlessMySqlDriver.js, driver/DremioDriver.js)
require target is inside files for every package that got exports ✅ — cubejs-cubestore-driver omits index.js from files, but npm always ships the main file, and the require target is that same file
./dist/* covers everything else the packages publish ✅ incl. dist/codegen/* in cubestore
Deep imports of driver subpaths anywhere in repo/docs/examples ✅ none — the extension-less-import caveat in the PR body is real but unexercised
@cubejs-backend/base-driver (the package third-party custom drivers import) ✅ already main: dist/src/index.js, so named ESM imports already worked — no gap
require(pkg) === (await import(pkg)).XDriver ✅ both conditions load the one dist/src/index.js instance

The 6 plain-JS one-liners are exactly right, non-breaking in every direction (the class stays module.exports, so the ESM default is unchanged), and consistent with the applyParams idiom already in DremioDriver.js.

Findings

Medium — types promises a default export the import condition doesn't deliver (inline, packages/cubejs-postgres-driver/package.json:20-24, applies to all 20). dist/src/index.d.ts carries export default PostgresDriver, so import PostgresDriver from '@cubejs-backend/postgres-driver'; new PostgresDriver(...) type-checks under node16/bundler but throws is not a constructor at runtime, because Node sets a CJS module's ESM default to module.exports. The PR body anticipates the default change and argues the ESM surface was unusable before — fair for named imports, but the default import did work, and the failure is now silent rather than loud. Either add a one-file-per-package index.mjs that preserves default, or flag it explicitly in the release notes, since this ships as feat: (minor).

Low — no "default" condition (inline). With exports present, main is never consulted; a resolver requesting neither import nor require now hard-fails instead of falling back. One line of insurance.

Low — no regression test lands with the fix. The subprocess probe described in the commit message ("27 of 28 fail before, 28 pass after") is the right verification, but it lives outside the repo. This is a packaging invariant that stayed broken for years precisely because nothing checked it, and any future edit to a package.json or a shim can silently reintroduce it. A test iterating DriverDependencies and asserting require(pkg) === (await import(pkg))[className] — the exact probe, checked in — would lock it down cheaply. Worth doing given the change touches 27 packages.

Nits (no action needed)

  • druid is now the only driver with a public entry point and no exports map, so its deep imports stay open while the other 20 are locked down. Harmless, just asymmetric.
  • module.exports.XDriver = XDriver makes each class carry a self-referential static that subclasses inherit (class My extends SqliteDriver {}My.SqliteDriver). No practical consequence.
  • "dist/package.json" in bigquery's and clickhouse's files is a dead entry — nothing generates that file. Pre-existing, unrelated to this PR.
· branch [`feat/drivers-esm-support`](https://github.com/cube-js/cube/tree/feat/drivers-esm-support)

@ovr ovr changed the title feat(drivers): support named ESM imports across all drivers feat: Support named ESM exports across all drivers Sep 10, 2026
Comment on lines +20 to +24
".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/src/index.js",
"require": "./index.js"
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The types condition now describes a default export that the import condition does not deliver. (Applies to all 20 packages that got this block; postgres is the exemplar.)

dist/src/index.d.ts is emitted from src/index.ts, which ends with export default PostgresDriver;. TypeScript will therefore type-check this under moduleResolution: node16/nodenext/bundler:

import PostgresDriver from '@cubejs-backend/postgres-driver';
new PostgresDriver({ ... }); // typed as the class

At runtime the import condition resolves ./dist/src/index.js, a CJS file, and Node sets a CJS module's ESM default to module.exports — i.e. the namespace object, not the class. The call becomes TypeError: PostgresDriver is not a constructor, with no compile-time signal. Before this PR the same import resolved through mainindex.js, whose module.exports is the class, so it worked.

The PR body treats this as acceptable because "the ESM surface of these packages was not usable before". That is true of named imports, but the default import worked and is the form an ESM consumer would already have reached for — and it now fails silently rather than loudly. Since this ships under feat: (a minor), it's worth either preserving the default or calling it out explicitly as breaking.

One file per package preserves both shapes and keeps the whole win:

// index.mjs
import mod from './dist/src/index.js';

export const PostgresDriver = mod.PostgresDriver;
export const PgClient = mod.PgClient;
export default mod.default ?? mod;

with "import": "./index.mjs". That is 20 new files; the cheaper alternative is to keep the current shape and document the default change in the release notes, since the type-level mismatch is the part that will actually cost users debugging time. Fix this →

Comment on lines +20 to +28
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/src/index.js",
"require": "./index.js"
},
"./dist/*": "./dist/*",
"./package.json": "./package.json"
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a "default" condition as the last key in the "." object (again, applies to all 20):

".": {
  "types": "./dist/src/index.d.ts",
  "import": "./dist/src/index.js",
  "require": "./index.js",
  "default": "./index.js"
}

Once exports exists, main is no longer consulted, and a resolver whose condition set contains neither import nor require gets no match at all — ERR_PACKAGE_PATH_NOT_EXPORTED — where previously it fell back to main. Node and mainstream bundlers always request one of the two, so this is insurance rather than a known break; the cost is one line, and it restores the pre-PR fallback for anything with a hand-configured conditionNames/resolve.conditions.

@ovr
ovr merged commit 1699910 into master Sep 10, 2026
101 of 103 checks passed
@ovr
ovr deleted the feat/drivers-esm-support branch September 10, 2026 16:21
@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 60.16%. Comparing base (047b457) to head (b18448a).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
packages/cubejs-druid-driver/src/index.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #11838      +/-   ##
==========================================
- Coverage   60.16%   60.16%   -0.01%     
==========================================
  Files         239      239              
  Lines       19204    19206       +2     
  Branches     3886     3886              
==========================================
+ Hits        11555    11556       +1     
- Misses       7099     7100       +1     
  Partials      550      550              
Flag Coverage Δ
cube-backend 60.16% <50.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

data source driver driver:athena Issues related to the AWS Athena driver driver:bigquery Issues related to the BigQuery driver driver:clickhouse Issues related to the ClickHouse driver driver:crate driver:dremio driver:druid Issues relating to the Druid driver driver:duckdb driver:firebolt driver:hive Issues relating to the Apache Hive driver driver:materialize driver:mongodb Issues relating to the MongoBI driver driver:mssql Issues relating to the MSSQL driver driver:mysql Issues relating to the MySQL/MariaDB driver driver:mysql-aurora-serverless Issues relating to the AWS MySQL Aurora Serverless driver driver:oracle Issues relating to the Oracle driver driver:pinot driver:postgres Issues relating to the Postgres driver driver:prestodb Issues relating to the PrestoDB driver driver:questdb driver:redshift Issues relating to the Redshift driver driver:snowflake Issues relating to the Snowflake driver driver:sqlite Issues relating to the SQLite driver driver:trino Issues relating to the Trino driver javascript Pull requests that update Javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants