Readiness: Implemented
The toolchain
Dependencies
uf can resolve, verify and install dependencies in its native manager, sharing file content across projects. It can also run the package manager your project selects. Both paths retain lockfile checks and disable install scripts by default.
What you will be able to do: install a project and keep its lockfile honest in CI, add and remove dependencies, find out why a package is in the tree and what is newer than your ranges allow, approve the dependencies that have to build, and read what uf checks about where a package came from.
What you need first: a project — Your first project — and the package manager you mean to use; npm comes with Node.js. Pinning which Node and which manager a project runs on is Environments.
Which manager runs
The first of these that says something decides:
packageManagerinuf.config.js, such as"pnpm@10";pm.packageManagerinuf.config.js, its older spelling;- the
packageManagerfield inpackage.json, such as"bun@1.3.13"; - a lockfile in the project root —
bun.lock,bun.lockb,pnpm-lock.yaml,yarn.lock,package-lock.jsonornpm-shrinkwrap.json, in that order; - a workspace root above the project that says one of the above;
- npm.
Every command that runs a manager prints what it chose and why, in its chosen by
row — no lockfile or packageManager field for a project uf new just wrote,
package-lock.json once npm has written one, packageManager field: bun@1.3.13
when the manifest names Bun. A yarn.lock is Yarn 2 or later when a .yarnrc.yml
sits beside it or the lockfile has a __metadata: block, and Yarn 1 otherwise.
Two lockfiles from different managers in one root are not an error: the one
higher in the list wins, and only uf inspect says the choice was ambiguous.
packageManager can pin a release as well as name a manager.
packageManager: "pnpm@10" runs the newest pnpm 10, locked in uf.lock and
installed into uf's store the first time a command needs it, so every machine
runs the same pnpm, on the project's runtime — see
Environments. packageManager: "pnpm", with no version, is
whichever pnpm is on PATH. A project still naming its manager in
pm.packageManager is told what to write instead:
! pm.packageManager is the top-level `packageManager` now, which can pin a release as well — write `packageManager: "pnpm"`
and a project that writes both, naming different managers, is refused with both keys named.
packageManager: "uf" selects the native manager. Its uf.lock records exact
versions, dependency edges, registry routes and archive integrity. A warm frozen
install reuses the graph and verified shared package trees. A cold frozen install
uses the locked tarball URLs without resolving metadata again.
Files are deduplicated across packages and hardlinked into projects. Across
filesystems, uf copies them. The terminal report shows downloads, hardlinks,
copies and measured resolve, fetch and link times. UF_PM_STORE selects an
absolute shared store location.
uf gc --global --dry-run lists unused package trees and shared files. Global
collection preserves live project references and files still linked into projects.
On Windows, shared file collection remains conservative; unused package trees
can be removed while file data is retained.
Installing
uf install · shop
─────────────────
manager npm
chosen by no lockfile or packageManager field
command npm install --ignore-scripts --loglevel=http
runtime node · …/node/26.8.1/bin/node
lockfile package-lock.json · 243 packages · 138.07 kB
plan …/shop/.uf/install.json
config ························ 565.5µs
workspace ························ 2.1ms
resolve ························ 3.68s
fetch ························ 1.71s
link ························ 62.1ms
lockfile ························ 8.1ms
total ························ 8.35s
dependency tree
added 241
…
provenance
attested 78
unknown 165
next steps
1. uf dev
2. uf check
✓ dependencies installed in 8.35s
That is a first install in a new project, with npm's own output — which uf passes
through above its summary — left out. Run again with nothing to do, the summary
is one line: ✓ already up to date in 1.23s.
The resolve, fetch and dependency-tree sections come from reading npm's own output and lockfile, so they appear for npm. pnpm, Yarn and Bun write to the terminal directly, and their summary is the rows, the timings uf can measure itself, and the next steps:
manager bun
chosen by packageManager field: bun@1.3.13
command bun install --ignore-scripts
lockfile bun.lock · 66.56 kB
.uf/install.json is written before the manager runs: which manager, which
lockfile, which hosts the project needs. It is there to read after a failure too.
In CI: exactly what the lockfile pins
uf install --frozen-lockfile installs what the lockfile pins and fails when the
manifests have moved away from it, rather than quietly resolving the difference:
| Manager | What runs |
|---|---|
| npm | npm ci |
| pnpm | pnpm install --frozen-lockfile |
| Yarn 1 | yarn install --frozen-lockfile |
| Yarn 2+ | yarn install --immutable |
| Bun | bun install --frozen-lockfile |
Each with --ignore-scripts, below. uf in CI says why this comes
before every check: uf check with nothing installed types every import it cannot
resolve as any.
Scripts
A package.json that declares scripts is refused before anything is fetched,
by uf install, uf add, uf remove, uf update, uf catalog set and
uf patch --commit alike:
error: package manifest …/shop/package.json declares scripts; use uf tasks in uf.config.js
A project's automation belongs in tasks, and install-time scripts are how a freshly cloned repository runs code nobody read — What uf does not do has the argument.
A dependency's install scripts are off too: every command that installs hands the
manager --ignore-scripts. Yarn 2+ rejects that flag on every command as an
unsupported option, so it is given the setting Yarn reads instead,
YARN_ENABLE_SCRIPTS=false, and pnpm remove, pnpm link and
pnpm patch-commit, which do not accept the flag either, are given
--config.ignore-scripts=true. Some dependencies genuinely need
to build, and
uf pm approve-builds lists the ones in your tree that would run code at install
time:
uf pm approve-builds · shop
───────────────────────────
manager npm
approvals in nothing: it is all or none
package version runs approved attested
acorn 8.18.0 prepare no unknown
lightningcss 1.33.0 prepare no unknown
rollup 4.63.2 prepare no yes
…
› 8 packages would run code at install time and are not approved, so uf does not let them run
! npm cannot approve one and not another: `--ignore-scripts` is all of them or none
› pm.allowLifecycleScripts in uf.config.js turns on every one of them, deliberately
On pnpm, Bun and Yarn 2+, uf pm approve-builds esbuild records the name in the
field that manager reads — pnpm.onlyBuiltDependencies, trustedDependencies,
dependenciesMeta — and the next install leaves --ignore-scripts off and lets
the manager build exactly what the list names. npm and Yarn 1 have no such list,
so on them an approval by name is refused rather than turned into "every package".
A name that is not waiting to build is refused with the list of the ones that are.
--dry-run writes nothing.
pm.allowLifecycleScripts: true lifts both refusals at once — the project's own
install-time lifecycle scripts and every dependency's — and is the one switch
that does. A script no install runs, such as start or ios, is never refused.
Adding and removing
$ uf add --dev is-number@6
manager npm
chosen by package-lock.json
command npm install --save-dev --ignore-scripts is-number@6
lockfile package-lock.json · 244 packages · 138.46 kB
manifest
field package range
+ devDependencies is-number ^6.0.0
dependency tree
added 1
package version
+ is-number 6.0.0
✓ 1 package recorded in devDependencies in 419.6ms
--dev, --optional and --peer choose the field. A specifier goes to the
manager exactly as written, so a range, a tag, an alias or a path means what it
means there; one that starts with - is refused, because the manager would read
it as a flag. uf remove — or uf uninstall — takes a name out of every field
that lists it, the lockfile and node_modules, and a name the manifest never
listed is not an error.
In a workspace, --filter chooses the members uf add, uf remove and
uf update change, with the selectors uf run --filter takes, and -w chooses
the workspace root from anywhere inside it:
$ uf add zod --filter api --filter web
manager pnpm
chosen by pnpm-lock.yaml
command pnpm add --ignore-scripts zod (api)
command pnpm add --ignore-scripts zod (web)
lockfile pnpm-lock.yaml · 212 packages · 96.10 kB
manifest
workspace field package range
+ api dependencies zod ^4.1.8
+ web dependencies zod ^4.1.8
✓ 2 packages recorded in dependencies in 2.41s
uf runs the manager in each member's directory, which npm, pnpm, both Yarns and
Bun all read as "this member", and the lockfile they write is the workspace
root's. A selector that picks nothing is refused with the list of members.
Yarn 2+ cannot update one member — yarn up moves a package everywhere — so
uf update --filter is refused there.
uf link ../ui links a package you are developing into the project, and uf link
inside it makes it linkable by name from other projects. Yarn 1 and Bun link by
name only, Yarn 2+ and pnpm 12 by path only, and the other form is refused with
the one to use. What uf link reports is read from node_modules, because npm,
Yarn 1 and Bun link without writing the manifest or the lockfile: the report names
the package and where its link leads, and a manager that exits 0 without linking
anything is a failure, not already up to date. uf unlink undoes either
direction. It takes a link out of the project and installs what the manifest
declares in its place, or removes what uf link registered. When there is
nothing to unlink, it says what it found and exits 0. uf dedupe collapses the
versions your ranges allow to be one, and uf dedupe --check names what it
would collapse, changes nothing, and exits non-zero when there is anything —
which is the form for CI.
Asking about the tree
uf why, uf ls and uf audit hand the question to the manager that resolved
the tree, so the answer is about what was actually installed:
$ uf why is-number
manager npm
chosen by package-lock.json
command npm explain is-number
is-number@6.0.0 dev
node_modules/is-number
dev is-number@"^6.0.0" from the root project
They take package names and nothing else. A manager's own flags are that manager's
surface, and uf cannot tell which of them only read — pnpm audit --fix rewrites a
lockfile — so a flag after a name is refused:
$ uf ls react --depth=0
error: "--depth=0" is not a package name: it starts with `-`, which the package manager would read as a flag; write the package name, or `./--depth=0` for a path
uf audit exits 1 when the manager reports an advisory. A project uf new has
just written already reports three high-severity ones today, all in toml, which
arrives through @uniflowed/vite
(#1009).
uf search is npm's and pnpm's. Yarn and Bun have no registry search, and uf says
so rather than running a manager the project did not choose:
error: bun has no `search`: npm and pnpm can search the registry; `uf exec --yes npm search` runs npm's without changing what this project installs with
| Command | npm | pnpm | Yarn 1 | Yarn 2+ | Bun |
|---|---|---|---|---|---|
uf install | npm install | pnpm install | yarn install | yarn install | bun install |
uf install --prod | npm install --omit=dev | pnpm install --prod | yarn install --production | yarn workspaces focus --all --production | bun install --omit=dev |
uf add | npm install | pnpm add | yarn add | yarn add | bun add |
uf remove | npm uninstall | pnpm remove | yarn remove | yarn remove | bun remove |
uf update | npm update | pnpm update | yarn upgrade | yarn up | bun update |
uf dedupe | npm dedupe | pnpm dedupe | refused | yarn dedupe | refused |
uf dedupe --check | npm dedupe --dry-run --json | pnpm dedupe --check | refused | yarn dedupe --check | refused |
uf link DIR | npm link | pnpm link | refused | yarn link | refused |
uf link NAME | npm link | pnpm link on pnpm 10; pnpm 12 refuses it | yarn link | refused | bun link |
uf unlink NAME | npm uninstall --no-save | pnpm unlink | yarn unlink | yarn unlink | uf removes the link |
uf why | npm explain | pnpm why | yarn why | yarn why | bun why |
uf ls | npm ls | pnpm list | yarn list | yarn info --all | bun pm ls |
uf audit | npm audit | pnpm audit | yarn audit | yarn npm audit | bun audit |
uf info | npm view | pnpm view | yarn info | yarn npm info | bun info |
uf search | npm search | pnpm search | refused | refused | refused |
uf patch | refused | pnpm patch | refused | yarn patch | refused |
uf info react prints the registry's metadata for a package, and
uf info react version one field of it.
Updating
uf update with no flag is the manager's own update — every package moves to the
newest version its range already allows, and package.json is not touched —
followed by a report of what is newer than the ranges permit. --dry-run is the
report alone:
$ uf update --dry-run
outside the range
package declared newest step
is-number ^6.0.0 7.0.0 major
› 1 dependency is newer than its range allows
› uf update --latest rewrites the ranges; --minor and --patch cap the step
--latest, --minor and --patch rewrite the ranges as far as that step and then
install, keeping the comparator the project chose — ^6.0.0 becomes ^7.0.0. Only
^x.y.z, ~x.y.z, x.y.z and >=x.y.z are rewritten; workspace:, catalog:,
npm:, a URL, and a range with more than one comparator are counted and left
alone. Name packages to move only those.
uf reads the registry for that report itself, over HTTPS, from pm.registry in
uf.config.js — not from .npmrc, which is the manager's.
Workspaces, catalogues and patches
uf add#web react runs the command in the workspace member named web, by its
package name or its path, and uf add react --filter web --filter 'api...' in
every member the selectors pick — see Adding and removing.
A workspace's members are what package.json#workspaces, pnpm-workspace.yaml
and the members' own uf.config.js say they are. uf catalog lists the packages more than one manifest
declares and the ones whose manifests disagree about the range, and
uf catalog set NAME RANGE makes every manifest that declares the package agree.
uf patch PACKAGE opens a copy of a dependency to edit and uf patch --commit DIR
writes the patch and installs — on pnpm and Yarn 2+, the two managers with a patch
command; on the others uf names patch-package rather than installing it for you.
Registries and provenance
pm.scopes binds a scope to a registry for the reads uf makes itself, and
uf install refuses a package-lock.json that resolved a bound scope's package
from somewhere else — the shape of a dependency-confusion attack. The manager
still resolves with its own configuration, so a scope has to be bound in .npmrc
as well; uf never reads or sends a token, and a registry that needs one cannot
answer uf's own reads. The keys are in the
configuration reference.
With pm.provenance at its default, report, uf install reads the npm
attestation of each package it added or updated, from an npm lockfile, up to 250
per install: the statement has to name the package and the SHA-512 in the
lockfile, and a mismatch stops the install. The Sigstore signature itself is not
verified. On some machines a package with no attestation is counted as unknown
rather than unattested (#1005),
which is what the install above shows.
What is not here
- Complete external-manager compatibility in native mode. Native installs support registry ranges, aliases, local directories and workspace links. Peer packages bind to compatible root declarations; separate nested peer contexts, git/URL dependencies and patch protocols need an external manager.
- Authenticated registry reads in native mode. Tokens are not loaded from
.npmrc; a registry that requires credentials reports a failure. - Native package provenance signatures. Native downloads verify SRI, not Sigstore signatures. The attestation report above applies to npm installs.
--jsonon every package command. Native queries anduf audit --jsonprovide JSON; install output remains a terminal report.- The manager's exit status. A manager that fails makes uf exit 1, whatever it exited with.
- A speed guarantee for every dependency graph. CI measures repeated installs and shared disk usage on fixed fixtures; project graphs and network conditions change the results.
- Publishing.
uf publishwrites a plan to.uf/publish.jsonand publishes nothing.
Where to go next
Environments is next: which Node.js, Bun or package manager a
project runs on, pinned. Tasks is where a scripts block goes
instead, uf in CI is where uf install --frozen-lockfile goes and why
it goes first, and the command reference has every
flag.
Edit this pagedocs/app/guide/dependencies/$page.mdx