Versioning and Dependency Resolution
df-cli uses Semantic Versioning (semver) for all package versions and supports a rich constraint expression syntax for declaring compatible version ranges.
Version Format
A version follows the semver MAJOR.MINOR.PATCH format, optionally followed by a pre-release tag and build metadata:
1.2.3
1.2.3-beta.1
1.2.3+build.456
1.2.3-rc.1+build.456
| Component | Description |
|---|---|
MAJOR |
Incremented for incompatible API changes. |
MINOR |
Incremented for backwards-compatible new functionality. |
PATCH |
Incremented for backwards-compatible bug fixes. |
| Pre-release | Optional. Appended after - (e.g. -beta.1, -rc.2). Pre-release versions have lower precedence than the associated normal version. |
| Build metadata | Optional. Appended after + (e.g. +build.456). Informational only and ignored during version matching. |
Versions can optionally include a leading v prefix (v1.2.3), which is stripped during parsing. Incomplete versions are normalized with zeros: 1 becomes 1.0.0, 1.2 becomes 1.2.0.
Version Constraint Expressions
When declaring a dependency, you can specify a version constraint that defines which versions of that package are acceptable. Constraints are specified in the dependencies section of the workspace .sws file or as part of a package string on the command line.
Exact Version
Pins to a single version. No other version will match.
1.2.3
=1.2.3
Caret Ranges (^)
Allow changes that do not modify the left-most non-zero digit. This is the most common constraint type — it allows updates that are expected to be backwards-compatible.
| Expression | Expands to | Allows |
|---|---|---|
^1.2.3 |
>=1.2.3 <2.0.0 |
Minor and patch updates |
^0.2.3 |
>=0.2.3 <0.3.0 |
Patch updates only (major is 0, so minor is significant) |
^0.0.3 |
>=0.0.3 <0.0.4 |
No updates (major and minor are 0) |
When to use: You trust the publisher to follow semver — breaking changes only happen in new major versions.
Tilde Ranges (~)
Allow patch-level changes only. More restrictive than caret ranges.
| Expression | Expands to | Allows |
|---|---|---|
~1.2.3 |
>=1.2.3 <1.3.0 |
Patch updates only |
~1.2 |
>=1.2.0 <1.3.0 |
Same as ~1.2.0 |
~1 |
>=1.0.0 <2.0.0 |
Same as ~1.0.0 |
When to use: You want bug fixes but not new features that could introduce subtle behavior changes.
Comparison Operators
Explicit bounds for fine-grained control.
| Operator | Example | Meaning |
|---|---|---|
>= |
>=1.0.0 |
Version 1.0.0 or higher |
> |
>1.0.0 |
Strictly above 1.0.0 |
<= |
<=2.0.0 |
Version 2.0.0 or lower |
< |
<2.0.0 |
Strictly below 2.0.0 |
Combine operators to define a range with both lower and upper bounds:
>=1.2.0 <2.0.0
OR Expressions (||)
Match if any of the listed constraints is satisfied. Useful when a package works with multiple major versions.
^1.0.0 || ^2.0.0
1.0.0 || 2.0.0 || 3.0.0
Pre-release Versions
Pre-release tags (e.g. -beta.1, -rc.1) are included in constraint matching only when the constraint itself references a pre-release:
>=1.0.0-rc.1 # matches 1.0.0-rc.1, 1.0.0-rc.2, 1.0.0, 1.1.0, ...
>=1.0.0 # matches 1.0.0, 1.1.0, ... but NOT 1.0.0-rc.1
Dependency Resolution
When the workspace is loaded, df-cli builds a complete dependency tree by resolving every declared dependency to a concrete version. Dependencies can come from the package repository at packages.dataflex.dev, from a Git URL, or from a local path.
Resolution Order
For each dependency, df-cli tries the following sources in order:
- Already loaded — If the dependency was already resolved earlier in the tree, reuse that version (if compatible).
- Lock file — If a
.sws.lockfile exists and contains a pinned version for this dependency, use it (if it still satisfies the constraint). - Fixed version — If the constraint is an exact version rather than a range, use it directly.
- Fetch latest — Query the package repository for the latest version that satisfies the constraint, filtered by the workspace's DataFlex version.
Shared Dependencies and Constraint Intersection
Packages in the DataFlex ecosystem can depend on each other and form a dependency tree. When multiple packages in the tree depend on the same shared package, their version constraints must be compatible.
Consider this scenario:
MyApp (workspace)
+-- Charts ^1.0.0
| +-- Utils ~1.2.0
+-- Utils ^1.0.0
MyApp depends on both Charts and Utils. Charts also depends on Utils, but with a different constraint. df-cli must find a single version of Utils that satisfies both constraints simultaneously.
How constraint intersection works:
- Collect all constraints on
Utils: MyApprequires^1.0.0(meaning>=1.0.0 <2.0.0)-
Chartsrequires~1.2.0(meaning>=1.2.0 <1.3.0) -
Extract the bounds from each constraint and intersect them:
- Lower bound: the highest minimum →
max(1.0.0, 1.2.0)= 1.2.0 -
Upper bound: the lowest maximum →
min(2.0.0, 1.3.0)= 1.3.0 -
The effective range for
Utilsis>=1.2.0 <1.3.0. df-cli fetches the latest version in this window (e.g.1.2.5).
If no version exists that satisfies all constraints, df-cli reports an incompatibility error and aborts the operation.
Deeper Diamond Dependencies
The same logic applies to arbitrarily deep dependency trees. Consider:
MyApp (workspace)
+-- UI ^2.0.0
| +-- Core ~2.1.0
+-- API ^2.0.0
| +-- Core ^2.0.0
+-- Core ^2.2.0
Three different packages all depend on Core with different constraints:
- UI requires ~2.1.0 → >=2.1.0 <2.2.0
- API requires ^2.0.0 → >=2.0.0 <3.0.0
- MyApp requires ^2.2.0 → >=2.2.0 <3.0.0
Intersection:
- Lower bound: max(2.1.0, 2.0.0, 2.2.0) = 2.2.0
- Upper bound: min(2.2.0, 3.0.0, 3.0.0) = 2.2.0
The only version that satisfies all three constraints is exactly >=2.2.0 <2.2.0 — which is impossible. This is a conflict, and df-cli will report an incompatibility error. The fix would be for UI to relax its constraint on Core (e.g. change ~2.1.0 to ^2.1.0).
Lock File
The lock file ensures that every developer and automated environment uses the exact same dependency versions, even when version ranges would allow newer releases.
Format
The lock file is named <workspace>.sws.lock (e.g. MyProject.sws.lock) and sits next to the workspace file. It contains the resolved version for every dependency:
{
"lockVersion": "1.0.0",
"dependencies": {
"DAE/Charts": {
"ref": "1.2.5"
},
"DAE/Utils": {
"ref": "1.2.3"
}
}
}
Behavior
- On install / load: If a lock file exists, its pinned versions are used as long as they still satisfy the constraints in the
.swsfile. This prevents unexpected version drift. - On upgrade: The lock file is updated to reflect the newly resolved versions after a successful upgrade.
- On conflict: If a locked version no longer satisfies a changed constraint (e.g. you updated the constraint in your
.swsfile), the lock is ignored for that dependency and a new version is fetched. - Missing lock file: Not an error. Dependencies are resolved normally and a lock file is written after the first successful resolution.
When to Commit the Lock File
The lock file should be committed to version control. This ensures that:
- All team members work against the same dependency versions.
- Automated builds are reproducible.
- Upgrading is an explicit action (df-cli package upgrade), not something that happens silently.
Version Constraints by Source Type
| Source Type | Constraint Support | Example |
|---|---|---|
| Repository (SRV) | Full semver range expressions | ^1.0.0, ~2.1, >=1.0.0 <3.0.0, ^0.x\|\|^1.x |
| Git | Semver range expressions, branch names, commit hashes | ^1.0.0, >=1.0.0, main, abc123… |
| Local | No versioning | Always uses the workspace as-is |
For Git dependencies, range expressions are matched against the semver-compatible tags fetched from the remote repository. The latest tag that satisfies the expression is selected and checked out. Branch names and commit hashes are also accepted and are treated as exact refs.
Constraint Behaviour Reference
Resolution — what add() / install() stores
| Constraint | Example | GIT | SRV |
|---|---|---|---|
| Exact semver tag | 1.0.0 |
Checks out that tag; sws stores 1.0.0 |
Downloads exact version |
Tilde ~ |
~1.0.0 |
Tag search >=1.0.0 <1.1.0; sws stores resolved tag |
webapi receives min/max window |
Caret ^ |
^1.0.0 |
Tag search >=1.0.0 <2.0.0; sws stores resolved tag |
webapi receives min/max window |
Lower bound >= |
>=1.0.0 |
Tag search >=1.0.0; sws stores resolved tag |
webapi receives only min-version |
Upper bound <= |
<=1.0.1 |
Tag search <=1.0.1; sws stores resolved tag |
webapi receives only max-version |
Strict lower > |
>1.0.0 |
Tag search >=1.0.1 (incremented); sws stores resolved tag |
webapi receives min-version=1.0.1 |
Strict upper < |
<2.0.0 |
Tag search, upper bound decremented; sws stores resolved tag | webapi receives decremented max-version |
| OR (overlapping) | ^1.0.0\|\|^2.0.0 |
Union window >=1.0.0 <3.0.0; tag search picks latest in window |
webapi receives union window |
| OR (disjoint exact) | 1.0.0\|\|2.0.0 |
Not supported — min/max window inverts | Not supported |
| Branch name | main |
Resolves to HEAD commit hash; sws stores branch name | N/A |
| Commit hash | abc123… |
Checks out that commit; sws stores hash | N/A |
update() — detecting newer versions
| sws constraint | GIT | SRV |
|---|---|---|
| Exact semver tag | Finds latest semver tag satisfying the tightest window across all dependents | Queries webapi with tightest window |
Expression (~, ^, >=, etc.) |
Tag search within tightest window across all dependents | webapi call with tightest window |
| OR (overlapping) | Same as expression — union window used | Same |
| Branch name | Compares HEAD commit hash of branch against lock file | N/A |
| Commit hash | Always UP_TO_DATE — pinned forever | N/A |
upgrade() — how the sws constraint is updated
| Condition | sws after upgrade |
|---|---|
Expression still covers new version (satisfies_range) |
Expression preserved; only lock file advances |
GIT branch dep (sws = branch name, old_dep->version = commit hash) |
Branch name preserved; new commit hash written to lock |
| GIT tag dep (sws = old semver tag, neither side is a commit hash) | sws updated to new semver tag |
| SRV exact version | sws updated to new version string |