Monorepos? Great. Publishing from a monorepo? Comically hard.
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.
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.
β οΈ 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 patchGiven 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.ts
Running 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.js
The 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.
- 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.
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 packageFor 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.
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"
}
}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.
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.
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 |
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. |