Skip to content

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

StrategyWhat it does
"auto" (default)Bump the patch digit. 2.1.02.1.1
"patch"Identical to "auto". Bump the patch digit
"minor"Bump minor, reset patch. 2.1.02.2.0
"major"Bump major, reset minor + patch. 2.1.03.0.0
Literal semver stringUse exactly this version. version: "3.0.0"3.0.0

Pre-release versions follow the same rules (semver-internal):

  • "patch" on 2.1.0-beta.12.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:

Terminal window
pkgist build @scope/utils --bump minor # ignore config version, minor-bump this run
pkgist build:family atom --bump 6.1.0 # set the family's shared version explicitly
pkgist 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 --version for pkgist --version. See the cli skill’s Per-run overrides section, and git-workflow for the paired --commit flag.

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.0

Family 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.4

This 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" — exact

Rules:

  • Family builds only. Standalone builds (build / the standalone half of build:all) never pin — their deps publish verbatim.
  • Applies to dependencies and peerDependencies. (devDependencies are dropped from the build entirely; optionalDependencies are not carried into the published package.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 areUse
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