Versioning
Versioning
pkgist computes the next version per package from a strategy you pick, then writes it back to the source package.json before building.
The five strategies
| Strategy | What it does |
|---|---|
"auto" (default) | Bump the patch digit. 2.1.0 → 2.1.1 |
"patch" | Identical to "auto". Bump the patch digit |
"minor" | Bump minor, reset patch. 2.1.0 → 2.2.0 |
"major" | Bump major, reset minor + patch. 2.1.0 → 3.0.0 |
| Literal semver string | Use exactly this version. version: "3.0.0" → 3.0.0 |
Pre-release versions follow the same rules (semver-internal):
"patch"on2.1.0-beta.1→2.1.0(drops pre-release)- An explicit
"2.1.0-beta.2"is also valid
Overriding per run from the CLI
The same five strategies are available as a per-run CLI override via --bump, so you don’t have to edit the version field in pkgist.config.ts for every release:
pkgist build @scope/utils --bump minor # ignore config version, minor-bump this runpkgist build:family atom --bump 6.1.0 # set the family's shared version explicitlypkgist build:all --bump patch # patch-bump every package this run--bump <strategy> takes the identical values to the config version field (auto / patch / minor / major / a literal semver) and overrides it for that invocation only — the config file is never mutated. For a standalone build it applies to every targeted package; for a family build it replaces the family’s single shared version (the highest-current-member rule still applies when the strategy is a bump keyword). When --bump is omitted, the config version (or its "auto" default) is used.
Named
--bump, not--version, because commander reserves--versionforpkgist --version. See thecliskill’s Per-run overrides section, andgit-workflowfor the paired--commitflag.
Standalone packages
{ name: "@scope/utils", root: "../utils", version: "auto" }// current: 2.1.0 → next: 2.1.1
{ name: "@scope/utils", root: "../utils", version: "minor" }// current: 2.1.0 → next: 2.2.0
{ name: "@scope/utils", root: "../utils", version: "3.0.0" }// any current → next: 3.0.0Family packages
Families synchronize. The family picks the highest current version across all members, then bumps it. All members land on the same new version.
// Members' current versions on disk:// atom = 1.0.5// react-atom = 5.1.3 ← highest// atomic-query = 0.1.0
families: [ { name: "atom-family", version: "patch", packages: [...] }]// → all three become 5.1.4This means adding a new low-versioned package to a family is intentional surgery: it jumps to match the family on the first build. The family version is the compatibility contract — if you publish react-atom@6.0.0, every other family member must also be 6.0.0 so consumers can lock to one version.
Strategy on the family, not on members
version is family-level only. Family-member entries don’t accept their own version — it’s whatever the family decided.
families: [ { name: "atom-family", version: "minor", // ← family-level packages: [ { name: "@scope/atom", root: "../atom" }, // no version { name: "@scope/react-atom", root: "../react-atom" }, // no version ], },]Intra-family dependency pinning
In a monorepo, sibling packages reference each other with "*" so the workspace links them to the local copy:
// @scope/react-atom — SOURCE package.json"dependencies": { "@scope/atom": "*" }"*" must never reach npm — it lets a consumer resolve the sibling to any published version, which breaks the “whole family on one exact version” contract and risks pulling an incompatible release. So during a family build, pkgist rewrites every dependencies / peerDependencies entry whose name is another family member to the exact shared release version in the published package.json. The source file is never mutated — it keeps "*" for workspace linking (a fresh object is written for the build).
// @scope/react-atom — PUBLISHED package.json, family released at 5.1.4"dependencies": { "@scope/atom": "5.1.4" } // not "*", not "^5.1.4" — exactRules:
- Family builds only. Standalone builds (
build/ the standalone half ofbuild:all) never pin — their deps publish verbatim. - Applies to
dependenciesandpeerDependencies. (devDependenciesare dropped from the build entirely;optionalDependenciesare not carried into the publishedpackage.json.) - Only family-member names are rewritten. Every non-member dependency keeps its original range exactly as written.
- The pin is the exact shared version string (e.g.
5.1.4) with no^/~— so an installed family resolves to one coherent, same-version set.
This is what makes the family version a true compatibility contract: members share it, and their cross-references resolve to it.
Picking the right strategy
This is a semantic question, not a pkgist one. The convention:
| When changes are | Use |
|---|---|
| Bug fixes, internal refactors, docs, lockfile/CI updates with NO API surface change | "patch" |
| Strictly additive: new exports, new optional params, new fields on result types | "minor" |
| Anything a consumer’s code would have to change to absorb (removals, renames, signature changes, default-behavior flips, runtime deps bumped major) | "major" |
For families specifically: pick based on the most significant change in any member. If one family member made a breaking change, the family bumps major even if the others only had docs updates.
Explicit strings for unusual paths
The literal-string form unlocks:
- First 1.0.0 release:
version: "1.0.0"(rather than bumping 0.x.x) - Re-publishing after a botched release:
version: "2.1.3"to overwrite intent (only works if npm hasn’t seen this version — never use to “force overwrite” a published version, which npm prevents anyway) - Pre-releases:
version: "2.0.0-beta.1", then"2.0.0-beta.2", then"2.0.0"for the final cut - Hotfix lineage:
version: "1.2.3"to ship a patch on an old major if you’ve moved past it