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:
forest i <scope>/<name>resolves the package, writes it into thedependenciesobject offorest.json(as^<installed-version>), and updatesforest-lock.json.forest publishuploads thedependenciesobject exactly as it appears inforest.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:
| Syntax | Meaning |
|---|---|
^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.3 | Exactly 1.2.3 |
>=1.2, <1.5 | Comma-separated comparators, all must hold |
1.2.* | Any patch of 1.2 |
"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:
- Update within your ranges.
forest updatemoves every dependency, direct and transitive, to the newest version its declared range allows, then prints what moved.forest.jsonis untouched, so a^1.4.0range can pick up1.6.2but never2.0.0. This is the safe, routine refresh. - See what’s beyond your ranges.
forest audit(aliasforest 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. - Cross them deliberately.
forest audit --updaterewrites each outdated dependency’s range inforest.jsonto^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 withforest 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/signalcan be satisfied by one version, exactly one copy is installed and shared. -
Incompatible ranges are where platforms differ. If package A needs
^1.0.0and 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.