Skip to content

moojo-tech/monocrate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

155 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

monocrate

npm version CI License: MIT

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 publish produces an uninstallable package - @acme/internal-utils was never published.
  • Publish-all approach (e.g., Lerna): Publishes every internal dependency. Now your internal @acme/internal-utils is 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 patch

What 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.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.

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 package

Programmatic 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-repo

This 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 patch

By 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 --max

This 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.

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages