Advanced Publishing Topics
This page covers patterns used by mature libraries and OSS projects for reliable, modern package publishing.
Dual CJS/ESM Publishingโ
Modern packages should support both CommonJS (CJS) and ES Modules (ESM) consumers using the exports field:
{
"name": "my-library",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./utils": {
"types": "./dist/utils.d.ts",
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
}
}
}
Key rules for the exports fieldโ
- Order matters โ Node.js uses the first matching condition. Put
typesfirst. "."is the main entry โ replaces themainfield for modern resolvers.- Subpath exports control what consumers can import โ anything not listed is inaccessible.
Conditional exportsโ
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"browser": "./dist/index.browser.mjs",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"default": "./dist/index.mjs"
}
}
}
TypeScript Declarationsโ
Bundled declarations (recommended)โ
Include .d.ts files directly in your package:
{
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs"
}
}
}
Separate @types packageโ
For packages not written in TypeScript, publish types to @types/your-package via DefinitelyTyped.
TypeScript 5.0+ exports resolutionโ
Set "moduleResolution": "bundler" or "node16" in consumer tsconfig.json to enable exports-based type resolution.
Build Tools for Librariesโ
| Tool | Approach | Strengths |
|---|---|---|
| tsup | Zero-config, esbuild-based | Fast, handles CJS/ESM/DTS in one command |
| unbuild | Rollup-based, auto-infers config | Great for monorepos, passive builds |
| Rollup | Manual config, plugin ecosystem | Maximum control |
| esbuild | Direct usage | Fastest builds, less DTS support |
tsup exampleโ
npm install -D tsup
{
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts"
}
}
unbuild exampleโ
{
"scripts": {
"build": "unbuild"
},
"build": {
"entries": ["src/index"],
"declaration": true,
"rollup": {
"emitCJS": true
}
}
}
Provenance and Supply Chain Securityโ
npm provenance links a published package to its source repository and build process, providing verifiable proof of origin.
Enabling provenanceโ
npm publish --provenance
This works automatically in supported CI environments (GitHub Actions, GitLab CI). It:
GitHub Actions workflow with provenanceโ
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for provenance
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Provenance requires the id-token: write permission in GitHub Actions. Without it, the publish will succeed but without provenance attestation.
Verifying provenanceโ
Consumers can verify provenance on npmjs.com (look for the green "Provenance" badge) or via:
npm audit signatures
Real-World OSS Examplesโ
Viteโ
Vite uses unbuild with a sophisticated exports map supporting multiple subpath entries:
{
"exports": {
".": {
"types": "./dist/node/index.d.ts",
"import": "./dist/node/index.js"
},
"./client": {
"types": "./client.d.ts"
}
}
}
TanStack Queryโ
TanStack packages use a per-framework export structure:
{
"exports": {
".": {
"import": {
"types": "./build/modern/index.d.ts",
"default": "./build/modern/index.js"
},
"require": {
"types": "./build/modern/index.d.cts",
"default": "./build/modern/index.cjs"
}
}
}
}
tRPCโ
tRPC demonstrates monorepo publishing with multiple packages that have interdependencies, using a workspace protocol and Changesets for coordinated releases.
These projects are good references for structuring your own package exports, build pipelines, and release workflows.