Skip to content

Support both ESM and CommonJS #64

Description

@mapitman

Ship the package as both ESM and CommonJS, so import and require are each first-class.

Why

Every source file is CommonJS today: require(...) and module.exports. The README, however, documents import api from "gettyimages-api" in all of its examples. Those examples work through Node's interop, so ESM consumers are not blocked. They are not getting real ESM either.

The goal is dual output rather than a pure ESM conversion. A pure conversion would break existing CommonJS consumers, for two reasons:

  • On Node before 22.12, require("gettyimages-api") throws ERR_REQUIRE_ESM.
  • On every Node version, the export shape changes. The entry point currently uses module.exports = GettyImagesApi, so require(...) returns the class directly. Under ESM, require(...) returns the module namespace object, and callers need .default.

The second point breaks working code silently. Dual output avoids both problems, so nobody has to rewrite an integration.

Approach

Author the source in one format and generate the other. TypeScript is already a devDependency for the declaration work, therefore tsc is the obvious tool.

Add an exports map to package.json:

{
  "exports": {
    ".": {
      "types": "./gettyimages-api.d.ts",
      "import": "./dist/esm/gettyimages-api.js",
      "require": "./dist/cjs/gettyimages-api.js"
    }
  },
  "main": "./dist/cjs/gettyimages-api.js",
  "types": "./gettyimages-api.d.ts"
}

require("gettyimages-api") then keeps returning the class directly, and import gets real ESM.

Scope

  • 18 files under lib/, plus the gettyimages-api.js entry point.
  • Roughly 20 test files, all of which use require.
  • lib/webhelper.js reads require("../package.json") for the user-agent string. Under ESM this needs an import attribute or a createRequire shim.
  • The package currently ships source directly from the repository root. Moving to a dist/ layout means files in package.json and the publish flow both need updating. The build step itself comes from Convert the source to TypeScript #85.
  • Add a test that loads the package both ways, to catch a regression in either entry point.

Sequencing

Do this last in the milestone, after #85 and #65.

  • Convert the source to TypeScript #85 (TypeScript conversion) introduces the build step this issue needs. Once tsc is building the package, dual output is mostly a tsconfig output setting plus the exports map shown above. Doing this issue first means adding a build step purely to transform module syntax, then reworking it.
  • Expose execute() as async #65 (async execute()) changes the public surface. The API shape should settle before the package is built two ways.

Notes

  • Known hazard: if a consumer's dependency graph loads both the ESM and CJS copies, instanceof checks across the two fail. The risk is low here, because the exported class is instantiated by consumers rather than passed between libraries, and there is no module-level shared state.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions