Skip to main content

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 (ignore src/, 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:

  1. Log in to npm.
  2. Navigate to Access Tokens.
  3. Create a new token with "Publish" permissions.
  4. 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:

  1. peerDependencies are declared for packages your library requires but does not own (React, Vue).
  2. dependencies are for internal utilities your library owns and bundles.
  3. devDependencies are 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 exports in package.json to define what consuming code can import.
  • List only built files in the files field to keep tarball size small.
  • Use peerDependencies for React and frameworks; use dependencies for owned utilities.
  • Generate TypeScript declaration files (.d.ts) so consuming code gets type hints.
  • Use prepublishOnly scripts 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.

Further Reading