monocrate
v0.14.8
Published
From monorepo to npm in one command
Maintainers
Readme
monocrate
Monorepos? Great. Publishing from a monorepo? Comically hard.
The Problem
Consider @acme/my-awesome-package, which imports @acme/internal-utils, a workspace dependency.
Now you want to publish it.
- Naive approach:
npm publishproduces an uninstallable package -@acme/internal-utilswas never published. - Publish-all approach (e.g., Lerna): Publishes every internal dependency. Now your internal
@acme/internal-utilsis a permanent public API - rename a function there, break consumers you never intended to have. - Bundler approach (e.g., esbuild): Produces a self-contained blob, but types and sourcemaps break, and consumers can't tree-shake.
The Solution
monocrate is a publishing CLI that gets monorepos. It produces a single publishable directory containing everything needed from your package and its in-repo dependencies.
- 📦 Consumers get a single package with exactly what they need
- 🔒 Internal packages remain unpublished
- ✅ Tree-shaking, sourcemaps, and types all work
The result: a standard npm package that looks like you had hand-crafted it for publishing.
Quickstart
⚠️ ESM only — monocrate only supports ES modules. If your monorepo uses CommonJS, you need to migrate to ESM first.
# Install
pnpm add --save-dev monocrate
# Or: yarn add --dev monocrate
# Or: npm install --save-dev monocrate
# Build first (monocrate publishes; it doesn't build)
npm run build
# Publish
npx monocrate publish packages/my-awesome-package --bump patch
# Or: assemble without publishing
npx monocrate 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 monocrate pack packages/my-awesome-package produces:
/tmp/monocrate-xxxxxx/
└── packages/
└── my-awesome-package/ # preserves the package's original path
├── 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, the exact notation may vary.
└── dist/
└── index.jsThe deps/ directory is where in-repo dependencies are embedded. Each dependency is placed under a mangled form of
its package name, avoiding collisions regardless of where packages live in the monorepo.
Note: The actual directory name includes a randomized suffix (e.g.,
deps-a1b2c3d4/) to prevent conflicts with existing directories in your package.
For details on how monocrate assembles packages, see The Assembly Process.
Supported Scope
⚠️ Important:
- ESM only — CommonJS
require()calls are not rewritten, which will cause runtime failures for consumers. Only use monocrate on ESM-compliant monorepos. - peerDependencies and optionalDependencies — preserved in the output
package.json, not embedded. It's your responsibility to ensure these are published and available to consumers.
monocrate validates (and rejects with a clear error):
- Dynamic imports must use string literals —
await import('@pkg/lib')works;await import(variable)does not. - Consistent third-party versions — two in-repo packages cannot require different versions of the same dependency.
- Symlinks must stay within the monorepo — packages symlinked from outside the monorepo root are not supported.
Version Resolution
The --bump flag determines the published version. There are three approaches:
| --bump value | Source of truth | Example result |
| ----------------------- | --------------- | ------------------------ |
| patch/minor/major | npm registry | 1.2.3 → 1.2.4 |
| 1.8.9 | CLI argument | 1.8.9 |
| package | package.json | (whatever's in the file) |
When publishing multiple packages, see Multiple Packages for unified versioning with --max.
In all cases, your source package.json is never modified—the resolved version only appears in the assembled output.
# Registry-based: query npm, bump from latest (default is minor)
npx monocrate publish packages/my-awesome-package # bumps minor
npx monocrate publish packages/my-awesome-package --bump patch
# Explicit: use exact version
npx monocrate publish packages/my-awesome-package --bump 2.3.0
# Package-based: read from package.json
cd packages/my-awesome-package
npm version minor --no-git-tag-version
npx monocrate publish . --bump packageProgrammatic API
For custom build steps, or integration with other tooling, you can use monocrate as a library instead of invoking the
CLI:
import { monocrate } from 'monocrate'
const result = await monocrate({
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 monocrate 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 monocrate.publishName field to
your package.json to publish under a different name without renaming the package across your monorepo:
{
"name": "@acme/my-awesome-package",
"monocrate": {
"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 monocrate 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 monocrate publish packages/lib-a packages/lib-b --bump patchBy default, each package will be published with 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 monocrate publish packages/lib-a packages/lib-b --bump patch --maxThis is purely a stylistic choice; it doesn't affect correctness since in-repo dependencies are always embedded.
Reference
CLI
monocrate <command> <packages...> [options]Arguments
| Argument | Description |
| ---------- | ---------------------------------------------------------- |
| command | pack (create tarballs only) or publish (publish to npm) |
| packages | One or more package directories to process (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 | -d | string | (temp dir) | Directory to write tarballs to (pack command only) |
| --root | -r | string | (auto) | Monorepo root directory (auto-detected if omitted) |
| --mirror-to | -m | string | — | Mirror source files to a directory (for public repos) |
| --result-file | | string | — | Write full result as JSON to file |
| --help | | | | Show help |
| --version | | | | Show version number |
API
monocrate(options): Promise<MonocrateResult>
Assembles one or more monorepo packages and their in-repo dependencies, and optionally publishes to npm.
MonocrateOptions
| 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). |
| packDestination | 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. |
MonocrateResult
| Property | Type | Description |
| ----------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| resolvedVersion | string \| undefined | The unified resolved version (only set when max: true). |
| summaries | Array<{ packageName: string; version: string; tarballPath: string }> | Details for each assembled package, including version and path to generated tarball. |
