ADR-001: Use ESM over CommonJS

Status: accepted
Deciders: Richard Attermeyer
Date: 2025-07-01

Context and Problem Statement

Node.js supports two module systems: CommonJS (CJS, require/module.exports) and ECMAScript Modules (ESM, import/export). The project needs to choose one as its primary module format. CJS is the legacy standard with the widest ecosystem compatibility. ESM is the forward-looking standard aligned with browser JavaScript and the ECMAScript specification. The decision affects tooling, dependency compatibility, and future maintenance burden.

Decision Drivers

  • Future-proofness: the choice should age well over the project’s lifespan

  • Antora compatibility: the extension is loaded by Antora’s require(), which expects CJS — ESM creates friction at the integration point

  • Ecosystem compatibility: npm packages used by the project (commander, chalk, mustache) must work

  • Developer experience: module resolution, tooling support, debugging

Considered Options

  • ESM with "type": "module" in package.json

  • CommonJS (CJS) as default

Decision Outcome

Chosen option: ESM with "type": "module", despite knowing it creates friction with Antora’s CJS extension loader. The project accepted the cost of .cjs wrappers as a one-time investment in exchange for long-term alignment with the ECMAScript standard and Node.js’s stated direction. CJS is in maintenance mode; ESM is where the ecosystem is heading.

Positive Consequences

  • Modern import/export syntax, better static analysis, tree-shaking support

  • Aligned with browser JavaScript — reduces mental model switching

  • node: protocol for built-in imports makes dependencies explicit

Negative Consequences

  • Antora loads extensions via require() which expects CJS. Every ESM .js file that Antora’s extension loader touches needs a .cjs wrapper — not only third-party packages. This is the primary integration friction: the project’s own extension entry point must be wrapped.

  • Third-party CJS packages (@antora/pdf-extension) require additional .cjs wrappers, compounding the issue.

  • __dirname and require not available without workarounds in ESM modules.

  • Stricter module resolution can surface edge cases in test runners (e.g., Mocha + ESM needed tsx workaround).

Pros and Cons of the Options

ESM

  • Good, because forward-looking — CJS is in maintenance mode

  • Good, because modern syntax aligned with browser JavaScript

  • Bad, because Antora’s extension loader uses require(), forcing .cjs wrappers for every entry point

  • Bad, because some third-party packages also need wrappers

CommonJS

  • Good, because seamless Antora integration — no wrapper files needed, require() works natively

  • Good, because maximum third-party package compatibility

  • Bad, because legacy technology — long-term maintenance risk as the Node.js ecosystem moves to ESM

  • Supersedes implicit CJS default in early prototypes