Skip to content

fix: ship .d.mts and .d.cts for dual ESM/CJS TypeScript consumers - #15

Merged
jwahdatehagh merged 3 commits into
mainfrom
dualfix
Sep 1, 2026
Merged

fix: ship .d.mts and .d.cts for dual ESM/CJS TypeScript consumers#15
jwahdatehagh merged 3 commits into
mainfrom
dualfix

Conversation

@caveman-eth

Copy link
Copy Markdown
Member

fix: ship .d.mts and .d.cts for dual ESM/CJS TypeScript consumers

Base: main. Patch release 4.2.1 for both packages.

Problem

Both packages set "type": "module" and publish dual .mjs / .cjs builds, but ship a single .d.ts that the import and require export conditions both point at. Under moduleResolution: "Node16", TypeScript reads that lone declaration as ESM-only, so any consumer emitting require() calls fails:

TS1479: The current file is a CommonJS module whose imports will produce 'require' calls;
however, the referenced file is an ECMAScript module and cannot be imported with 'require'.

Runtime require() of the .cjs build already works, and so do Jest suites that skip the CJS emit. This is purely a type declaration packaging gap, and any CJS plus Node16 consumer hits it.

Reported by MetaMask in MetaMask/core#8497, where it blocks their migration off the unmaintained SpruceID packages. The same failure reproduces on a clean main in MetaMask/core#10049, so it is not specific to that branch.

Fix

Emit .d.mts and .d.cts alongside the existing .d.ts, and point each export condition at the declaration whose module format matches the JavaScript it describes.

The rolled-up declaration is copied in vite-plugin-dts's afterBuild hook, which runs once, after the files are on disk. Copying verbatim is safe here: both public surfaces use named exports rather than export =, and neither declaration contains relative imports.

Both packages need the change. controller-utils and profile-sync-controller are separate consumer entry points, and siwe.d.cts itself imports @signinwithethereum/siwe-parser under the require condition, so fixing only one leaves the other broken.

Root "types" is unchanged as a node10 fallback.

Verification

Reproduced against published 4.2.0, then confirmed fixed, in the MetaMask/core repro:

Package Published 4.2.0 This PR
controller-utils TS1479 at src/siwe.ts:2:31 builds successfully
profile-sync-controller TS1479 at flow-siwe.ts:1:29 TS1479 gone

For profile-sync-controller the full error sets before and after are identical except that single TS1479. The other errors in that clone are pre-existing unbuilt project reference noise, unrelated to this change.

Also checked:

  • A CJS consumer under module: Node16 fails on both imports against published 4.2.0 and type checks cleanly against this build, so the test genuinely detects the bug.
  • An ESM consumer under the same settings still type checks, confirming no regression from the exports rewrite.
  • arethetypeswrong reports no problems across node10, node16 from CJS, node16 from ESM, and bundler for both packages. Run manually against the packed tarballs rather than wired into CI, to keep this change small.
  • A clean build emits .mjs, .cjs, .d.ts, .d.mts, and .d.cts for both packages on the first run.
  • Existing tests (424), lint, and format checks all pass. No test files changed.

Release notes

Patch 4.2.1. No API, parsing, or verification behaviour changes. MetaMask already depends on ^4.2.0, so they pick this up with a lockfile refresh and no manifest edit.

Copy whatever .d.ts vite-plugin-dts actually rolled up instead of
hardcoding the entry name, resolve dist relative to the config file
rather than process.cwd(), and fail the build explicitly if no
declaration was emitted. Keeps both package hooks identical and
survives an entry rename without silently shipping broken exports.
Add attw --pack for both packages as a root check:exports script and
run it in CI after the build, so an exports/declaration mismatch like
the TS1479 regression fails the pipeline instead of surfacing in
consumers. Verified the check fails when the require condition points
back at the ESM-flavoured .d.ts.
@jwahdatehagh

Copy link
Copy Markdown
Member

Reviewed and verified this end to end, then pushed two follow-up commits.

Verification, independent of the PR description:

  • Reproduced TS1479 on both imports with a module: Node16 CJS consumer against published 4.2.0; the same consumer type-checks and runs against tarballs packed from this branch. An ESM consumer is unaffected in both cases.
  • attw reports all green (node10, node16 from CJS, node16 from ESM, bundler) for both packed tarballs.
  • The rolled-up declarations are safe to copy verbatim: named exports only, no relative imports, no triple-slash references, and siwe.d.cts's import of @signinwithethereum/siwe-parser resolves through the require condition to parsers.d.cts.
  • vite-plugin-dts 4.5.4 runs afterBuild after the rollup output is written and intermediates are deleted, so the hook timing is correct, and the plugin is apply: 'build', so vitest never triggers it.
  • Release CI builds before changeset publish, so the new exports paths can never ship from a stale dist.

Pushed:

  • 02e2319 makes the copy hook derive from whatever .d.ts the plugin emitted instead of hardcoding the entry name, resolves dist from the config file location rather than process.cwd(), and fails the build explicitly if no declaration is present. Both package hooks are now identical and survive an entry rename.
  • 97a86fe wires attw --pack into CI as pnpm check:exports (this supersedes the "run manually, keep the change small" note above). Verified it fails with "Masquerading as ESM" when the require condition is pointed back at the lone .d.ts, and passes on this branch.

All 424 tests, lint, and format checks still pass after both commits.

@jwahdatehagh
jwahdatehagh merged commit ec3955b into main Sep 1, 2026
3 checks passed
@jwahdatehagh jwahdatehagh mentioned this pull request Sep 1, 2026
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.

2 participants