Opal is an all-in-one toolkit for JavaScript and TypeScript projects, built to cover package management, running code, bundling, and testing. It ships as a single executable called opal.
At its core is opal-core, a module graph engine written in Rust. It works out what every file imports and where each import resolves, then caches that answer by content hash, so the next run over unchanged files is a cache hit. Every tool Opal adds is meant to share that one graph instead of carrying its own resolver.
The first of those tools is the package manager, and it works today. It picks the same versions npm would, lays out node_modules the way npm does, and links every file in from a single content-addressed store shared by all the projects on your machine.
opal install # install the dependencies in package.jsonThe opal command-line tool also lets you inspect a project's module graph and the shared store. Installs are crash-safe: if one is killed partway through, running opal install again finishes the job.
opal graph index.js --root . # resolve everything index.js imports
opal cache verify # re-hash the shared store and check it for corruption
opal cache gc # delete store files no project uses anymoreStatus: Beta. The package manager works today —
opal installresolves against the real npm registry and produces anode_modulestree Node runs against, validated on real projects (a Next.js scaffold at 365 packages, express, webpack, and a curated compatibility suite), and it is under active development, so expect rough edges and breaking releases. The runtime (opal run), bundler (opal build), and test runner (opal test) are not implemented; their directories undercrates/hold placeholder files only.
Opal supports Linux (x64 & arm64) and macOS (x64 & Apple Silicon). On Windows, it runs inside WSL2.
Linux users: the prebuilt binaries need glibc 2.34 or newer (Ubuntu 22.04+, Debian 12+, Fedora 35+, RHEL 9+). Check yours with
ldd --version. On an older system, Opal fails to start with aGLIBC_2.34 not founderror. Alpine and other musl-based distributions aren't supported.
Windows users: install and run Opal inside WSL2. Native Windows support is planned.
# with install script (recommended)
curl -fsSL https://raw.githubusercontent.com/saintparish4/opal/master/install.sh | bash
# a specific version
curl -fsSL https://raw.githubusercontent.com/saintparish4/opal/master/install.sh | OPAL_VERSION=v0.3.0 bash
# from source (prerequisites are in CONTRIBUTING.md)
git clone https://github.com/saintparish4/opal && cd opal
cargo build --release # binary at ./target/release/opalInstalling with npm or Homebrew is planned. Until then, use the install script.
The script picks the binary for your OS and CPU from the GitHub Release, verifies its SHA256 checksum, and installs it to ~/.opal/bin/opal. It puts that directory on your PATH by adding one line, marked # added by opal's install.sh, to ~/.zshrc, ~/.bashrc, or ~/.profile. To uninstall, delete the directory opal cache path prints, then ~/.opal and that line.
Opal is pre-1.0 beta software, and the lockfile format can change between minor versions. Don't point it at a project you can't reinstall.
To upgrade to the latest version of Opal, run:
opal upgradeTo switch to a specific version, older or newer, name it:
opal upgrade 0.3.1It downloads the release for your platform from GitHub, checks it against the release's SHA256SUMS, and runs it once to make sure it starts before replacing the binary, so a failed upgrade leaves the old one untouched. Running the install script again works too.
If the new version changed the lockfile format, the next opal install in each project re-resolves opal.lock and says so. Commit the rewritten file, because opal install --frozen-lockfile refuses to rewrite it and CI fails until you do. An older Opal can't read a lockfile written by a newer one.
There's no canary channel; every release is a tagged GitHub Release. To run unreleased changes from master, build from source.
Opal has four commands today: opal install, opal graph, opal cache, and opal upgrade. Run opal <command> --help to see their flags.
Commit opal.lock. In CI, run opal install --frozen-lockfile, which installs exactly what opal.lock records and fails instead of changing it.
add, remove, update, why, outdated, audit, and publish aren't implemented yet, so the binary doesn't have them. A command that exists and does nothing is worse than one that doesn't exist.
Opal against npm, pnpm, yarn, and bun, each installing the same package.json. Measured 2026-09-26 with scripts/compare-pms.py, in four scenarios:
- cold: no lockfile, cache, or
node_modules(a first install) - ci: a lockfile, but no cache or
node_modules(a fresh CI runner) - warm: a lockfile and a cache, but no
node_modules(a reinstall on your machine) - noop: everything already installed
Each number is the median of 3 runs (cold, ci) or 5 (warm, noop); fastest in bold. Every tool gets its own copy of the project and its own empty cache, the tools take turns so a network swing hits all of them, and install scripts are off for all five. Machine: AMD Ryzen 5 5625U (8 threads), 16 GB RAM, Linux under WSL2, Node 24.19.0.
express (68 packages)
| cold | ci | warm | noop | Peak memory (cold) | |
|---|---|---|---|---|---|
| opal 0.3.0 | 8.24s | 5.55s | 101ms | 21ms | 15 MB |
| npm 11.17.0 | 1.88s | 1.04s | 636ms | 355ms | 153 MB |
| pnpm 11.17.0 | 1.45s | 1.25s | 788ms | 505ms | 329 MB |
| yarn 1.22.22 | 1.71s | 1.32s | 598ms | 290ms | 160 MB |
| bun 1.3.14 | 519ms | 328ms | 77ms | 11ms | 40 MB |
Next.js 16.3.2, the create-next-app defaults (about 360 packages)
| cold | ci | warm | noop | Peak memory (cold) | |
|---|---|---|---|---|---|
| opal 0.3.0 | 124.59s | 100.81s | 1.08s | 96ms | 285 MB |
| npm 11.17.0 | 29.44s | 16.25s | 13.46s | 734ms | 416 MB |
| pnpm 11.17.0 | 22.42s | 17.15s | 2.70s | 595ms | 1,884 MB |
| yarn 1.22.22 | 59.98s | 53.90s | 6.70s | 414ms | 633 MB |
| bun 1.3.14 | 19.27s | 13.52s | 1.32s | 22ms | 565 MB |
The same benchmark on a second machine: Intel Core i9-9900K (16 threads), 16 GB RAM, Linux under WSL2
Same tool versions and Node 24.19.0, measured 2026-09-26 on a different network.
express
| cold | ci | warm | noop | Peak memory (cold) | |
|---|---|---|---|---|---|
| opal 0.3.0 | 7.53s | 6.07s | 93ms | 16ms | 15 MB |
| npm 11.17.0 | 1.61s | 934ms | 590ms | 291ms | 158 MB |
| pnpm 11.17.0 | 1.32s | 1.14s | 739ms | 418ms | 470 MB |
| yarn 1.22.22 | 1.61s | 1.25s | 560ms | 225ms | 162 MB |
| bun 1.3.14 | 480ms | 557ms | 42ms | 6ms | 40 MB |
Next.js 16.3.2
| cold | ci | warm | noop | Peak memory (cold) | |
|---|---|---|---|---|---|
| opal 0.3.0 | 180.29s | 177.91s | 673ms | 77ms | 277 MB |
| npm 11.17.0 | 27.88s | 14.03s | 12.37s | 738ms | 420 MB |
| pnpm 11.17.0 | 18.60s | 15.78s | 1.55s | 447ms | 1,789 MB |
| yarn 1.22.22 | 56.11s | 48.00s | 6.36s | 319ms | 649 MB |
| bun 1.3.14 | 16.87s | 11.97s | 641ms | 16ms | 718 MB |
Across both machines:
- First installs and CI are where Opal loses. It's 4–7× slower than npm on a cold install and 5–13× slower on CI, because it downloads packages one at a time. The gap is widest on Next.js's large downloads, and it depends on the network. Parallel downloads are the next thing being built.
- Reinstalls are where it wins. A warm install is 6× faster than npm on express and 12–18× on Next.js, where it's roughly tied with bun. A no-op install is 17–18× (express) and 8–10× (Next.js) faster than npm; bun is faster still.
- It uses the least memory of the five on every cold install.
- On Next.js, opal, yarn, and bun also download six musl builds that npm and pnpm skip (see Limitations), which adds to their cold and CI times.
Absolute times vary between sessions and machines, so compare tools within one table rather than across tables.
Worth knowing before you point Opal at a project:
- First installs and CI installs are slower than npm's, 4–7× on a cold install and up to 13× on CI in the benchmarks, because packages download one at a time. Linking already runs in parallel; parallel downloads are next.
- Lifecycle scripts (
preinstall/install/postinstall) do not run. Packages shipping prebuilt binaries (esbuild,sharp,@next/swc) work; a package that needsnode-gypto compile at install time installs but does not build.opal installsays so on every run: it names each dependency whose install scripts were skipped (including native addons that declare none and rely on npm runningnode-gyp rebuildfor theirbinding.gyp), and the project's own lifecycle scripts,prepareincluded. libcisn't checked. On Linux with glibc (most distributions), Opal also installs the musl builds of native packages, which npm and pnpm skip. On a Next.js app that's six extra packages and 124 MB, 91 MB of it@next/swc-linux-x64-musl. A glibc system doesn't use them, but they cost download time and disk.- Peers are recorded and classified, never auto-installed.
- Versions can differ slightly from npm's. Opal reuses a version already in the tree whenever it satisfies a range, where npm sometimes adds a newer copy: on a Next.js app, that's one package (
postcss8.5.23, where npm also installs 8.5.28). Opal doesn't prefer versions whoseenginesmatch your Node, which npm does. And it doesn't honorbundleDependencies: packages a dependency ships inside its own tarball are also resolved and downloaded from the registry, though Node still loads the bundled copy. - Changing
package.jsonre-resolves the whole tree. Adding one dependency can move unrelated packages to newer versions, where npm keeps each locked version that still satisfies its range. package-lock.jsonis ignored. In a project npm already installed, the firstopal installresolves every version and downloads every package again.git:andfile:specifiers are unsupported and reported as such; dependencies come from a registry only.- If your project and cache sit on different filesystems (a project on
/mnt/cunder WSL2 with the default cache, for instance), every file is copied instead of hardlinked and the install warns. Keep both on the same filesystem, or setOPAL_CACHE_DIRto a directory on the project's filesystem to move the shared store there.
See CONTRIBUTING.md to build Opal from source, run the tests, and check a change before opening a pull request.
MIT — see LICENSE.