Skip to content

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

Open
cryptodev-2s wants to merge 2 commits into
migrate/drop-node-18-20from
migrate/pr2i-esm-only
Open

feat!: convert the package to ESM only#323
cryptodev-2s wants to merge 2 commits into
migrate/drop-node-18-20from
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/:

  • lodashimport { 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
Major breaking changes for all consumers on CommonJS/require or Node 18/20, plus a new published module format and build pipeline that must resolve correctly at runtime.

Overview
This PR makes @metamask/utils ESM-only and aligns it with MetaMask core: require('@metamask/utils') fails; main / module and dual CJS+ESM builds are removed in favor of a single exports-based ./dist/*.js + ./dist/*.d.ts layout for . and ./node.

Build and packaging: ts-bridge is replaced by tsc, with rimraf for clean builds. "type": "module" is set; constraints.pro enforces the new export shape and engines.node: ^22.14.0 || ^24. CI matrices drop Node 18/20 and test on 22.x and 24.x.

Source and runtime fixes for ESM: Relative imports across src/ use explicit .js specifiers; ESLint adds n/file-extension-in-import (matching core). Jest keeps running tests as CJS via moduleNameMapper and a ts-jest CommonJS override. Runtime import fixes include lodash/memoize.js (default import) and an explicit .js on the scure-bip39 wordlist deep import.

The changelog also records breaking Node support removal and dependency bumps already noted in the stack (e.g. @ethereumjs/tx v5).

Reviewed by Cursor Bugbot for commit 0067215. 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
Updated@​types/​node@​18.18.14 ⏵ 22.20.1100 +121008196100
Addedrimraf@​5.0.109910010083100

View full report

@socket-security

socket-security Bot commented Sep 4, 2026

Copy link
Copy Markdown

Caution

MetaMask internal reviewing guidelines:

  • Do not ignore-all
  • Each alert has instructions on how to review if you don't know what it means. If lost, ask your Security Liaison or the supply-chain group
  • Copy-paste ignore lines for specific packages or a group of one kind with a note on what research you did to deem it safe.
    @SocketSecurity ignore npm/PACKAGE@VERSION
Action Severity Alert  (click "▶" to expand/collapse)
Block High
Obfuscated code: npm rimraf is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package.jsonnpm/rimraf@5.0.10

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/rimraf@5.0.10. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Low
Environment variable access: npm rimraf reads __TESTING_RIMRAF_PLATFORM__

Env Vars: TESTING_RIMRAF_PLATFORM

Location: Package overview

From: package.jsonnpm/rimraf@5.0.10

ℹ Read more on: This package | This alert | What is environment variable access?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should be clear about which environment variables they access, and care should be taken to ensure they only access environment variables they claim to.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/rimraf@5.0.10. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Low
Environment variable access: npm rimraf

Env Vars: TESTING_RIMRAF_NODE_VERSION

Location: Package overview

From: package.jsonnpm/rimraf@5.0.10

ℹ Read more on: This package | This alert | What is environment variable access?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should be clear about which environment variables they access, and care should be taken to ensure they only access environment variables they claim to.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/rimraf@5.0.10. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

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
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
cryptodev-2s changed the base branch from migrate/pr2h-fix-tsd to migrate/drop-node-18-20 September 8, 2026 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant