This page is also available in Italiano.

Continua in italiano

Preparing TinyDI for npm: package.json, the exports map, and tree-shaking

Published on July 24, 2026

Writing the core library is one problem. Shipping it as a package other projects can npm install and import from either ESM or CJS, with correct types on both sides, is a different one. This post is about the actual mechanics: switching bundlers, the exports map, and one bug that came from the tooling, not from us.

From plain tsc to tsup

The build moved from a straight per-file tsc emit to tsup, producing a genuine dual ESM+CJS package: dist/index.js (ESM), dist/index.cjs (CJS), and matching dist/index.d.ts/dist/index.d.cts declaration files. A plain tsc emit gives you one module format; a real dual package needs the build tool to bundle and emit both, with separate type declarations, since ESM and CJS type resolution aren't quite the same thing.

The exports map: two conditions, each with its own types

json
{
  "exports": {
    ".": {
      "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
      "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
    }
  }
}

Each condition — import for ESM consumers, require for CJS ones — carries its own types entry, not a shared one. Get this wrong (e.g. one shared .d.ts for both) and a CJS consumer can end up with type errors from ESM-specific type resolution, or vice versa, in a way that only shows up in their build, not in ours.

Zero dependencies, one peer dependency

package.json has no dependencies field at all — only devDependencies (tooling) and an optional peerDependencies.typescript: ">=6.0.0". That's not an incidental detail; it's the whole point of the library. Anyone auditing what they're pulling in by adding TinyDI to a project gets a real answer: nothing.

A bug that was the tooling's, not ours

tsup's declaration-bundling step synthesizes an internal baseUrl option, which TypeScript 6.0 hard-errors on as TS5101: Option 'baseUrl' is deprecated. This has nothing to do with anything in this project's own tsconfig.json — it's an interaction between tsup/rollup-plugin-dts internals and TypeScript 6's stricter deprecation policy. The fix is one line, added to tsconfig.build.json specifically (not the base tsconfig.json used for editor/lint/type-check):

json
{
  "compilerOptions": {
    "ignoreDeprecations": "6.0"
  }
}

Why this matters beyond the one-line fix

Recognizing that an error comes from the build tool's internals rather than your own configuration changes how you fix it: the alternative would have been chasing a nonexistent baseUrl setting in our own tsconfig.json files, where it was never the actual problem.

What this bought, concretely

The result is a package that tree-shakes cleanly (ESM output, no side-effecting module-level code) and works the same whether a consumer's bundler picks the import or require condition — verified in practice by the seven examples in this project, which cover both a Vite-based frontend toolchain and plain Node tsx scripts, all resolving the same package correctly.

Getting the package itself right turned out to be necessary but not sufficient. Actually publishing it — the point where a package.json meets npm's real infrastructure — surfaced a separate set of problems none of this predicted, covered in the release post.

to navigate to select Esc to close