Skip to Content
ConceptsDependencies & Versioning

Dependencies & Versioning

This page defines exactly how Forest records, resolves, and versions dependencies. These rules are the same on every platform; where a package physically lands on disk is platform-specific and covered per platform under Roblox and UEFN.

How dependencies are recorded

Dependencies are read from your forest.json, nothing else. forest publish does not scan your source for imports:

  1. forest i <scope>/<name> resolves the package, writes it into the dependencies object of forest.json (as ^<installed-version>), and updates forest-lock.json.
  2. forest publish uploads the dependencies object exactly as it appears in forest.json.

So “automatic dependency detection” at publish time means: whatever you installed (and didn’t remove) is a dependency. If you hand-edit forest.json to add or change an entry, that’s honored too. Run forest install afterward to re-resolve.

Version ranges

Dependency versions are ranges, not exact pins, following SemVer . The default written by forest i is a caret range:

SyntaxMeaning
^1.2.3≥ 1.2.3 and < 2.0.0, compatible updates (the default)
~1.2.3≥ 1.2.3 and < 1.3.0, patch updates only
=1.2.3Exactly 1.2.3
>=1.2, <1.5Comma-separated comparators, all must hold
1.2.*Any patch of 1.2
A bare version like "1.2.3" is treated as ^1.2.3 (caret semantics), not an exact pin. Use =1.2.3 if you need to pin exactly.

Exact ranges (=) are how a set of related packages can be kept in lockstep, though if two packages must always move together, they usually want to be one package with one root instead.

Actual installed versions are pinned by forest-lock.json; ranges only matter when the lockfile is (re)generated. To move pinned versions forward, see Updating dependencies below.

Updating dependencies

Installed versions never change behind your back: forest install reproduces the lockfile’s pinned tree. Updating is an explicit step, with two tools depending on how far you want to move:

  1. Update within your ranges. forest update moves every dependency, direct and transitive, to the newest version its declared range allows, then prints what moved. forest.json is untouched, so a ^1.4.0 range can pick up 1.6.2 but never 2.0.0. This is the safe, routine refresh.
  2. See what’s beyond your ranges. forest audit (alias forest outdated) compares, for every direct dependency, the version in your lockfile (Current), the highest version your declared range allows (Wanted), and the newest published version (Latest). It also reports license considerations across your resolved tree.
  3. Cross them deliberately. forest audit --update rewrites each outdated dependency’s range in forest.json to ^latest, then re-resolves and reinstalls. This crosses major-version boundaries, so read the report first: a major bump means the author declared a breaking change. Pass a package name to do it for one dependency, or move to a specific version with forest i <scope/name> -v <version>.

Two related tools for when an upstream release is broken: forest override forces a transitive dependency onto a range you choose, and forest exclude bans specific versions from resolution entirely.

Conflict resolution: dedupe first

When the same package appears more than once in the dependency graph, Forest deduplicates wherever it can:

  • Compatible ranges are deduplicated. If every requirement on forest/signal can be satisfied by one version, exactly one copy is installed and shared.

  • Incompatible ranges are where platforms differ. If package A needs ^1.0.0 and package B needs ^2.0.0, there is no single version that satisfies both. How that is handled depends on the platform:

    • Roblox installs both versions side by side, each nested where its dependent can reach it. Nothing fails, but you pay in duplicate code, and instances of “the same” class from the two copies won’t be shared. See Dependency layout on disk.
    • UEFN installs exactly one version per package (Verse module paths carry no version), so an unsatisfiable conflict is a hard error naming the requesters. Align the ranges or drop one. See Installing & Importing.

Either way, prefer depending on compatible ranges when you can.

Circular dependencies

Circular dependencies are not rejected, neither at publish nor at install time, and the resolver handles them without looping. In practice a cycle can only be created retroactively: you can only depend on versions that are already published, so A → B → A requires publishing A, then B depending on A, then a new version of A depending on B.

Treat cycles as a design smell rather than a tool feature: if two packages genuinely need each other, they are one package. Give it a single entry point that exposes both surfaces (see Recipe: Server & Client APIs).

Versioning your own package

forest publish manages your version number for you. It asks what kind of change you made (bugfix, new feature, or breaking change) and bumps patch, minor, or major accordingly, plus a follow-up question to catch accidental breaking changes. You can enter a version manually, but this is discouraged: consumers on caret ranges rely on your version numbers meaning what SemVer says they mean.

  • First publish defaults to 0.1.0.
  • A version, once published, can never be reused.
  • Because consumers default to ^, a major bump is the only bump existing users won’t pick up automatically, which is the expected behavior for breaking changes.
Last updated on