For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
Sign inTry it free
DocsGuidesSDKsIntegrationsAPI docsTutorialsFlagship blog
DocsGuidesSDKsIntegrationsAPI docsTutorialsFlagship blog
  • Tutorials
    • The AI Iteration Loop for Deploying Reliable Agents with LangGraph
    • Using LaunchDarkly feature flags and Experimentation with Wordpress
    • Migrate a Hardcoded LangGraph Agent to LaunchDarkly AgentControl in 20 Minutes
    • Offline Evaluation of RAG-Grounded Answers in AgentControl
    • Beyond n8n for Workflow Automation: Agent Graphs as Your Universal Agent Harness
    • Catch your first silent AI failure with Vega AI in under 10 minutes
    • Evaluate LLM code generation with LLM-as-judge evaluators
    • OpenTelemetry for LLM Applications: A Practical Guide with LaunchDarkly and Langfuse
    • Use LaunchDarkly Agent Skills in Claude Code and Cursor
    • Detection to Resolution: Real World Debugging with Rage Clicks and Session Replay
    • Compare AI orchestrators: LangGraph vs Strands vs OpenAI Swarm
    • Building a data extraction pipeline with LaunchDarkly
    • Day 12 | 🎊 New Year, New Observability
    • Day 11 | ✉️ Letters to Santa: What engineering teams really want from Observability in 2026
    • Day 10 | Why observability and feature flags go together like milk and cookies
    • Day 9 | 👻 The Three Ghosts Haunting Your AI This Holiday Season
    • Day 7 | 🎄✨The Rockefeller tree in NYC: SLOs that actually drive decisions
    • Day 6 | 💸 The famous green character that stole your cloud budget: the cardinality problem
    • Day 5 | 🧹 Using a Popular Tidying Method to Consolidate Your Observability Stack
    • Day 4 | ❄️ Tracing the impact of holiday styling in your Node.js app
    • Day 8 | 🎁 Observable Multi-Modal Agentic Systems
    • Day 3 | 🔔 Jingle All the Way to Zero-Config Observability
    • Day 2 | 🎅 He knows if you have been bad or good... But what if he gets it wrong?
    • Collecting user feedback in your app with feature flags
    • Day 1 | 🎄 Observability Under the Tree: What Changed in 2025
    • Build a User Frustration Detection & Response System
    • When to Add Online Evals to Your AgentControl
    • Detecting User Frustration: Understanding Rage Clicks and Session Replay
    • AgentControl config CI/CD Pipeline: Automated Quality Gates and Safe Deployment
    • A Deeper Look at LaunchDarkly Architecture: More than Feature Flags
    • Add Observability to Your React Native App in 5 minutes
    • Smart AI Agent Targeting with MCP Tools
    • Build a LangGraph Multi-Agent System in 20 Minutes with LaunchDarkly AgentControl
    • Snowflake Cortex Completion API + LaunchDarkly SDK Integration
    • Using AgentControl to review database changes
    • How to implement WebSockets and kill switches in a Python application
    • 4 hacks to turbocharge your Cursor productivity
    • Create a feature flag in your IDE in 5 minutes with LaunchDarkly's MCP server
    • Observability for Your Go ORM: OpenTelemetry Integration with GORM
    • The complete guide to OpenTelemetry in Next.js
    • How to instrument your React Native app with OpenTelemetry
    • The complete guide to OpenTelemetry in Python
    • Monitoring Browser Applications with OpenTelemetry
    • How to Use OpenTelemetry to Monitor Next.js Applications
    • What is OpenTelemetry and Why Should I Care?
    • Distributed Tracing in Next.js Apps
    • Tracing Distributed Systems in Next.js
    • Real-time Monitoring in Django: Essential Tools and Techniques
    • DeepSeek vs Qwen: local model showdown featuring LaunchDarkly AgentControl
    • Application Tracing in .NET for Performance Monitoring
    • The Ultimate Guide to Ruby Logging: Best Libraries and Practices
    • Using Materialized Views in ClickHouse (vs. Postgres)
    • Filtering and Sampling LaunchDarkly Ingest
    • How to Set Up Your Production AWS MSK Kafka Cluster
    • Publishing an NPM Package with Private pnpm Monorepo Dependencies
    • How To Use The Chrome Inspector & Debugger
    • 3 Levels of Data Validation in a Full Stack Application With React
    • The power of the monorepo: Keep your fullstack app in sync!
    • Compression: The simple, powerful upgrade for your web stack
    • Video tutorials
Sign inTry it free
LogoLogo
On this page
  • What is a monorepo?
  • So how does the referencing work?
  • Publishing an NPM package with workspace dependencies
  • The Solution
  • A simplified example of private dependencies
  • tsconfig.json changes required
  • Use pnpm-workspace.yaml with pnpm
  • Individual package build steps
Tutorials

Publishing an NPM Package with Private pnpm Monorepo Dependencies

Was this page helpful?
Previous

How To Use The Chrome Inspector & Debugger

Next
Built with

Published January 27, 2023

portrait of Vadim Korolik.

by Vadim Korolik

Trying to publish an npm package but have a complicated monorepo setup? Publishing a library that depends on other packages you’ve built but don’t want published to npm? We’ll be covering how we do this at LaunchDarkly to setup our npmjs package. But first…

