npm-utils
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.
Library
[]
= "0.6" # Rust 1.88+
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. |
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. |
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 ;
#
Install a committed lockfile's full tree (an npm ci, in Rust):
use Path;
#
Generate a license summary — or a CycloneDX / SPDX SBOM — from a committed lock:
use ;
#
Audit those same packages against vulnerability advisories — multiple sources behind one trait:
use ;
#
CLI
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:
That 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 version
Run 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.
License checks
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.7
npm-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.
Vulnerability checks
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 tree
A 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.
Progress output
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 stdout
Examples
See 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.
Scope
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), public registry only (no .npmrc/auth), 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.
Anything unsupported — a dist-tag like next, overrides, lockfile v1 — fails with a clear error rather than silently.
License
MIT — see LICENSE.