Status: Accepted
Date: 2026-04-08
Branch / PR: chore/lib-tsconfig-node-types
Prior to this decision, the monorepo’s TypeScript configuration had drifted significantly:
types array to every @types/* package leaked into every buildlib array to DOM globals were silently available in node-only packages (bitcoin, kms, cli)tsconfig.cjs.json) inherited module: NodeNext from the root and produced ESM-syntax output that was tricked into being treated as CJS via a post-hoc dist/cjs/package.json override: but four packages (cryptosuite, method, api, cli) had this CJS output silently broken at runtime because their transitive dependencies (multiformats subpath exports, helia) are ESM-only and could not be require()‘dtsc ran 9 times from scratch on every build.ts files (api/lib/, smt/lib/)The accumulated drift made the build system fragile, the publish surface dishonest (CJS published but broken), and onboarding contributors difficult.
A monorepo-wide normalization landed in five commits on the chore/lib-tsconfig-node-types branch. Key choices:
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.
composite: true and tsBuildInfoFileEach 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.
Enabled in the base config:
noImplicitOverride: truenoFallthroughCasesInSwitch: trueforceConsistentCasingInFileNames: trueisolatedModules: trueverbatimModuleSyntax: truemoduleDetection: "force"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.
lib and typesThe 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.
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.
api/lib/tsconfig.json and smt/lib/tsconfig.json pointed at directories with zero .ts files. Deleted.
Positive:
require(). Previously 5/9 worked, 4/9 were broken.verbatimModuleSyntax) and lint time (consistent-type-imports).@types/* leakage is fixed: published .d.ts files no longer carry test-framework types.Negative:
import type cascade.tsup is now a build-time dependency in 4 packages (small impact: tsup itself has few transitive deps)..js (not .cjs), which still requires the dist/cjs/package.json {"type": "commonjs"} override hack to tell Node how to interpret the files. A future cleanup could migrate all 9 packages to tsup, switch the extension to .cjs, and drop the override entirely.appendix.ts fetchFromCas() now does dynamic import() instead of static: slightly more cognitive overhead at the call site, but it’s the only place this pattern is needed and the comment block explains why.require export condition), even though those consumers could never have used CJS successfully anyway. The tsup approach restores the ability to claim dual ESM/CJS publishing without breaking shape.did:btcr2 is a reference implementation that needs to behave identically to consumers’ Node runtime, (b) several native deps in the chain may not work cleanly under Bun, (c) the migration cost outweighs the benefit for a published library.typedoc-vitepress-theme for docs. Considered. Rejected because the docs site was being rebuilt anyway as a contributor-only resource (see ADR 016, forthcoming) and the simpler TypeDoc + projectDocuments approach is a better fit.pnpm build: all 9 packages build cleanlypnpm build:ts: incremental tsc build via project references is cleanpnpm build:tests && pnpm test: 810 tests passingpnpm lint: zero warnings monorepo-widerequire() and import() both work for all 9 packagesbtcr2 --version returns the correct versiondocs/contributing/build-system.md: full build pipeline documentationverbatimModuleSyntax TypeScript option