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.exports → export |
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
- Add
"type": "module" to package.json
- Convert
lib/utils.js (simplest source file)
- Convert
app.js (add __dirname polyfill, convert imports, handle JSON import)
- Rename
jest.config.js → jest.config.cjs
- Rename test files to
.test.cjs to preserve existing mock patterns (or convert to jest.unstable_mockModule() if it has stabilized by then)
- Convert
tests/setup.js — replace require.cache pattern
- Upgrade blocked dependencies:
readdirp v5, isbinaryfile latest
- Consider dropping
node-fetch for built-in fetch (Node.js 18+)
Related PRs
Background
Several dependencies have moved to ESM-only (
isbinaryfile,readdirpv5). 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)
package.json"type": "module"app.jsrequire()→import, add__dirnamepolyfill (3 usages), handle JSON import forpackage.json, restructure chained patterns likerequire('socket.io')(http)lib/utils.jsmodule.exports→ namedexportTest files (high effort)
jest.config.js.cjsbasic.test.jslib-utils.test.jsfunctional.test.jstests/setup.jsrequire.cache/require.resolvehas no ESM equivalent — needs restructuringapp.test.jsjest.mock()calls →jest.unstable_mockModule()+ async restructuringutils.test.jsjest.mock()calls → sameroutes-simple.test.jsjest.mock()calls + 1__dirnameusageTotals
require()conversions__dirnamepolyfillsmodule.exports→exportpackage.json)jest.mock()rewritesKey considerations
jest.mock()is the main pain point. It's a CJS-only synchronous hoisting trick. In ESM, it becomesjest.unstable_mockModule()which is async and still marked unstable in Jest 30..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.readdirpv5 (ESM-only, also removes.promise()API — one call site atapp.js:186needs async iteration),isbinaryfile(ESM-only), and optionallynode-fetchv3 (or drop it for Node.js built-infetch).Suggested approach
"type": "module"topackage.jsonlib/utils.js(simplest source file)app.js(add__dirnamepolyfill, convert imports, handle JSON import)jest.config.js→jest.config.cjs.test.cjsto preserve existing mock patterns (or convert tojest.unstable_mockModule()if it has stabilized by then)tests/setup.js— replacerequire.cachepatternreaddirpv5,isbinaryfilelatestnode-fetchfor built-infetch(Node.js 18+)Related PRs
readdirpv5 upgrade (blocked by ESM)isbinaryfileESM mock workarounds