Pure-Rust utilities for the npm registry and web assets — resolve a package version, download npm tarballs and GitHub archives, extract files, and install a real node_modules/ from a package.json or package-lock.json.
No Node or npm at build time; just ureq + archive extraction.
Handy from a build.rs to vendor browser/JS dependencies into your own asset tree.
It's both a library (the modules below) and an optional command-line tool — a pure-Rust subset of npm's verbs (install / add / ci / sbom / …).
See CLI.
[dependencies]
npm-utils = "0.7" # Rust 1.88+
# a GitHub token for private release assets and archives (see Scope):
npm-utils = { version = "0.7", features = ["github"] }Composable modules — the full API is on docs.rs:
| Module | What it does |
|---|---|
registry |
Resolve the newest version in a semver range; build tarball URLs; fetch packuments (abbreviated or full). |
download |
Fetch over HTTPS with one retry and a 100 MB cap; build GitHub archive URLs; with the github feature, a token from GH_TOKEN or GITHUB_TOKEN for private release assets and archives. |
extract |
Unpack .tar.gz / .zip — all files, an explicit file map, or a predicate — path-traversal-safe. |
integrity |
Verify a tarball's sha512 Subresource-Integrity before its bytes are trusted. |
install |
Build a real node_modules/: resolve a package.json (npm install) or reproduce a package-lock.json exactly (npm ci), every tarball integrity-checked. Hoisted workspaces are reproduced too — member/file: links are symlinked into node_modules/. |
package_json |
Parse package.json / package-lock.json and the npm version-spec grammar; write npm-faithful manifests and v3 locks. |
sbom |
Render a committed lock as a license summary, CycloneDX 1.6, or SPDX 2.3. |
audit |
Check a project, manifest/lockfile path, or name=range spec against vulnerability advisories (npm registry + OSV) behind a pluggable source trait. |
cache |
Content-hash markers and a cross-process lock for skip-if-unchanged downloads. |
path_safety |
The traversal/symlink hardening shared by extract and install. |
Vendor a single package's browser assets:
use npm_utils::{download, extract, registry::Registry};
# fn main() -> Result<(), Box<dyn std::error::Error>> {
let reg = Registry::npm();
let lit = reg.resolve("lit", &"^3".parse()?)?;
let tgz = download::fetch(&lit.tarball_url)?;
extract::tar_gz(&tgz, "dist/lit".as_ref(), Some("package/"), extract::Select::All)?;
# Ok(()) }Install a committed lockfile's full tree (an npm ci, in Rust):
use std::path::Path;
# fn main() -> Result<(), Box<dyn std::error::Error>> {
let project = Path::new("examples/app");
npm_utils::install::from_lockfile(&project.join("package-lock.json"), project)?;
// → project/node_modules/ populated + .bin shims + workspace-member symlinks;
// now run `node node_modules/.bin/tsc`.
# Ok(()) }Generate a license summary — or a CycloneDX / SPDX SBOM — from a committed lock:
use npm_utils::{package_json::lock::Lockfile, sbom};
# fn main() -> Result<(), Box<dyn std::error::Error>> {
let lock = Lockfile::parse(&std::fs::read_to_string("package-lock.json")?)?;
let bom = sbom::components(&lock);
print!("{}", sbom::render_summary(&bom)); // license overview
std::fs::write("sbom.cdx.json", sbom::to_cyclonedx(&bom, "my-app", "1.0.0", None))?;
# Ok(()) }Audit those same packages against vulnerability advisories — multiple sources behind one trait:
use npm_utils::{audit, package_json::lock::Lockfile, sbom};
# fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let lock = Lockfile::parse(&std::fs::read_to_string("package-lock.json")?)?;
let components = sbom::components(&lock);
let sources: Vec<Box<dyn audit::AdvisorySource>> = vec![
Box::new(audit::npm::NpmRegistrySource::new("https://registry.npmjs.org")),
Box::new(audit::osv::OsvSource),
];
let report = audit::run_audit(&components, &sources); // dedup'd across sources,
print!("{}", audit::render_summary(&report)); // filtered to installed versions
# Ok(()) }The same engine ships as a command-line tool behind the cli feature — a pure-Rust subset of npm's verbs, no Node or npm:
cargo install npm-utils --features cliThat installs two binaries — npm-utils and cargo-npm-utils — so every verb works standalone or as a cargo subcommand (npm-utils add lit ≡ cargo npm-utils add lit).
$ npm-utils --help
Pure-Rust npm registry tools: install · ci · add · remove · init · upgrade · search · sbom · audit
Usage: npm-utils [OPTIONS] <COMMAND>
Commands:
install Resolve dependencies, write package-lock.json, install node_modules/ (npm install)
ci Install the exact tree package-lock.json pins (npm ci)
add Add packages to package.json, write the lock, and install (npm add)
remove Remove packages from package.json, refresh the lock, reinstall (npm remove)
init Create a package.json (npm init -y)
upgrade Re-resolve within ranges, refresh the lock, and install (npm update)
resolve Print the newest version matching a range (version, tarball, integrity)
download Download a package tarball — resolve and fetch, no install
search Search the registry for packages (npm search)
sbom Bill of materials from package-lock.json: license summary, CycloneDX, or SPDX
audit Check packages against vulnerability advisories (npm audit)
help Print this message or the help of the given subcommand(s)
Options:
--timeout <SECS>
Per-fetch timeout in seconds (default 120) — caps each registry/tarball request, not the whole run
--no-timeout
Disable download timeouts entirely (no per-fetch or connect bound)
--progress <MODE>
Progress rendering on stderr: auto (live on a terminal), on (live even piped), verbose (a line per event), none
Possible values:
- auto: Live rendering on a terminal, plain line pairs when piped
- on: Live rendering even when stderr is piped
- verbose: One terminated line per event — logfile-friendly, no control characters
- none: No status output (npm-utils: warnings and errors still print)
[default: auto]
-q, --quiet
Suppress status lines on stderr — shorthand for --progress=none (reports on stdout and npm-utils: messages still print)
-h, --help
Print help (see a summary with '-h')
-V, --version
Print versionRun npm-utils <command> --help for a verb's flags.
Package sources are written name, name@range, or name=range (e.g. lit, lit@^3, lit=^3) — the = form is never ambiguous with a path, and ./-prefixed arguments always read as paths.
install / add / upgrade write a lockfileVersion-3 package-lock.json that both npm and npm-utils ci read — every tarball pinned with its sha512.
It is an npm-compatible lock for the registry/production tree, not a byte-for-byte npm reproduction; the CLI mirrors npm's vocabulary for the subset it supports and is not a full npm drop-in.
The lockfile-writing verbs default to the fast abbreviated packument, which carries no license; pass --no-skip-license to fetch the full packument and record each package's license in the lock.
sbom then renders the license tree of the whole dependency graph — handy for auditing an external package you don't own:
$ mkdir lit-licenses && cd lit-licenses
$ npm-utils init --name lit-licenses
$ npm-utils add --no-skip-license lit@3.3.3 # resolve, record each license, install
$ npm-utils sbom # the transitive license tree, grouped
6 package(s) across 2 license(s)
BSD-3-Clause (5)
@lit-labs/ssr-dom-shim@1.6.0
@lit/reactive-element@2.1.2
lit-element@4.2.2
lit-html@3.3.3
lit@3.3.3
MIT (1)
@types/trusted-types@2.0.7npm-utils sbom --format cyclonedx (or spdx) emits the same tree as a standards-based compliance document — each component carrying its purl, declared license, and sha512.
audit checks packages against vulnerability advisories — like npm audit, but querying multiple sources behind one trait (npm's registry endpoint and OSV by default), deduped across sources and filtered to the versions you actually have:
$ npm-utils audit # query npm + OSV, group by package
found 7 vulnerabilities (1 critical, 3 high, 3 moderate, 0 low) in 1 package(s)
lodash@4.17.11
CRITICAL GHSA-jf85-cpcp-j695 Prototype Pollution in lodash
range <4.17.12 · CWE-1321, CWE-20 · https://github.com/advisories/GHSA-jf85-cpcp-j695
HIGH GHSA-35jh-r3h4-6jhm Command Injection in lodash
range <4.17.21 · CWE-77, CWE-94 · https://github.com/advisories/GHSA-35jh-r3h4-6jhm
...The positional source names what to audit — a project directory (its package-lock.json, else its package.json), an explicit manifest or lockfile path, or a registry spec:
$ npm-utils audit web/ # a project directory: lockfile preferred
$ npm-utils audit /tmp/package.json # an explicit package.json or package-lock.json path
$ npm-utils audit lit=^3 # a package and its full transitive dependency treeA finding at or above --audit-level exits 1, and an incomplete audit — failed advisory sources or unaudited dependencies — fails closed with exit 2 unless --allow-incomplete.
docs/audit.md covers the details: source spellings, the in-memory nested resolution, omissions, and the full exit and flag semantics.
Long-running phases report progress on stderr as named tasks (every fetch inside them is bounded by --timeout): [resolve] while a dependency tree resolves, with the in-flight registry fetches on its detail line; [install] with an (x/y) counter while tarballs download, verify, and extract (install/ci/add/remove/upgrade); one [npm]/[osv] task per advisory source during an audit.
The report on stdout (including --format json) and npm-utils: warnings/errors are never affected, so piping stdout stays clean in every mode.
The global --progress <MODE> selects the rendering:
--progress |
renders |
|---|---|
auto (default) |
live multi-line task blocks on a terminal; plain begin/finish line pairs when stderr is piped |
on (also 1, yes, true) |
the live rendering even when stderr is piped |
verbose |
one terminated line per event — begin, each item, finish — no control characters, for logfiles |
none (also 0, off, false, no) |
no status output; -q/--quiet is the shorthand |
$ npm-utils ci --progress=verbose 2>install.log # one line per installed package, log-friendly
$ npm-utils audit -q --format json | jq . # silent stderr, clean JSON on stdoutSee examples/date-converter for a runnable Lit + Temporal demo that vendors its browser dependencies with this crate — no Node or bundler in the build.
examples/audit-playground pins deliberately outdated packages so npm-utils audit has something to find — a reproducible vulnerability report to explore.
Not a general npm: npm-utils vendors public-registry packages and reproduces a committed package-lock.json — that's the remit.
So: no lifecycle scripts (by design), the public registry stays unauthenticated (no .npmrc), and node_modules() resolves a flat, prod-only tree that errors on a version conflict npm would nest — install from a lockfile (from_lockfile/ci) for a full tree.
GitHub is the one exception, behind the github feature (on in the CLI, off for a library build so no token is picked up by accident): api.github.com takes a token from GH_TOKEN or GITHUB_TOKEN, github.com never sees it, and a private release asset's or repository archive's browser URL is resolved through the GitHub API when a token is set, which npm itself cannot do; other GitHub hosts stay unauthenticated.
Anything unsupported — a dist-tag like next, overrides, lockfile v1 — fails with a clear error rather than silently.
MIT — see LICENSE; every file's licence and copyright holder are declared through REUSE (REUSE.toml, LICENSES/).
npm-utils is an independent project, not affiliated with or endorsed by npm, Inc. or GitHub; npm in its name and text refers to the registry and the tool it works with.
What each module follows and the registry's terms it works under are listed in docs/sources.md.