Skip to main content

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โ€‹

  1. Order matters โ€” Node.js uses the first matching condition. Put types first.
  2. "." is the main entry โ€” replaces the main field for modern resolvers.
  3. 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โ€‹

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โ€‹

ToolApproachStrengths
tsupZero-config, esbuild-basedFast, handles CJS/ESM/DTS in one command
unbuildRollup-based, auto-infers configGreat for monorepos, passive builds
RollupManual config, plugin ecosystemMaximum control
esbuildDirect usageFastest 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:

  1. Generates a SLSA provenance statement
  2. Signs it with Sigstore
  3. Attaches it to the published package

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 }}
warning

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.