feat!: convert the package to ESM only - #323
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
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:
|
0504ee1 to
a2d5883
Compare
a7b0926 to
a2d5883
Compare
a2d5883 to
0067215
Compare
0067215 to
f80e63f
Compare
f80e63f to
5d24967
Compare
5d24967 to
483dd65
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
483dd65 to
73194e8
Compare
7742fac to
ccfbfb6
Compare
| - **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. |
There was a problem hiding this comment.
This is not accurate. Modern versions of Node.js support require(esm).
There was a problem hiding this comment.
Yes thats right that was thrown in 18 which we already dropped, fixing
4b45e57 to
88435d0
Compare
4b00bd6 to
af16832
Compare
|
@SocketSecurity ignore npm/rimraf@5.0.10 |
ab4c590 to
cdb4ff1
Compare
cdb4ff1 to
a26e119
Compare
a26e119 to
1e9f477
Compare
1e9f477 to
570f74a
Compare
570f74a to
ff2463b
Compare
ff2463b to
5f7a410
Compare
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.
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 -->
5f7a410 to
7c27d33
Compare

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 withERR_REQUIRE_ESM.mainandmoduleare gone, and both.and./noderesolve throughexportsto a single./dist/*.jswith./dist/*.d.tstypes. Consumers already usingimportare unaffected.ts-bridgetsc.cjs+.mjs+.d.cts+.d.mts.js+.d.ts@ts-bridge/clirimrafadded for cleaning)ts-bridgeexists to emit both formats, so it goes with the CJS half.Import specifiers
102 relative specifiers across 45 files gained explicit
.jsextensions, which ESM requires; directories resolve to/index.js. Core's sources already look like this and enforce it withn/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 tolodash/memoize.js, a default import of the single method. Core hit the same wall and solved it withlodash-esplus a jestmoduleNameMapper; 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/hashesandpony-causeall have lexer friendly CJS. Verified by importing all 27 built modules individually, plus the exports map by bare specifier.Tooling
jest.config.jsand.prettierrc.jsare renamed to.cjs, since"type": "module"makes bare.jsfiles ESM. Tests still compile to CommonJS via a ts-jesttransformoverride (matching core) with amoduleNameMapperstripping the.jsspecifiers back off.constraints.prois 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/ dualimport/requireexports are removed;.and./noderesolve to single./dist/*.js+./dist/*.d.ts. Plainrequire('@metamask/utils')fails withERR_REQUIRE_ESMunless consumers use Node 22+require(esm)or dynamic import (noted inCHANGELOG.md).Build & publish: Output moves from
ts-bridgedual.cjs/.mjsartifacts totsc-emitted.js(seetsconfig.build.json— declarations are emitted with JS, not declaration-only).@ts-bridge/*is dropped;rimrafandbuild:cleanhandle dist cleanup. Yarnconstraints.proenforces the new export shape.Sources: Relative imports across
src/use explicit.jsspecifiers (ESLintn/file-extension-in-importaligned with core). Runtime fixes for ESM:hex.tsuseslodash/memoize.js(default import) instead of{ memoize } from 'lodash';mnemonic.tsadds.json the deep@metamask/scure-bip39wordlist path.Tooling: Jest stays on CommonJS for tests via
ts-jestoverride and amoduleNameMapperthat strips.jsfrom relative paths; config stays.cjsunder"type": "module".Reviewed by Cursor Bugbot for commit 7c27d33. Bugbot is set up for automated code reviews on this repo. Configure here.