Skip to content

ESM compat: please add .js extensions in import specifiers (or provide a proper exports map) #4

@codycordova

Description

@codycordova

Summary
In strict ESM environments (Next.js / Node 20 with TS "moduleResolution": "NodeNext"), @creit.tech/sorobandomains-sdk fails to load because the published ESM contains extensionless relative imports (e.g., ./types instead of ./types.js). Node’s ESM resolver does not infer file extensions unless subpaths are covered by a package.json exports map.

Package / Repo

  • npm: @creit.tech/sorobandomains-sdk
  • version: <please fill, e.g. 0.x.y>
  • repo: Creit-Tech/sorobandomains-sdk-js

Environment

  • OS: Windows 11
  • Node: 20.x
  • Package manager: npm
  • Framework: Next.js (App Router, ESM)
  • TypeScript config highlights:
    • "module": "ESNext"
    • "moduleResolution": "NodeNext"
    • "verbatimModuleSyntax": true
  • Also reproducible when running scripts with ts-node -P tsconfig.scripts.json

Steps to Reproduce

  1. Create a Next.js or Node ESM project using TypeScript with "moduleResolution": "NodeNext".
  2. Install @creit.tech/sorobandomains-sdk.
  3. Import the package:
    import { SorobanDomainsSDK } from "@creit.tech/sorobandomains-sdk";
  4. Start the app or run the script.

Actual Behavior
Node throws ERR_MODULE_NOT_FOUND for an internal file without a .js extension, for example:

Error [ERR_MODULE_NOT_FOUND]: Cannot find module ".../node_modules/@creit.tech/sorobandomains-sdk/dist/types"
imported from ".../node_modules/@creit.tech/sorobandomains-sdk/dist/index.js"

Expected Behavior
The package should load in strict ESM environments without custom resolvers, bundler-specific shims, or transpiler hacks.

Why This Happens

  • Node’s native ESM loader requires explicit file extensions for relative imports (e.g., ./x.js instead of ./x).
  • Alternatively, a robust package.json exports map can present stable entry points so consumers aren’t relying on internal relative paths.

Proposed Fix (either approach works)

  1. Add .js extensions to all relative ESM imports in the published files (and in TS sources when using "moduleResolution":"NodeNext"; TypeScript will emit the correct .js in dist):
- import { Foo } from "./types";
+ import { Foo } from "./types.js";
  1. Provide a robust exports map in package.json so consumers resolve only through defined entry points:
{
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs",
      "default": "./dist/index.js"
    }
  }
}

Adjust paths/filenames to match the actual build output; add subpath exports if you intend to support them.

Workarounds Tried

  • Custom resolvers/transpilation: possible but undesirable for consumers.
  • Forcing CommonJS: defeats the purpose of ESM in frameworks like Next.js.

I am here for you team!
Happy to open a PR adding either the .js extensions or an exports map—whichever you prefer. Thanks for considering!

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