An all-in-one JavaScript toolkit in a single Rust binary, built on one shared module graph. Today: opal install, a beta npm-compatible package manager with a content-addressed store, crash-safe installs, and resolution cross-checked against npm.
Rust
1
48 commits
updated Oct 1, 2026
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.json
The 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 anymore
Status: 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. v0.3.1 is the exception: it needs glibc 2.39 (Ubuntu 24.04+, Debian 13+, Fedora 40+). On an older system, the install script stops without installing anything. 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/opal
Installing 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 upgrade
To switch to a specific version, older or newer, name it:
opal upgrade 0.3.1
It 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 benchmarks/compare-pms.py, in four scenarios:
node_modules (a first install)node_modules (a fresh CI runner)node_modules (a reinstall on your machine)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 |
Across both machines (the second is an Intel Core i9-9900K, 16 threads):
Absolute times vary between sessions and machines, so compare tools within one table rather than across tables.
The full method, min–max ranges, the second machine's tables, and how to reproduce every number are in benchmarks/BENCHMARKS.md.
Worth knowing before you point Opal at a project:
preinstall/install/postinstall) do not run. Packages shipping prebuilt binaries (esbuild, sharp, @next/swc) work; a package that needs node-gyp to compile at install time installs but does not build. opal install says so on every run: it names each dependency whose install scripts were skipped (including native addons that declare none and rely on npm running node-gyp rebuild for their binding.gyp), and the project's own lifecycle scripts, prepare included.libc isn'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.postcss 8.5.23, where npm also installs 8.5.28). Opal doesn't prefer versions whose engines match your Node, which npm does. And it doesn't honor bundleDependencies: packages a dependency ships inside its own tarball are also resolved and downloaded from the registry, though Node still loads the bundled copy.package.json re-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.json is ignored. In a project npm already installed, the first opal install resolves every version and downloads every package again.git: and file: specifiers are unsupported and reported as such; dependencies come from a registry only./mnt/c under 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 set OPAL_CACHE_DIR to 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.
Rust
97.4%
Python
1.9%
An all-in-one JavaScript toolkit in a single Rust binary, built on one shared module graph. Today: opal install, a beta npm-compatible package manager with a content-addressed store, crash-safe installs, and resolution cross-checked against npm.
Rust
1
48 commits
updated Oct 1, 2026
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.json
The 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 anymore
Status: 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. v0.3.1 is the exception: it needs glibc 2.39 (Ubuntu 24.04+, Debian 13+, Fedora 40+). On an older system, the install script stops without installing anything. 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/opal
Installing 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 upgrade
To switch to a specific version, older or newer, name it:
opal upgrade 0.3.1
It 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 benchmarks/compare-pms.py, in four scenarios:
node_modules (a first install)node_modules (a fresh CI runner)node_modules (a reinstall on your machine)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 |
Across both machines (the second is an Intel Core i9-9900K, 16 threads):
Absolute times vary between sessions and machines, so compare tools within one table rather than across tables.
The full method, min–max ranges, the second machine's tables, and how to reproduce every number are in benchmarks/BENCHMARKS.md.
Worth knowing before you point Opal at a project:
preinstall/install/postinstall) do not run. Packages shipping prebuilt binaries (esbuild, sharp, @next/swc) work; a package that needs node-gyp to compile at install time installs but does not build. opal install says so on every run: it names each dependency whose install scripts were skipped (including native addons that declare none and rely on npm running node-gyp rebuild for their binding.gyp), and the project's own lifecycle scripts, prepare included.libc isn'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.postcss 8.5.23, where npm also installs 8.5.28). Opal doesn't prefer versions whose engines match your Node, which npm does. And it doesn't honor bundleDependencies: packages a dependency ships inside its own tarball are also resolved and downloaded from the registry, though Node still loads the bundled copy.package.json re-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.json is ignored. In a project npm already installed, the first opal install resolves every version and downloads every package again.git: and file: specifiers are unsupported and reported as such; dependencies come from a registry only./mnt/c under 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 set OPAL_CACHE_DIR to 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.
Rust
97.4%
Python
1.9%