Skip to content
This repository was archived by the owner on Sep 11, 2026. It is now read-only.

feat!: convert the package to ESM only - #323

Merged
cryptodev-2s merged 4 commits into
mainfrom
migrate/pr2i-esm-only
Sep 10, 2026
Merged

cryptodev-2s merged 4 commits into
mainfrom
migrate/pr2i-esm-only

Conversation

@cryptodev-2s

@cryptodev-2s cryptodev-2s commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

Top of stack #315, on #322.

Core is moving to an ESM only monorepo (MetaMask/core#9536), so this package should arrive already converted rather than landing as the only hybrid one in it.

Breaking

require('@metamask/utils') now fails with ERR_REQUIRE_ESM. main and module are gone, and both . and ./node resolve through exports to a single ./dist/*.js with ./dist/*.d.ts types. Consumers already using import are unaffected.

Before After
build ts-bridge tsc
output .cjs + .mjs + .d.cts + .d.mts .js + .d.ts
@ts-bridge/cli yes removed (rimraf added for cleaning)

ts-bridge exists to emit both formats, so it goes with the CJS half.

Import specifiers

102 relative specifiers across 45 files gained explicit .js extensions, which ESM requires; directories resolve to /index.js. Core's sources already look like this and enforce it with n/file-extension-in-import, so the same three import rules are adopted here verbatim.

Two things only the built output revealed

Neither would have been caught by the test suite, because tests run against src/:

  • lodash — import { memoize } from 'lodash' throws at runtime under ESM: Node's lexer cannot see named exports through lodash's CJS. Switched to lodash/memoize.js, a default import of the single method. Core hit the same wall and solved it with lodash-es plus a jest moduleNameMapper; this approach needs neither.
  • @metamask/scure-bip39 — the deep wordlist import needed an explicit .js.

Every other CJS dependency survives named imports untouched. semver, @metamask/superstruct, @scure/base, @noble/hashes and pony-cause all have lexer friendly CJS. Verified by importing all 27 built modules individually, plus the exports map by bare specifier.

Tooling

jest.config.js and .prettierrc.js are renamed to .cjs, since "type": "module" makes bare .js files ESM. Tests still compile to CommonJS via a ts-jest transform override (matching core) with a moduleNameMapper stripping the .js specifiers back off. constraints.pro is rewritten for the single entrypoint shape.


Note

High Risk
Dropping CommonJS and changing the public export map is a breaking integration change for any consumer still on require() or dual-package resolution, despite unchanged utility APIs.

Overview
Breaking: The package is now ESM-only ("type": "module"). main / dual import/require exports are removed; . and ./node resolve to single ./dist/*.js + ./dist/*.d.ts. Plain require('@metamask/utils') fails with ERR_REQUIRE_ESM unless consumers use Node 22+ require(esm) or dynamic import (noted in CHANGELOG.md).

Build & publish: Output moves from ts-bridge dual .cjs/.mjs artifacts to tsc-emitted .js (see tsconfig.build.json — declarations are emitted with JS, not declaration-only). @ts-bridge/* is dropped; rimraf and build:clean handle dist cleanup. Yarn constraints.pro enforces the new export shape.

Sources: Relative imports across src/ use explicit .js specifiers (ESLint n/file-extension-in-import aligned with core). Runtime fixes for ESM: hex.ts uses lodash/memoize.js (default import) instead of { memoize } from 'lodash'; mnemonic.ts adds .js on the deep @metamask/scure-bip39 wordlist path.

Tooling: Jest stays on CommonJS for tests via ts-jest override and a moduleNameMapper that strips .js from relative paths; config stays .cjs under "type": "module".

Reviewed by Cursor Bugbot for commit 7c27d33. Bugbot is set up for automated code reviews on this repo. Configure here.

@socket-security

socket-security Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedrimraf@​5.0.109910010083100

View full report

@socket-security

socket-security Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

Ignoring alerts on:

  • rimraf@5.0.10

View full report

@cryptodev-2s
cryptodev-2s force-pushed the migrate/pr2i-esm-only branch 2 times, most recently from 0504ee1 to a2d5883 Compare September 7, 2026 12:31
@cryptodev-2s
cryptodev-2s force-pushed the migrate/pr2i-esm-only branch 2 times, most recently from a7b0926 to a2d5883 Compare September 8, 2026 17:00
@cryptodev-2s
cryptodev-2s changed the base branch from migrate/pr2h-fix-tsd to migrate/drop-node-18-20 September 8, 2026 18:52
@cryptodev-2s
cryptodev-2s removed this pull request from stack #329 September 9, 2026 11:46
@cryptodev-2s
cryptodev-2s added this pull request to stack #331 September 9, 2026 11:47

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 483dd65. Configure here.

Comment thread package.json
@cryptodev-2s
cryptodev-2s force-pushed the migrate/pr2i-esm-only branch 2 times, most recently from 7742fac to ccfbfb6 Compare September 9, 2026 19:23
Comment thread CHANGELOG.md Outdated
- **BREAKING:** Drop support for Node 18 and 20 ([#328](https://github.com/MetaMask/utils/pull/328))
- The supported range is now `^22.14.0 || ^24`, matching core.
- **BREAKING:** The package is now ESM only ([#323](https://github.com/MetaMask/utils/pull/323))
- The CommonJS build is gone. `require('@metamask/utils')` now fails with `ERR_REQUIRE_ESM`; use `import` instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not accurate. Modern versions of Node.js support require(esm).

@cryptodev-2s cryptodev-2s Sep 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes thats right that was thrown in 18 which we already dropped, fixing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reworded here 4b45e57

Comment thread CHANGELOG.md Outdated
@cryptodev-2s
cryptodev-2s requested a review from Mrtenz September 10, 2026 11:04
@cryptodev-2s

Copy link
Copy Markdown
Contributor Author

@SocketSecurity ignore npm/rimraf@5.0.10

Base automatically changed from migrate/drop-node-18-20 to main September 10, 2026 13:33
Core is moving to an ESM only monorepo (MetaMask/core#9536), so this package
should arrive already converted rather than landing as the sole hybrid one.

BREAKING: the CommonJS build is gone. require('@metamask/utils') now fails
with ERR_REQUIRE_ESM. `main` and `module` are removed, and both `.` and
`./node` resolve through `exports` to a single ./dist/*.js with ./dist/*.d.ts
types. Consumers already using `import` are unaffected.

  package.json    adds "type": "module", collapses the dual exports map
  build           ts-bridge -> tsc, since ts-bridge exists to emit both formats
  @ts-bridge/cli  removed, rimraf added for build:only-clean
  tsconfig.build  drops emitDeclarationOnly, tsc now emits the JS too

102 relative import specifiers across 45 files gained explicit .js
extensions, which ESM requires. Directories resolve to /index.js. Core's
sources already look like this and enforce it with n/file-extension-in-import,
so the same three import rules are adopted here verbatim.

Two things only surfaced by running the built output rather than the tests:

  lodash  `import { memoize } from 'lodash'` throws at runtime under ESM,
          because Node's lexer cannot see named exports through lodash's CJS.
          Switched to `lodash/memoize.js`, a default import of the single
          method. Core solved the same problem by moving to lodash-es plus a
          jest moduleNameMapper; this needs neither.

  scure-bip39  the deep wordlist import needed an explicit .js.

Every other CJS dependency survives named imports untouched: semver,
superstruct, @scure/base, @noble/hashes and pony-cause all have lexer
friendly CJS. Verified by importing all 27 built modules individually.

jest.config.js and .prettierrc.js are renamed to .cjs, since "type": "module"
makes bare .js ESM. Tests still compile to CommonJS through a ts-jest
transform override, matching core, with a moduleNameMapper stripping the .js
specifiers back off. constraints.pro is rewritten for the single entrypoint
shape.
test:source runs `jest && jest-it-up`, and jest-it-up defaults to looking
for jest.config.js, which is now jest.config.cjs. It supports --config, so
point it there.

Caught by CI rather than locally: I had been running `yarn jest` directly to
work around a broken watchman on this machine, which skipped jest-it-up
entirely, so test:source was never actually exercised.
cryptodev-2s added a commit that referenced this pull request Sep 10, 2026
Replaces #324, which GitHub auto-closed as merged during a stack reorder
when its head briefly became an ancestor of its base. The changes never
reached `main`; this carries the same two commits.

Mirrors
[MetaMask/core#9976](MetaMask/core#9976), the
bottom of core's foundational stack.

| | Before | After |
| --- | --- | --- |
| `engines.node` | `^18.18 \|\| ^20.14 \|\| >=22` | `^22.14.0 \|\| ^24`
|
| `@types/node` | `~18.18.14` | `^22.13.14` |
| CI matrix | 18, 20, 22 | 22, 24 |

`constraints.pro` is updated so `yarn constraints` enforces the new
range. Core makes the `@types/node` bump in this same PR rather than
with its TypeScript change, since the types track the supported runtime.

## Position in the stack

This now sits **below** the ESM conversion (#323), so everything up to
and including this PR is still a hybrid CJS/ESM build:

```
main → … → #322 → this → #323 (ESM only) → #325 (TypeScript)
```

That matters for testing. A preview build from here still resolves
through `main: ./dist/index.cjs` with the `require` condition intact, so
it can be consumed by `metamask-extension` as-is. Everything below the
ESM cut can therefore be verified against a real downstream consumer
before the breaking change lands.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Semver-breaking runtime requirement may block consumers still on Node
18 or 20; in-repo changes are mostly policy, CI, and test cleanup with
limited logic impact.
> 
> **Overview**
> **Breaking:** supported Node is now `^22.14.0 || ^24` instead of
18/20/22. That is enforced in `package.json`, `constraints.pro`, and
documented under Unreleased in `CHANGELOG.md`.
> 
> CI **prepare**, **test**, and **compatibility-test** jobs now run on
Node **22.x** and **24.x** only (18 and 20 dropped from the matrix). Dev
typings move from `@types/node` ~18 to **^22.13.14**, with matching
`yarn.lock` updates.
> 
> Test and lint tooling align with the new floor: ESLint comments for
`n/no-unsupported-features/node-builtins` reflect that global `crypto`
is expected on 22+, and `hashing.test.ts` drops the Node 18 `webcrypto`
polyfill/`beforeEach` setup—tests assume `globalThis.crypto.subtle`
exists.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ba81eca. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
@cryptodev-2s
cryptodev-2s merged commit 0c93a38 into main Sep 10, 2026
19 checks passed
@cryptodev-2s
cryptodev-2s deleted the migrate/pr2i-esm-only branch September 10, 2026 13:40
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants