Publishing Libraries

Purpose and Scope

This guide explains how to take a library package that already works inside a Turborepo workspace and prepare it for publishing to the npm registry. The core decision is whether the package actually needs to become an external package. If the code is only consumed by apps and packages in the same workspace, an Internal Package is the easier strategy. If the package must be installed by consumers outside the repository, it needs a build artifact, package entrypoints, task dependencies, and a CI workflow that can reproduce the publishable output consistently. Sources: apps/docs/content/docs/guides/publishing-libraries.mdx

The first-party publishing guide frames the workflow as a practical baseline rather than a complete answer for every library architecture. Its stated path is to bundle, version, and publish packages from a monorepo using common tooling such as tsup and Changesets. The source example starts from a @repo/math package created as an Internal Package, then upgrades it into a package that can be used locally and deployed to npm. That makes the guide most useful for teams that already understand workspace packages and now need reliable external distribution. Sources: apps/docs/content/docs/guides/publishing-libraries.mdx

Relevant Source Files

  • apps/docs/content/docs/guides/publishing-libraries.mdx — Main guide for publishing libraries from a Turborepo monorepo, including the @repo/math example, bundling with tsup, dist outputs, package entrypoints, and task dependency guidance.
  • apps/docs/content/blog/free-vercel-remote-cache.mdx — Explains Vercel Remote Cache as a distributed cache and names the authentication variables used by non-Vercel CI providers.
  • apps/docs/content/blog/joining-vercel.mdx — Provides project context for Vercel-backed zero-config remote caching and the open-source Turborepo CLI.
  • apps/docs/content/docs/guides/ci-vendors/github-actions.mdx — Shows a concrete GitHub Actions workflow that installs dependencies, runs root Turborepo scripts, and can opt into Remote Caching with TURBO_TOKEN and TURBO_TEAM.
  • apps/docs/content/docs/guides/ci-vendors/vercel.mdx — Describes Vercel’s zero-config Turborepo integration and automatic use of Vercel Remote Cache for imported monorepos.
  • apps/docs/content/blog/2-10.mdx — Adds release-note context for current workflow concerns such as combining --affected and --filter, deferred input hashing, and local cache eviction.

Internal Package or Publishable Package

An Internal Package is source code that lives inside the workspace and is installed by other workspace packages using the package manager’s workspace resolution. That pattern is ideal for shared UI components, configuration, utilities, and business logic that never need to leave the repository. A publishable package has a stricter contract because npm consumers do not share the repository’s source layout, TypeScript configuration, package manager, or task graph. The published package must expose files that can be resolved by Node.js, bundlers, TypeScript, and downstream package managers without relying on Turborepo being present.

The practical dividing line is distribution. Internal Packages optimize for local development and low setup cost, while external packages optimize for compatibility outside the monorepo. The publishing guide explicitly warns that if a package does not need to be published to npm, it should remain an Internal Package because that is much easier to set up and use. When a team decides to publish, the library should stop assuming that consumers can compile its source directly. Instead, the package should emit a stable dist directory and declare the correct package metadata for each module format. Sources: apps/docs/content/docs/guides/publishing-libraries.mdx

Build Outputs and Package Entrypoints

The guide’s example uses tsup because it gives a compact path from TypeScript source to npm-ready artifacts. In ./packages/math/package.json, the package adds a build script that bundles src/index.ts to both CommonJS and ECMAScript module formats and emits type declarations. The dual-format setup is not mandatory, but it is recommended because npm packages are consumed in many environments. A library author can choose a narrower format strategy, but the package metadata must match the files that the bundler actually writes.

./packages/math/package.json
{
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts"
  }
}

