Skip to content

Migrate package to ESM (dual build), following apex-parser #95

Description

@nawforce

Problem

benchmarker is published as CommonJS, and a growing part of its dependency tree has gone ESM-only. This is no longer a per-dependency annoyance — it is now blocking security and version updates outright.

Currently blocked Dependabot PRs, both failing for the same underlying reason:

  • chore(deps-dev): bump the chai-stack group across 1 directory with 5 updates #89 — chai stack. The locked versions all declare "type": "module": chai 6.2.2, chai-as-promised 8.0.2, sinon-chai 4.0.1. npm run build fails with widespread TS1479 — CommonJS test files cannot statically import ESM-only packages. Compilation fails before any test runs.
  • chore(deps): bump puppeteer from 22.15.0 to 25.3.0 #91 — puppeteer 22 → 25. v23 and v24 are actually clean against this codebase. v25 is ESM-only, and additionally raises Puppeteer's minimum Node to 22 while this package declares "node": ">20.0.0". There is also an API rename to absorb: PuppeteerNodeLaunchOptions (imported by src/shared/env.ts) is now LaunchOptions.

Until this is resolved, both PRs are unmergeable and the repo stays pinned on older majors.

Current state

package.json    no "type" field  →  CommonJS
main            dist/src/index.js
types           dist/types/src/index.d.ts
exports         "." and "./database", single "default" condition, no import/require split
engines         node >20.0.0
tsconfig        module: node16, target: es2020, outDir: ./dist
build           tsc only (no bundler)

Worth noting the setup is already partly hybrid and inconsistent:

  • npm test runs with NODE_OPTIONS='--loader ts-node/esm --experimental-specifier-resolution=node'
  • .mocharc.yml requires ts-node/register (the CommonJS loader)
  • npm run test:system uses neither

So the test path already has one foot in ESM while the package contract is CommonJS.

Prior art: apex-parser

apex-parser has already been through this and is the reference implementation. It went dual-build rather than ESM-only, so existing CommonJS consumers kept working.

Its npm/package.json now looks like:

{
  "type": "module",
  "main": "./dist/cjs/index.cjs",
  "module": "./dist/esm/index.js",
  "types": "./dist/types/index.d.ts",
  "exports": {
    ".": {
      "browser": { "types": "...", "default": "./dist/browser/apex-parser.mjs" },
      "import":  { "types": "./dist/types/index.d.ts",  "default": "./dist/esm/index.js" },
      "require": { "types": "./dist/types/index.d.cts", "default": "./dist/cjs/index.cjs" }
    }
  }
}

Dual output is produced by Rollup (npm/rollup.config.js) emitting both format: "es"dist/esm/*.js and format: "cjs"dist/cjs/*.cjs with preserveModules: true, plus separate .d.ts / .d.cts type outputs per condition.

Useful commits to follow:

  • ed23f8b — update inner npm package for ESM and exports
  • 802347a — update src to ES modules, fix strict type errors (PR [codex] Fix TypeScript 6 test runner #79)
  • f375e4f — add CJS and browser builds via Rollup
  • 06949ca — use dynamic import() in the check script for ESM compatibility (PR #106)

That last one is the pattern for reaching an ESM-only dependency from a CommonJS context, which is directly relevant to the Puppeteer situation.

Scope

  • Add "type": "module"; decide dual-build vs ESM-only (dual recommended, per apex-parser)
  • Introduce a bundler for dual output, or an equivalent two-pass tsc setup
  • Rework exports with proper import / require conditions and matching .d.ts / .d.cts
  • Review module / moduleResolution in tsconfig.json
  • Audit imports for explicit file extensions and CommonJS assumptions (__dirname, require, module.exports)
  • Reconcile the test setup: .mocharc.yml ts-node/register vs the ts-node/esm loader in npm test, plus nyc
  • Decide whether to raise engines.node to >=22 (required if Puppeteer 25 becomes a production dependency)
  • Check downstream consumers — this is a package-level breaking change to the published contract

Related

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