Publishing an npm package in 2026 involves more decisions than npm publish and a version bump. The ecosystem has settled on dual ESM/CJS output, exports maps, automated versioning, and provenance attestation. Get these right once and the ongoing publishing workflow is nearly automatic.
What changed in 2026
- npm provenance is expected —
npm audit and security tooling now flag packages published without provenance. It is a single CI flag.
- Pure ESM is still not universal — enough tooling (Jest configs, CJS-only bundlers) still requires CJS output. Ship both unless your audience is exclusively modern bundlers.
exports map replaced main — the exports field in package.json controls exactly what consumers can import, preventing accidental deep imports.
- Changesets became the standard for monorepo and multi-package versioning; it replaced manual
npm version commands on most maintained projects.
Package structure
my-package/
├── src/
│ └── index.ts
├── dist/
│ ├── index.js # ESM
│ ├── index.cjs # CJS
│ └── index.d.ts # Types
├── package.json
├── tsconfig.json
└── tsup.config.ts
Build setup with tsup
tsup is the standard build tool for TypeScript libraries in 2026:
npm install --save-dev tsup typescript
// tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
sourcemap: true,
});
Build:
npx tsup
package.json exports map
{
"name": "@myorg/my-package",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsup",
"prepublishOnly": "npm run build"
}
}
The files array ensures only dist/ is published — not src/, test files, or config files.
Versioning with Changesets
npm install --save-dev @changesets/cli
npx changeset init
Workflow:
- When your PR adds a change, run
npx changeset and select the bump type (patch/minor/major) with a description.
- This creates a file in
.changeset/.
- Merge the PR. Run
npx changeset version to apply bumps and update CHANGELOG.md.
- Commit, tag, push, and publish.
The GitHub bot (changeset-bot) can automate steps 3–4 via a release PR.
GitHub Action for automated publishing
# .github/workflows/publish.yml
name: Publish
on:
push:
tags: ['v*']
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: '22'
registry-url: 'https://registry.npmjs.org'
- run: npm ci && npm run build
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
--provenance links the published package to the exact GitHub Actions run, commit SHA, and repository. Consumers can verify the package was not tampered with.
Checklist before first publish
| Item |
Command / check |
| Package name available |
npm view @myorg/my-package returns 404 |
Only dist/ in tarball |
npm pack --dry-run |
| Types resolve correctly |
npx arethetypeswrong . |
| No secrets in dist |
Review npm pack output |
README.md present |
Shown on npmjs.com |
| License set |
"license": "MIT" in package.json |
arethetypeswrong (attw) is a 2026-standard tool that verifies your exports map resolves types correctly in all module resolution modes.
How to pick your versioning strategy
- Single package repo —
npm version patch/minor/major + a git tag is sufficient.
- Monorepo with multiple packages — Changesets; it handles coordinated version bumps.
- Automated release — semantic-release if you want fully automated semver from commit messages. Changesets if you prefer human-described changes.
Common mistakes
Publishing with --access public missing for scoped packages. Scoped packages default to private; add --access public on first publish.
Not checking the tarball contents. Run npm pack --dry-run before publishing. You will occasionally find test files or .env in the output.
Missing files field. Without it, everything in the repo is published. Always specify "files": ["dist"].
Incorrect exports map. Tools that respect exports will throw if the paths do not exist. Test with attw before publishing.
What to skip
- Publishing from your local machine. A published package should come from a reproducible CI environment, not a developer machine with local patches.
npm version commit messages without automation. Once you have more than one package, do Changesets.
- Shipping TypeScript source without compiled output. Some consumers cannot run TypeScript source; always ship compiled JS.
FAQ
Do I need to publish to npm, or can I use a private registry?
Both. GitHub Packages, Verdaccio, and JFrog Artifactory support the npm protocol. Point your .npmrc to the registry URL.
How do I deprecate a version?
npm deprecate @myorg/my-package@"<1.2.0" "Upgrade to 1.2.0 — security fix". The message shows in install output.
What is the difference between main and exports?
main is the legacy CJS entry point. exports is the modern map that supports conditions (import/require/types) and blocks deep imports. Both should be set for maximum compatibility.
How do I test a local package before publishing?
Use npm link or pnpm add ../my-package to install the local version in a test project. Or npm pack and install the tarball.
Where to go next