Documentation menu

Dependency resolution and flog.lock

Flog resolves npm registry and workspace dependencies into a versioned strict JSON lockfile. Resolution is deterministic: package names, versions, dependency edges, peer checks, diagnostics, and JSON map keys are processed in stable order. The canonical encoding uses two-space indentation and one trailing newline.

Schema#

The root version identifies the lockfile schema and is currently 0. Flog is not yet released, so this is the first and only supported schema. A lock contains:

  • targets: explicitly represented npm-style OS, CPU, and optional libc targets. Each target references the root (.) and every workspace importer snapshot and records only package-snapshot overrides that differ by target;
  • importers: shared importer snapshots with original dependency specifiers preserved by dependency field, exact package resolutions, and derived peerProviders edges;
  • workspaces: local package names, versions, and repository-relative link paths; and
  • packages: shared immutable registry snapshots containing version, tarball URL, integrity, regular, optional, and derived peer-provider edges, peer constraints and metadata, effective peer context, resolution context, and platform restrictions.

Identical importer and package snapshots are encoded once. Package membership is reconstructed from each target's importer roots and dependency edges. If the same importer path or logical package ID has target-specific edges, each distinct snapshot receives a deterministic content-hash suffix and only the affected target references that variant. This keeps common graph data shared while preserving optional selection and peer-provider differences exactly.

An importer or package edge stores both its original specifier and exact target. Aliases therefore retain values such as npm:typescript@^7.0.2 while resolving to the actual typescript snapshot. Workspace edges use workspace:<relative-path> and never query the registry.

Registry snapshot IDs include a deterministic SHA-256 context suffix only when an ancestor peer provider can affect that package or one of its descendants. This prevents two otherwise identical package versions from sharing incorrect child edges under conflicting peer environments, while packages without peer context retain the readable name@version identity.

Tarball URLs are required to be HTTP(S), may not contain user information, and are recorded without query strings or fragments. Tokens and machine-specific cache paths are never written to the lock.

Selection behavior#

For each registry range, flog chooses the highest valid npm SemVer version that satisfies the range. npm prerelease range semantics come from the shared SemVer implementation. A registry dist-tag such as latest or next selects the exact version named by that package's dist-tags metadata; the lock records that version, so locked and offline installs do not silently follow later tag changes. npm: aliases select ranges or tags from their actual package name. workspace: dependencies must target a discovered local package and explicit ranges must accept its local version.

Production dependencies take precedence over duplicate development dependencies, and optional dependencies override regular dependencies with the same name. Overridden importer entries retain their original specifier with a shadowed marker, while only the effective field carries a resolution. Development dependencies are resolved for every importer. Registry package development dependencies are not installed.

Peer dependencies are validated against deterministic parent/ancestor provider sets. Explicit providers take precedence. When a required provider is absent, flog selects the highest registry version satisfying every peer range in that environment, or the matching local workspace for a workspace: peer, and records its exact placement in peerProviders. Incompatible explicit providers and unsatisfiable shared constraints fail with stable importer, consumer, range, and dependency-chain diagnostics. Peers marked optional in peerDependenciesMeta may be absent and are not automatically installed, but an installed provider must still satisfy their range. Peer-sensitive transitive graphs get distinct package snapshots.

Locked validation reconstructs each environment's required-peer closure from its resolved declared consumers. Every derived provider must be reached by that closure, with providers allowed to introduce further required peers; orphan providers and self-supporting orphan peer cycles are rejected rather than materialized.

os, cpu, and libc metadata supports positive entries, negated entries, and any. An incompatible required package fails resolution. An incompatible or unavailable optional package remains in its dependency field with a stable skipped reason and no package snapshot. Optional providers are stabilized before required peer decisions; if an optional subtree is skipped, it cannot satisfy a peer and every dependent peer check is recomputed without it.

Locked workflow#

Commit the generated lock and use plain flog install in development and CI. An existing lock is parsed strictly, checked against every workspace manifest and the current target, and installed without registry metadata requests or lockfile writes. A missing lock, stale lock, or unsupported target fails rather than being silently created or reconciled.

Dependency changes are deliberate: flog add, flog remove, and flog upgrade update manifests and the exact lock together. After a manual manifest edit, use flog lock to preserve compatible locked versions and write the reconciled graph without installing it.

Target matrices#

Declare the complete portable target matrix while explicitly generating the lock. Repeat --target in npm-style os/cpu[/libc] form:

flog lock \
  --target linux/x64/glibc \
  --target linux/x64/musl \
  --target linux/arm64/glibc \
  --target linux/arm64/musl \
  --target darwin/x64 \
  --target darwin/arm64 \
  --target win32/x64 \
  --target win32/arm64

amd64 is accepted as an alias for x64, and windows as an alias for win32. Linux targets must declare glibc or musl; other operating systems must omit libc. An explicit matrix replaces the previous declaration, so the same command converges to byte-identical output on every host. Without explicit targets, lock reconciliation preserves and refreshes the existing matrix. Named dependency installs and flog upgrade also resolve every represented target before replacing manifests or the lock; they never collapse the matrix to the invoking host. A failure in any target leaves all project files unchanged.

Plain flog install selects only the current target graph. A represented target does not access registry metadata or rewrite the lock; an unsupported target fails and lists those represented. Downloads, offline cache checks, bins, and lifecycle ordering are limited to packages reachable from the selected graph. flog lock may generate any complete matrix from any host.

Only an explicit integer "version": 0 is accepted. A missing or null version is not interpreted as zero, and no earlier development schema is read or rewritten by install or lock.

Before the version-zero decision, development builds briefly wrote target-matrix locks labeled version 2. That matrix shape is the current version-zero shape, so the coordinated migration is an operator-reviewed one-line relabel: retain a backup, change only the root version from 2 to 0, verify that this is the only diff, and then run plain flog install for each represented target. Do not use Flog to rewrite the file. Earlier development locks labeled version 1 used a different single-target shape and must instead be backed up, removed, and regenerated with an explicit complete --target matrix; review every resolution against the backup before committing it.

Unknown lockfile fields, unsupported versions, incomplete records, dangling edges, and additional JSON values are rejected. Default mode reports the exact importer field that differs from package.json and directs the operator to flog lock.