Publishing npm Packages from Monorepo Workspaces
Publishing packages from your monorepo to npm gives other teams and external developers access to your shared libraries without copying code. Unlike the workspace:* protocol used internally, published packages use real version numbers and are installed from the npm registry. This article covers configuring your package for publication, handling dependencies correctly, managing access tokens, and automating versioning and publishing workflows.
Preparing a Package for Publication
Before publishing to npm, ensure your package has a clean package.json with all required fields. Here is a complete example for a UI library:
{
"name": "@mycompany/ui-library",
"version": "1.0.0",
"description": "Shared React component library for MyCompany products",
"license": "MIT",
"author": {
"name": "Your Name",
"email": "[email protected]",
"url": "https://mycompany.com"
},
"homepage": "https://github.com/mycompany/monorepo#readme",
"repository": {
"type": "git",
"url": "https://github.com/mycompany/monorepo.git",
"directory": "packages/ui-library"
},
"bugs": {
"url": "https://github.com/mycompany/monorepo/issues"
},
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.es.js",
"exports": {
".": {
"import": "./dist/index.es.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./package.json": "./package.json"
},
"files": ["dist"],
"scripts": {
"build": "esbuild src/index.ts --bundle --platform=node --target=es2020 --format=esm --outfile=dist/index.es.js && tsc --declaration --emitDeclarationOnly",
"prepublishOnly": "pnpm build && pnpm test",
"postpublish": "git push --follow-tags"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"esbuild": "workspace:*",
"react": "workspace:*",
"react-dom": "workspace:*",
"typescript": "workspace:*"
}
}
The key fields are:
name: scoped name (@scope/package) for organization; unscoped for public packages.exports: defines which files consuming code can import.files: lists which files to include in the tarball (ignoresrc/, test files).peerDependencies: declares React as required but not bundled.prepublishOnly: runs tests and builds before every publish to prevent broken releases.
Building and Bundling
Configure your TypeScript compiler to emit declaration files (.d.ts) for type safety:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
Your build script should produce both ES modules (for modern bundlers) and CommonJS (for older environments):
pnpm add -D esbuild tsup
Create a build.js script using tsup for simplicity:
pnpm add -D tsup
Update package.json script:
{
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts",
"dev": "tsup src/index.ts --format esm,cjs --dts --watch"
}
}
Running pnpm build creates dist/index.es.js, dist/index.cjs, and dist/index.d.ts.
Setting Up npm Authentication
Before publishing, authenticate with npm using an access token. Generate a token at npmjs.com/settings/tokens:
- Log in to npm.
- Navigate to Access Tokens.
- Create a new token with "Publish" permissions.
- Copy the token.
Configure your local environment:
npm login
# or use a token directly:
npm config set //registry.npmjs.org/:_authToken YOUR_TOKEN_HERE
For CI/CD, set the NPM_TOKEN environment variable:
export NPM_TOKEN="your_token_here"
npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
Publishing a Single Package
From the package directory, publish to npm:
cd packages/ui-library
npm publish --access=public
The --access=public flag makes scoped packages public by default (private scopes require paid plans). If successful, your package is now available on npm:
npm install @mycompany/ui-library
Publishing Multiple Packages in Sequence
Turborepo provides the turbo prune command to build a subset of packages for publishing. First, build all packages:
turbo build
Then publish each package individually or use a script. Create scripts/publish.sh at the monorepo root:
#!/bin/bash
set -e
for dir in packages/*/; do
package_name=$(jq -r '.name' "$dir/package.json")
echo "Publishing $package_name..."
cd "$dir"
npm publish --access=public
cd ../..
echo "$package_name published!"
done
git push --follow-tags
Run it with:
chmod +x scripts/publish.sh
./scripts/publish.sh
Handling Dependencies in Published Packages
When publishing, ensure:
peerDependenciesare declared for packages your library requires but does not own (React, Vue).dependenciesare for internal utilities your library owns and bundles.devDependenciesare never bundled; they are only for development.
For example, if your UI library uses a internal utility package:
{
"name": "@mycompany/ui-library",
"dependencies": {
"@mycompany/utils": "^1.0.0"
},
"peerDependencies": {
"react": "^18.0.0"
},
"devDependencies": {
"typescript": "workspace:*"
}
}
When published, consuming code installs both @mycompany/ui-library and @mycompany/utils. The workspace:* in devDependencies is converted to a real version number during publish.
Automating Versioning and Publishing
Use changesets to automate semantic versioning across your monorepo. Install it:
pnpm add -D @changesets/cli @changesets/changelog-github
pnpm exec changeset init
This creates a .changeset directory. Before publishing, create changeset files:
pnpm exec changeset
Answer the prompts to select which packages changed and the bump type (patch, minor, major). This creates a .changeset/*.md file. Commit these files with your code.
Then, when ready to publish, run:
pnpm exec changeset version
git add .
git commit -m "chore: release"
pnpm exec changeset publish
This bumps versions, updates changelogs, publishes to npm, and pushes tags to git.
Key Takeaways
- Configure
exportsinpackage.jsonto define what consuming code can import. - List only built files in the
filesfield to keep tarball size small. - Use
peerDependenciesfor React and frameworks; usedependenciesfor owned utilities. - Generate TypeScript declaration files (
.d.ts) so consuming code gets type hints. - Use
prepublishOnlyscripts to run tests and builds before every publish, preventing bad releases. - Automate versioning and publishing with changesets to ensure consistency across your monorepo.
Frequently Asked Questions
Should I publish every package in my monorepo?
No. Mark internal packages as private in package.json: "private": true. Publish only packages other teams or external developers need.
How do I handle breaking changes across multiple packages?
Use semantic versioning strictly. Document breaking changes in release notes. If package A breaks, bump it to v2.0.0. Dependent packages can then upgrade gradually.
Can I use the same npm token for all packages?
Yes, a single npm token with "Publish" permissions can publish all your organization's packages. Store it securely in CI/CD secrets, never in code.
What if I publish a bad version?
You can unpublish a package version within 72 hours using npm unpublish @scope/pkg@version. After that, you must publish a new patch version with the fix.
How do I handle npm scope access?
Create an organization on npm, then create teams with publish permissions. Grant team members access to publish packages under your org scope. This provides better access control than personal accounts.