Skip to content

Convert codebase from CommonJS to ESM #134

@antonym

Description

@antonym

Background

Several dependencies have moved to ESM-only (isbinaryfile, readdirp v5). Currently we work around this with manual mock factories and pinned versions. A full ESM conversion would unblock these upgrades and align with the Node.js ecosystem direction.

Scope

Source files (low effort)

File Changes needed
package.json Add "type": "module"
app.js Convert 16 require()import, add __dirname polyfill (3 usages), handle JSON import for package.json, restructure chained patterns like require('socket.io')(http)
lib/utils.js Move 2 lazy requires to top-level imports, module.exports → named export

Test files (high effort)

File Difficulty Notes
jest.config.js Trivial Rename to .cjs
basic.test.js Easy 1 require, no mocks
lib-utils.test.js Easy 1 require, no mocks
functional.test.js Easy 3 requires, no mocks
tests/setup.js Medium require.cache/require.resolve has no ESM equivalent — needs restructuring
app.test.js Hard 6 jest.mock() calls → jest.unstable_mockModule() + async restructuring
utils.test.js Hard 3 jest.mock() calls → same
routes-simple.test.js Hard 5 jest.mock() calls + 1 __dirname usage

Totals

Category Count Effort
Source require() conversions 18 Low — mechanical
__dirname polyfills 4 Trivial
module.exportsexport 2 Trivial
JSON import (package.json) 1 Trivial
jest.mock() rewrites 14 across 3 files High — each needs manual restructuring
Docker/build changes 0 None

Key considerations

  • jest.mock() is the main pain point. It's a CJS-only synchronous hoisting trick. In ESM, it becomes jest.unstable_mockModule() which is async and still marked unstable in Jest 30.
  • Alternative approach: Keep test files as .cjs (rename .test.js.test.cjs) while converting only source code to ESM. This sidesteps the jest.mock problem entirely and tests keep working as-is.
  • Dependency upgrades unblocked: readdirp v5 (ESM-only, also removes .promise() API — one call site at app.js:186 needs async iteration), isbinaryfile (ESM-only), and optionally node-fetch v3 (or drop it for Node.js built-in fetch).
  • No Dockerfile or shell script changes needed.

Suggested approach

  1. Add "type": "module" to package.json
  2. Convert lib/utils.js (simplest source file)
  3. Convert app.js (add __dirname polyfill, convert imports, handle JSON import)
  4. Rename jest.config.jsjest.config.cjs
  5. Rename test files to .test.cjs to preserve existing mock patterns (or convert to jest.unstable_mockModule() if it has stabilized by then)
  6. Convert tests/setup.js — replace require.cache pattern
  7. Upgrade blocked dependencies: readdirp v5, isbinaryfile latest
  8. Consider dropping node-fetch for built-in fetch (Node.js 18+)

Related PRs

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