did-btcr2-js

ADR 021: tsconfig Normalization, Project References, and CJS via tsup

Status: Accepted

Date: 2026-04-08

Branch / PR: chore/lib-tsconfig-node-types

Context

Prior to this decision, the monorepo’s TypeScript configuration had drifted significantly:

The accumulated drift made the build system fragile, the publish surface dishonest (CJS published but broken), and onboarding contributors difficult.

Decision

A monorepo-wide normalization landed in five commits on the chore/lib-tsconfig-node-types branch. Key choices:

1. Four shared base tsconfig files at the repo root

tsconfig.base.json          : shared compiler defaults
tsconfig.base.cjs.json      : CJS overrides (module: CommonJS, Node10 resolution)
tsconfig.base.tests.json    : test overrides (types: node/mocha/chai, verbatim relaxed)
tsconfig.base.lib.json      : lib script editor-only typecheck (noEmit, types: node)

Every per-package config extends one of these bases. No per-package config copies compiler options.

2. Project references with composite: true and tsBuildInfoFile

Each package’s tsconfig.json declares composite: true and lists its workspace dependencies in a references array. The root tsconfig.json is a solution file (files: [] + references: [ ... 9 packages ]). Running tsc -b from the root walks the dependency graph and rebuilds only the changed packages.

This was exposed via four root scripts: build:ts, build:ts:watch, build:ts:clean, build:ts:force.

3. Strict compiler flags

Enabled in the base config:

verbatimModuleSyntax is the largest cascade: it forced ~380 source files to split mixed import { Type, value } statements into separate import type and import statements. This was applied via the @typescript-eslint/consistent-type-imports ESLint rule with the separate-type-imports autofix.

4. Explicit lib and types

The root sets lib: ["ES2022", "DOM", "DOM.Iterable"] and types: []. Browser-compatible packages inherit the defaults. Node-only packages (bitcoin, kms, cli) override with lib: ["ES2022"] + types: ["node"] to exclude DOM globals.

This stops @types/* packages from auto-leaking into builds and makes each package’s runtime contract explicit.

5. CJS via tsup for the four ESM-only-dep packages

cryptosuite, method, api, and cli cannot produce working CJS via plain tsc because their transitive dependencies (multiformats subpath exports, helia) only define import conditions in their package.json exports fields. Plain require() of these packages fails at runtime.

The fix: introduce tsup as a devDependency in those four packages and use it to bundle the ESM-only dependencies inline into the CJS output. tsup is invoked via a per-package tsup.config.ts and a build:cjs script that replaces the tsc -p tsconfig.cjs.json invocation. The other five packages (common, keypair, bitcoin, kms, smt) continue to use tsc for CJS: their dep graphs are CJS-compatible without bundling.

A subtlety: helia and @helia/strings use native modules (libp2p to node-datachannel) that can’t be statically bundled. To work around this, method/src/utils/appendix.ts was refactored to lazy-load them via dynamic import() so tsup doesn’t pull the native deps into the bundle. Node 22+ supports await import(esm) from CJS contexts, so the lazy load works at runtime in both the ESM and CJS builds.

6. Two orphan tsconfigs deleted

api/lib/tsconfig.json and smt/lib/tsconfig.json pointed at directories with zero .ts files. Deleted.

Consequences

Positive:

Negative:

Alternatives considered

Verification

References