Skip to content

Published dist output uses extensionless relative imports, breaking Node's native ESM resolver (ERR_MODULE_NOT_FOUND) #267

Description

@tplass-ias

Summary

Every published package in this SDK (@databricks/sdk-auth, @databricks/sdk-core, @databricks/sdk-scim, and likely others under packages/*) fails to load under Node's native ESM loader with ERR_MODULE_NOT_FOUND, because the compiled dist/*.js output uses extensionless relative import/export specifiers (e.g. from './errors' instead of from './errors.js').

Since each package's package.json declares "type": "module", Node resolves these packages with the strict ESM resolver, which requires explicit file extensions on relative specifiers (Node docs). tsc's moduleResolution: "bundler" (set in tsconfig.base.json) permits extensionless specifiers at compile time on the assumption that a downstream bundler will resolve them — but the packages here are shipped as plain tsc -b output with no bundling step, so nothing ever adds the extensions before publish.

Repro

npm install @databricks/sdk-auth
node -e "require('@databricks/sdk-auth')"
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/node_modules/@databricks/sdk-auth/dist/auth' imported from /node_modules/@databricks/sdk-auth/dist/index.js

Same failure reproduces on the credentials subpath export:

node -e "require('@databricks/sdk-auth/credentials')"
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/node_modules/@databricks/sdk-auth/dist/credentials/errors' imported from /node_modules/@databricks/sdk-auth/dist/credentials/index.js

And in @databricks/sdk-core (a dependency of sdk-auth/sdk-scim), e.g. the profiles subpath used by sdk-auth's default-credentials chain:

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/node_modules/@databricks/sdk-core/dist/profiles/errors' imported from /node_modules/@databricks/sdk-core/dist/profiles/index.js

And @databricks/sdk-scim's v1 subpath:

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/node_modules/@databricks/sdk-scim/dist/v1/client' imported from /node_modules/@databricks/sdk-scim/dist/v1/index.js

Reproduced against the latest published version at the time of filing (@databricks/sdk-auth@0.21.0), so this isn't specific to an old release — I confirmed the same extensionless imports are present in the current main branch's compiled output shape (tsconfig.base.json still sets moduleResolution: "bundler").

Environment

  • Node.js v24.18.0 (also reproduces on other Node 20+/22+ versions using the native ESM loader — this is a Node spec requirement, not version-specific)
  • @databricks/sdk-auth@0.17.0, 0.18.0, 0.21.0 (latest at time of filing)
  • @databricks/sdk-core@0.18.0
  • @databricks/sdk-scim@0.17.0

Root cause

tsconfig.base.json:

{
  "compilerOptions": {
    "moduleResolution": "bundler",
    ...
  }
}

packages/*/package.json build script: "build": "tsc -b" — plain tsc compile, no bundler in the publish pipeline, yet moduleResolution: "bundler" is what permits the extensionless specifiers to type-check. Combined with "type": "module" in each published package's package.json, this produces output that is invalid for Node's ESM resolver.

Suggested fix

Either:

  1. Switch to moduleResolution: "node16"/"nodenext" (requires explicit extensions in source import statements, matching what actually gets published), or
  2. Add a build step (e.g. via tsc-alias, a small custom transform, or a bundler like tsup/esbuild) that rewrites/appends .js to relative specifiers in the emitted dist/*.js before publish.

Either fix needs to land in every packages/* package that ships compiled ESM dist/*.js, since the same tsconfig.base.json inheritance affects all of them, not just the three I hit directly.

Impact

This makes every SDK package here unusable under plain require()/import in Node — they can currently only "work" if bundled through a tool (webpack/esbuild/rollup) that resolves extensionless specifiers itself, which most Node.js backend service code does not do. For consumers running compiled/backend Node.js code directly (no bundler in the loop — our case, a Backstage backend plugin), every entry point that imports across files within these packages throws at require-time.

(filed w/ Claude Code)

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