What is a monorepo?

A monorepo is a code repository that stores multiple distinct projects side by side, typically organized via the directory structure. Projects of a common language may reside in a

Here’s what our application’s structure looks like abridged:

highlight/
├─ backend/
│ ├─ main.go
├─ packages/
│ ├─ ui/
│ │ ├─ package.json
│ │ ├─ src/
│ │ │ ├─ index.ts
├─ sdk/
│ ├─ highlight-run/
│ │ ├─ package.json
│ │ ├─ src/
│ │ │ ├─ index.ts
│ ├─ highlight-next/
│ │ ├─ package.json
│ │ ├─ src/
│ │ │ ├─ index.ts
│ ├─ highlight-py/
│ │ ├─ pyproject.toml
│ │ ├─ highlight_io/
│ │ │ ├─ ...
├─ frontend/
│ ├─ package.json
│ ├─ src/
│ │ ├─ index.tsx
├─ package.json
├─ yarn.lock
├─ ...

Our repository stores our golang backend, sdks (nodejs, nextjs, python, and more), and typescript frontend all side-by-side. Why? Packages in a monorepo can use one another directly, without having to push/pull from an external store like npm. Another way to put it: we can test our highlight-run package from our frontend directly, without the need for yalc or any other steps to sync the dependency.

So how does the referencing work?

In our repository, we use yarn v3 workspaces to bring together our javascript / typescript packages. Setting the workspaces key in the top level package.json is all we need to do, and yarn handles pulling in dependencies of packages into the overall yarn.lock file. Conveniently, running yarn from anywhere in our monorepo works, as yarn finds the top level package.json and resolves dependencies accordingly.

...
"workspaces": [
"packages/*",
"frontend",
"sdk/highlight-next",
"sdk/highlight-run",
...
],
...

Let’s take a look at how frontend might use the highlight.run library. If highlight.run is built as a library that will be published to NPM, the library should be referenced as a package import. The import looks exactly the same as if you were to use the library from npm, but yarn will automatically use the local version instead. In our frontend index.tsx, we reference the local package as follows:

import { H } from 'highlight.run'
H.init('...')

Yarn knows that highlight.run exists under sdk/... because that directory is part of the workspaces key and has the corresponding name in its package.json. That’s all

Publishing an NPM package with workspace dependencies

That setup sounded simple, right? Just write JS/TS, reference other local packages as you would if they were imported from npm, and you can publish your library. There’s a bit of a catch here though: our highlight.run library uses our internal client typescript package that isn’t public. While we want to publish highlight.run to npm, we don’t want to publish the client library. Though highlight.run references typescript type definitions from client, we also don’t want to bundle most of the code into highlight.run as that would increase the bundle size (instead we have highlight.run inject client as a deferred <script> tag at browser runtime; see more as to why in our performance docs).

Just like with the frontend usage of highlight.run, we started with having highlight.run import from client by referencing the package.json name of @highlight-run/client. This successfully worked in development as the reference could be resolved, but when we built highlight.run for production, the bundle contained references to @highlight-run/client which could not be resolved in our customers’ environments since it was not a published package.

Next, we tried to use relative imports to make sure that the bundle didn’t have any references to the private package. We replaced imports of @highlight-run/client with relative paths like ../../client/src/foo.ts . This worked great both in development for publishing the bundle to npm, until we noticed that our highlight.run npmjs package had a large bundle size as it was bundling the entire codebase of @highlight-run/client

The Solution

After a bit of trial and error, we arrived at a solution: use relative path imports and rely on code splitting and type imports.

Here’s a snippet of the imports from our highlight.run entrypoint.

...
import { GenerateSecureID } from '../../client/src/utils/secure-id'
import type { Highlight, HighlightClassOptions } from '../../client/src/index'
...

When we need to import source code, like a function or a constant, we import it using a path import, making sure that the file that is imported from has as little other code as possible by breaking up our code into many files. This allows our bundler, rollup, to minimize the amount of code it needs to pull in when resolving the import.

When we import a typescript type, using an import type statement allows rollup to ensure it is only importing the type definitions from the file, without importing the actual source code implementations. As a result, the output is efficiently constrained just to what is actually necessary, yielding a smaller bundle size as a result.

A simplified example of private dependencies

Check out our pnpm example of the monorepo setup here (with tsup bundling using rollup under the hood)! A other few gotchas that we discovered along the way that we show how to configure in the example repo:

tsconfig.json changes required

You’ll need to update your tsconfig.json to include a references key to resolve the types of workspace packages. Private packages imported via references also need to have "composite": true set.

Use pnpm-workspace.yaml with pnpm

If you are migrating to pnpm from a yarn workspaces setup, you’ll need to move your workspace definitions from your package.json to a new file named pnpm-workspace.yaml in the top level of your repository.

Individual package build steps

When setting up your code bundling, your packages may need build steps to output the production bundle that will be published to npm. In our example, we start simple with a manual tsup src --target esnext --dts script in the packages’ package.json files, but for a larger project, you’ll likely be interested in setting up something like turborepo or Nx. These build systems provide automation for the bundling steps that make it easier to manage the order in which your packages must be built. We’ll have a blog about digging into javascript build systems in the future! If you’re interested, let us know!