tsup writes to dist by default in the documented setup, and that directory becomes a generated build artifact rather than source. The guide tells authors to add dist to .gitignore so bundled output is not committed, then add dist/** to the build task outputs in turbo.json. That second step is the Turborepo-specific piece: once the output directory is declared, Turborepo can cache and restore the library build instead of rerunning the bundler whenever the hash says the previous result is still valid. Sources: apps/docs/content/docs/guides/publishing-libraries.mdx

./turbo.json
{
  "tasks": {
    "build": {
      "outputs": ["dist/**"]
    }
  }
}

After the bundler is configured, the package needs npm-facing entrypoints. The example changes main to ./dist/index.js for CommonJS consumers, module to ./dist/index.mjs for ECMAScript module consumers, and types to ./dist/index.d.ts for TypeScript tooling. These fields are small, but they are the bridge between a successful local build and a usable published package. If they point to source files, missing files, or the wrong module format, consumers may install the package successfully and still fail at import time.

./packages/math/package.json
{
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts"
}

Task Graph Integration

Publishing from a monorepo is not only a package-level concern. Once an application depends on the publishable library, Turborepo must understand that the library’s build should complete before the application’s build. The guide calls this a task dependency and points to dependsOn as the configuration mechanism. In practice, the common pattern is to make the root build task depend on ^build, which tells Turborepo to build dependency packages before the current package’s build. That keeps local development, CI, and release preparation aligned around the same task graph. Sources: apps/docs/content/docs/guides/publishing-libraries.mdx, apps/docs/content/docs/guides/ci-vendors/github-actions.mdx

./turbo.json
{
  "tasks": {
    "build": {
      "outputs": ["dist/**"],
      "dependsOn": ["^build"]
    }
  }
}

This dependency ordering matters because publishable packages often feed both apps and other packages. If apps/web imports @repo/math, the app build should not race ahead with stale or missing library artifacts. The GitHub Actions guide shows the same principle in a CI-oriented turbo.json, where build has dependsOn: ["^build"] and output globs describe generated directories. The exact output list differs by framework and package type, but the goal is the same: make build products explicit so the cache and scheduler have enough information to execute correctly.

CI, Remote Cache, and Release Readiness

A publishing workflow should prove that the artifacts can be rebuilt from a clean checkout before a registry publish happens. The GitHub Actions guide starts from root scripts such as build: turbo run build and test: turbo run test, then creates .github/workflows/ci.yml to check out code, set up the package manager and Node.js, install dependencies, build, and test. For a library repository, a release job can build on the same foundation: run the Turborepo tasks first, then let the chosen versioning and publishing tool handle npm credentials and publication. Sources: apps/docs/content/docs/guides/ci-vendors/github-actions.mdx

./package.json
{
  "scripts": {
    "build": "turbo run build",
    "test": "turbo run test"
  },
  "devDependencies": {
    "turbo": "latest"
  }
}

Remote Caching is especially useful when publishing libraries because the same build may be requested by developers, pull request validation, and release automation. The Vercel Remote Cache blog describes Remote Caching as a distributed caching layer that prevents developers and CI from doing the same work twice. On Vercel, Turborepo commands are automatically configured to use Vercel Remote Cache. On other CI providers, the documented authentication variables are TURBO_TOKEN and TURBO_TEAM, and the GitHub Actions guide shows those variables as optional workflow environment entries. Sources: apps/docs/content/blog/free-vercel-remote-cache.mdx, apps/docs/content/docs/guides/ci-vendors/github-actions.mdx, apps/docs/content/docs/guides/ci-vendors/vercel.mdx

.github/workflows/ci.yml
env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}

The Vercel integration guide adds an important deployment-oriented distinction: monorepos imported into Vercel are preconfigured to use Vercel Remote Cache, while general CI systems need explicit setup. That does not replace a library publishing process, because npm publishing still needs registry credentials, versioning decisions, and package access configuration. It does mean that build and test verification can share cached artifacts across local machines, Vercel deployments, and CI providers when those environments are linked to the same remote cache. The older Vercel acquisition announcement also records the project direction toward Vercel-backed zero-config remote caching. Sources: apps/docs/content/docs/guides/ci-vendors/vercel.mdx, apps/docs/content/blog/joining-vercel.mdx

Compact Reference

ConcernConcrete setting or commandWhy it matters
Package build scripttsup src/index.ts --format cjs,esm --dtsProduces JavaScript bundles and type declarations from the package source.
Generated outputdist/** in tasks.build.outputsLets Turborepo cache and restore bundled library output.
CommonJS entrypointmain: ./dist/index.jsGives CommonJS consumers a resolvable file.
ESM entrypointmodule: ./dist/index.mjsGives ESM-aware bundlers and runtimes a module build.
Type declarationstypes: ./dist/index.d.tsGives TypeScript consumers the package API surface.
Task dependencydependsOn: ["^build"]Builds dependency packages before dependent package builds.
CI cache authTURBO_TOKEN, TURBO_TEAMAuthenticates non-Vercel CI to Vercel Remote Cache.

Turborepo release notes also highlight adjacent workflow features that can matter in larger publishing pipelines. Turborepo 2.10 describes deferred input hashing for generated files and dependency outputs, composable --affected and --filter for narrowing CI runs, and local cache eviction for reclaiming cache disk space. Those are not replacements for the basic library publishing setup, but they help mature repositories keep release validation fast and targeted as the number of packages grows. For example, an affected package query can reduce validation scope, while task outputs still preserve correctness for packages that must be rebuilt. Sources: apps/docs/content/blog/2-10.mdx

Start by keeping a package internal until there is a real external consumer. When npm distribution becomes necessary, add a package-local build script, emit dist, ignore the generated directory in source control, and declare dist/** as a Turborepo output. Then update main, module, and types so downstream consumers resolve the generated artifacts rather than workspace source. Finally, make the root task graph build dependency packages first, run the same build and test scripts in CI, and enable Remote Caching where possible so release validation does not repeat work unnecessarily.

The next pages to read are the Internal Packages concept page for deciding what should remain workspace-only, the TypeScript tool guide for library type-checking strategy, the turbo.json configuration reference for outputs and dependsOn, and the CI vendor guides for adapting the build-and-test workflow to the platform that will run releases.