monodrop
v0.18.0
Published
From monorepo to npm in one command
Maintainers
Readme
monodrop
Monorepos? Great. Publishing from a monorepo? Comically hard.
The Problem
Consider @acme/my-awesome-package, which imports @acme/internal-utils, a workspace dependency. The naive
approach - running npm publish - produces an uninstallable package because @acme/internal-utils was never published
to npm.
The standard solution is the "publish everything" approach. Tools like Lerna will publish every
internal dependency as its own public package. Installation now works, but @acme/internal-utils just became a
permanently published API you're committed to maintaining. Your internal refactoring freedom is gone.
You can throw a bundler at the problem: tools like esbuild or Rollup produce a self-contained file from a given entrypoint. But type declarations and sourcemaps often break, and consumers can't tree-shake a pre-bundled blob.
The Solution
monodrop is a publishing CLI that gets this right. It produces a single publishable directory containing everything needed from your package and its in-repo dependencies. Essentially, it produces a standard npm package that looks like you hand-crafted it for publication.
- 📦 Consumers get one package with exactly the code they need
- 🔒 Internal packages remain unpublished
- ✅ Tree-shaking, sourcemaps, and types all work
[!NOTE] ESM only — monodrop supports ES modules exclusively. CommonJS packages (
.cjsfiles or.jswithout"type": "module") are not supported. If your monorepo uses CommonJS, consider migrating to ESM.
Quickstart
# Install
pnpm add --save-dev monodrop
# or: yarn add --dev monodrop
# or: npm install --save-dev monodrop
# Build (monodrop publishes, it doesn't build)
npm run build
# Publish
npx monodrop publish packages/my-awesome-package --bump patch
# Or use "pack" to do everything short of publishing
npx monodrop pack packages/my-awesome-package --pack-destination /tmp/inspect --bump patchWhat Gets Published
Given this monorepo structure:
/path/to/my-monorepo/
└── packages/
├── my-awesome-package/
│ ├── package.json # name: @acme/my-awesome-package
│ └── src/
│ └── index.ts # import ... from '@acme/internal-utils'
└── internal-utils/
├── package.json # name: @acme/internal-utils (private)
└── src/
└── index.tsRunning npx monodrop publish packages/my-awesome-package produces:
/tmp/monodrop-xxxxxx/
└── packages/
└── my-awesome-package/ # preserves the package's path in the monorepo
├── package.json # name: "@acme/my-awesome-package", version: "1.3.0" (the new resolved version)
├── dist/
│ └── index.js # rewritten:
│ # import ... from '../deps/__acme__internal-utils/dist/index.js'
└── deps/
└── __acme__internal-utils/ # mangled package name, exact notation may vary.
└── dist/
└── index.jsThe deps/ directory is where the files of in-repo dependencies get embedded. Each dependency is placed under a
mangled version of its package name. This avoids name collisions regardless of where packages live in the monorepo.
Version Resolution
monodrop uses registry-based versioning: it queries the registry for the latest published version and bumps it
according to your --bump flag (patch, minor, major). Your source package.json is never modified.
This means you don't need to maintain version numbers in your source code. The registry is the versioning source of
truth, and monodrop computes the next version at publish time. Of course, if an exact version is specified
(--bump 1.7.9) it is used as-is.
For first-time publishing (when the package doesn't exist in the registry yet), monodrop treats the current version
as 0.0.0 and applies the bump—resulting in 0.0.1 for patch, 0.1.0 for minor (the default), or 1.0.0 for major.
If the version to publish to is already set in the package's package.json file (via npm version, Changesets, Lerna,
etc.), you can use --bump package to read the version directly from there:
npm version minor --no-git-tag-version # Sets version in package.json
npx monodrop publish . --bump package # Uses that versionExamples
# --bump defaults to "minor", so these two are equivalent:
npx monodrop publish packages/my-awesome-package --bump minor
npx monodrop publish packages/my-awesome-package
# Explicit version
npx monodrop publish packages/my-awesome-package --bump 2.3.0
# Prepare everything (including the tarball) without publishing
npx monodrop pack packages/my-awesome-package --bump 2.3.0
# Package location is resolved relative to CWD
cd /path/to/my-monorepo/packages
npx monodrop publish my-awesome-package --bump 2.3.0Programmatic API
For custom build steps, or integration with other tooling, you can use monodrop as a library instead of invoking the
CLI:
import { monodrop } from 'monodrop'
const result = await monodrop({
pathToSubjectPackages: ['packages/my-awesome-package'],
publish: true,
bump: 'minor',
cwd: process.cwd()
})
console.log(result.summaries[0].version) // '1.3.0'The above snippet is the programmatic equivalent of npx monodrop publish packages/my-awesome-package --bump minor.
Advanced Features
Custom Publish Name
Sometimes your internal package name doesn't match the name you want on npm. Add a monodrop.publishName field to
your package.json to publish under a different name without renaming the package across your monorepo:
{
"name": "@acme/my-awesome-package",
"monodrop": {
"publishName": "best-package-ever"
}
}Mirroring to a Public Repo
Want to open-source your package while keeping your monorepo private? Use --mirror-to to copy the package and its
in-repo dependencies to a separate public repository:
npx monodrop publish packages/my-awesome-package --mirror-to ../public-repoThis way, your public repo stays in sync with what you publish—all necessary packages included. Contributors can clone and work on your package.
Requires a clean working tree. Only committed files (from git HEAD) are mirrored.
Multiple Packages
If you have several public packages in your monorepo, publish them in one go by listing multiple directories:
npx monodrop publish packages/lib-a packages/lib-b --bump patchBy default, each package will be published at its own version (individual versioning). If lib-a is at 1.0.0 and lib-b
is at 2.0.0, a patch bump publishes them at 1.0.1 and 2.0.1 respectively.
You can also publish all specified packages at the same version (unified versioning, à la AWS SDK v3), by using the
--max flag. This applies the bump to the maximum version and publishes all packages at that version.
# Now both will be published at 2.0.1 (the max)
npx monodrop publish packages/lib-a packages/lib-b --bump patch --maxThis is purely a stylistic choice; correctness is unaffected since in-repo dependencies are always embedded.
Scope
monodrop makes a few deliberate choices:
- Runtime dependencies only — Only
dependenciesare traversed and embedded.devDependenciesare ignored since consumers don't need your build tools. - Version conflicts fail early — If two in-repo packages require different versions of the same third-party dependency, monodrop stops with a clear error rather than silently picking one.
- File selection via
npm pack— Yourfilesfield in package.json is the source of truth for what gets published. monodrop doesn't introduce its own file configuration. - Validates before heavy work — npm login and other prerequisites are checked upfront, before any file copying begins.
A few constraints to be aware of:
- Dynamic imports must use string literals —
await import('@pkg/lib')works; if a dynamic module path likeawait import(variable)is detected,monodropstops with a clear error. - Prerelease versions require explicit
--bump—--bump packageexpects strict semver (X.Y.Z). For prereleases, pass the version explicitly:--bump 1.0.0-beta.1. - peerDependencies are preserved, not embedded — As with any package publishing tool, you're responsible for ensuring peer dependencies (in-repo or not) are published and available to consumers.
- optionalDependencies are preserved, not embedded — If you list an in-repo package as optional, you're responsible for publishing it separately.
- Symlinks must stay within monorepo — Packages symlinked from outside the monorepo root are rejected.
- Undeclared in-repo imports fail — If your code imports an in-repo package not listed in
dependencies, monodrop catches this and fails with a clear error.
CLI Reference
monodrop publish <packages...> [options]
monodrop pack <packages...> [options]Commands
| Command | Description |
|---------|-------------|
| publish | Assemble package(s) with their in-repo dependencies and publish to npm |
| pack | Assemble package(s) and create tarball(s) without publishing |
Arguments
| Argument | Description |
|----------|-------------|
| packages | One or more package directories to assemble (required) |
Options
| Option | Alias | Type | Default | Description |
|--------|-------|------|---------|-------------|
| --bump | -b | string | minor | Version bump strategy: patch, minor, major, package, or explicit semver (e.g., 2.3.0). Use package to read version from package.json. |
| --max | | boolean | false | Use max version across all packages (unified versioning). When false, each package uses its own version. |
| --pack-destination | | string | (cwd) | pack only: directory where the tarball(s) are placed |
| --root | -r | string | (auto) | Monorepo root directory (auto-detected if omitted) |
| --mirror-to | -m | string | — | Mirror source files to a directory (for public repos) |
| --report | | string | — | Write resolved version to a file instead of stdout |
| --dynamic-imports-policy | | allow \| reject | reject | How to treat dynamic import() calls with computed (non-literal) module names: reject fails the packaging process, allow leaves them as-is |
| --help | | | | Show help |
| --version | | | | Show version number |
API Reference
monodrop(options): Promise<MonodropResult>
Assembles one or more monorepo packages and their in-repo dependencies, and optionally publishes to npm.
MonodropOptions
| Property | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
| pathToSubjectPackages | string \| string[] | Yes | — | Package directories to assemble. Relative paths resolved from cwd. |
| publish | boolean | Yes | — | Whether to publish to npm after assembly. |
| cwd | string | Yes | — | Base directory for resolving relative paths. |
| bump | string | No | "minor" | Version specifier: "patch", "minor", "major", "package", or explicit semver. |
| max | boolean | No | false | Use max version across all packages (unified versioning). |
| outputRoot | string | No | (temp dir) | Output directory for the assembled package. |
| monorepoRoot | string | No | (auto) | Monorepo root directory; auto-detected if omitted. |
| mirrorTo | string | No | — | Mirror source files to this directory. |
| npmrcPath | string | No | — | Path to .npmrc file for npm authentication. |
MonodropResult
| Property | Type | Description |
|----------|------|-------------|
| outputDir | string | Directory where the first package was assembled. |
| resolvedVersion | string \| undefined | The unified resolved version (only set when max: true). |
| summaries | Array<{ packageName: string; outputDir: string; version: string }> | Details for each assembled package, including its version. |
The Assembly Process
Here's a conceptual breakdown of the steps that happen at a typical monodrop run:
- Setup: Creates a dedicated output directory
- Version Resolution: Computes the new version (see above)
- Dependency Discovery: Traverses the dependency graph to find all in-repo packages the package depends on, transitively
- File Embedding: Copies the publishable files (per
npm pack) of each in-repo dependency into the output directory - Entry Point Resolution: Examines each package's entry points (respecting
exportsandmainfields) to compute the exact file locations that import statements will resolve to - Import Rewriting: Scans the
.jsand.d.tsfiles, converting imports of workspace packages to relative path imports (@acme/internal-utilsbecomes../deps/__acme__internal-utils/dist/index.js) - Package.json Rewrite: Sets the resolved version, removes in-repo deps, and adds any third-party deps they brought in
