Skip to content

build!: ship ESM-only output for Node.js ^20.19.0 || >=22.12.0 - #97

Merged
donskov merged 1 commit into
masterfrom
donskov/webcrypto-4-build
Oct 27, 2026
Merged

donskov merged 1 commit into
masterfrom
donskov/webcrypto-4-build

Conversation

@donskov

@donskov donskov commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #87: the package is now "type": "module" with a single ESM entry (build/index.js), so the import condition no longer points at ESM syntax in a CommonJS-typed .js file. Verified with tsx@4.23.1 from the report.

Stacked on #96 (part 4 of 7 for @peculiar/webcrypto 2.0.0; merge in order).

Packaging only.

  • rollup + @rollup/plugin-typescript → tsdown (webcrypto-core's config + platform: "node", SPDX banner kept). Generated declarations replace the hand-written index.d.ts. tslib, rimraf, react-native field and the clear/rebuild scripts are gone.
  • tsconfig = pvtsutils + allowed deviations: lib: ["es2023", "DOM"] (typecheck fails without DOM: KeyUsage, JsonWebKey, …), types: ["node"], experimentalDecorators (@JsonProp in src/keys, src/mechs/{aes,des,hmac}). No stripInternal.
  • engines >=14.18.0 → ^20.19.0 || >=22.12.0, "type": "module", "sideEffects": false, publishConfig.access: public, files build/, README.md, LICENSE.md.
  • Type-only imports (7 lines). Dev dependencies updated (vitest 5.0.5, oxlint 1.90, oxfmt 0.75, tsdown 0.23.2, attw); TypeScript stays on 6.
  • @types/node stays on 25 (rule 1 exception). 26 drops the crypto.RsaPublicKey/RsaPrivateKey option types the RSA code uses, and 24 and lower reject the DOM JsonWebKey in createPublicKey({ format: "jwk" }). On 25 the source compiles unchanged.
  • Type fix: with plain generated types, crypto.subtle would be core's SubtleCrypto, which only accepts AlgorithmIdentifier. Code that compiles against 1.7.1 (sign({ name: "ECDSA", hash }), encrypt({ name: "AES-GCM", iv }), generateKey(..., usages as const)) would fail. Crypto.subtle is now GenerateKeyPair & SubtleCrypto & globalThis.SubtleCrypto; src/index.spec.ts compiles these calls in npm run typecheck.

Public types vs npm 1.7.1 index.d.ts

1.7.1 (hand-written) 2.0.0 (generated)
class Crypto implements globalThis.Crypto class Crypto extends core.Crypto (which implements globalThis.Crypto)
subtle: SubtleCrypto (DOM) subtle: GenerateKeyPair & SubtleCrypto & globalThis.SubtleCrypto; adds providers and core's overloads
getRandomValues<T extends ArrayBufferView | null>(array: T): T same
randomUUID(): \${string}-…`` same (inherited)
class CryptoKey implements globalThis.CryptoKey (algorithm, extractable, type, usages) webcrypto-core's CryptoKey (same members, plus static create/isKeyType)

Same two runtime exports. Consumers still need the DOM lib (as with 1.7.1). Consumer call shapes, 1.7.1 vs 2.0.0: all compile in both.

Verification (packed tarball of the full stack, installed with its peers)

Workload: ECDSA P-256 sign/verify, JWK and PKCS8/SPKI round trips, AES-GCM, RSA-PSS, Ed25519, and pkijs 4 setEngine/CryptoEngine with a self-signed certificate sign/parse/verify().

Node.js require() import
20.19.0 ok ok
22.11.0 ERR_REQUIRE_ESM (expected) ok
22.12.0 ok (ExperimentalWarning) ok
24.15.0 ok ok

Dual-package probe:

Check npm 1.7.1 2.0.0
require(x).Crypto === import(x).Crypto fail pass
require(x).CryptoKey === import(x).CryptoKey fail pass
webcrypto CryptoKey === core CryptoKey (require / import) pass / pass pass / pass
key from require instanceof import / core CryptoKey fail / fail pass / pass
import Crypto signs / exports JWK with a require key fail / fail pass / pass

sideEffects: false (rolldown 1.2.15, esbuild 0.28.2, webpack 5.112.2, target node):

Entry false true
Full workload (incl. AES/HMAC JWK, which needs the @JsonProp registrations) runs: 170.5 / 173.0 / 172.1 KB runs: 170.5 / 173.0 / 172.0 KB
CryptoKey only 0.3 KB or less 106–113 KB

TypeScript 6 consumers (skipLibCheck: false, no experimentalDecorators, lib DOM):

Setting Result
nodenext, node20 (.mts and .cts) pass
preserve + bundler pass
node16 .mts pass, .cts TS1479 (documented)
commonjs + node10 needs ignoreDeprecations: "6.0"; then passes with skipLibCheck. With skipLibCheck: false, 2 × TS2307 from webcrypto-core 2's own .d.ts (@peculiar/utils/bytes subpath); core alone reproduces it

release-please dry run on the full stack: finds v1.7.1, proposes chore(release): v2.0.0 with both BREAKING CHANGE notes.

BEGIN_COMMIT_OVERRIDE
build!: ship ESM-only output for Node.js ^20.19.0 || >=22.12.0

BREAKING CHANGE: @peculiar/webcrypto is ESM-only and requires Node.js ^20.19.0 || >=22.12.0 (was >=14.18.0). The bundles build/webcrypto.js (CommonJS) and build/webcrypto.es.js and the hand-written index.d.ts are replaced by unbundled ES modules with build/index.js and build/index.d.ts as the entry point; the react-native field is removed. Deep imports into build/ stay blocked by exports. CommonJS code can still require("@peculiar/webcrypto") on supported Node.js versions through require(esm); Node.js 22.0-22.11 can't. require() and import now load the same module, so CryptoKey instanceof checks and keys work across both. TypeScript projects that compile to CommonJS need "module": "node20" or "nodenext". The exported names (Crypto, CryptoKey) are unchanged; their declarations are now generated, so Crypto shows its webcrypto-core base class. See "Breaking Changes In v2" in the README.
END_COMMIT_OVERRIDE

Replace rollup and @rollup/plugin-typescript with tsdown, using the
pvtsutils/webcrypto-core config with platform "node" (the mechanisms
use node:crypto) and the SPDX banner. Types are generated from the
source, so the hand-written index.d.ts is gone. tsconfig follows
pvtsutils with lib DOM, types ["node"] and experimentalDecorators
(@JsonProp in src). Type-only imports for verbatimModuleSyntax, tslib
dropped, dev dependencies updated (TypeScript stays on 6).

Crypto.subtle is typed with the DOM SubtleCrypto overloads on top of
core's type, so consumer code written against the old index.d.ts
(EcdsaParams/AesGcmParams literals, `as const` usages) still compiles.
src/index.spec.ts covers that. @types/node stays on 25: 26 drops the
crypto.RsaPublicKey/RsaPrivateKey option types the RSA code uses.

BREAKING CHANGE: @peculiar/webcrypto is ESM-only and requires Node.js
^20.19.0 || >=22.12.0 (was >=14.18.0). The bundles
build/webcrypto.js (CommonJS) and build/webcrypto.es.js and the
hand-written index.d.ts are replaced by unbundled ES modules with
build/index.js and build/index.d.ts as the entry point; the
react-native field is removed. Deep imports into build/ are blocked by
exports. CommonJS code can still require("@peculiar/webcrypto") on
supported Node.js versions through require(esm); Node.js 22.0-22.11
can't. TypeScript projects that compile to CommonJS need "module":
"node20" or "nodenext". The exported names (Crypto, CryptoKey) are
unchanged; their declarations are now generated, so Crypto shows its
webcrypto-core base class. See "Breaking Changes In v2" in the README.
@donskov
donskov force-pushed the donskov/webcrypto-4-build branch from 7a1d61c to 1f210fb Compare October 27, 2026 11:07
@donskov donskov self-assigned this Oct 27, 2026
@donskov
donskov merged commit c82e321 into master Oct 27, 2026
2 checks passed
@donskov
donskov deleted the donskov/webcrypto-4-build branch October 27, 2026 11:09
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.7.1: exports map "import" condition points at ESM-syntax .js in a CJS package — named ESM imports break under strict resolvers

1 participant