Skip to content
Draft
6 changes: 6 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Mops CLI Changelog

## Next
- Add version range support for dependencies: caret (`^1.2.3`) and tilde (`~1.2.3`) operators
- `mops add` now defaults to caret range (e.g. `mops add core` writes `core = "^1.2.3"`)
- `mops add core@1.2.3` still pins an exact version
- `mops update` preserves range type when bumping versions
- Resolver verifies that the resolved version satisfies all transitive range constraints, and reports a warning/error otherwise
- `mops publish` warns when publishing a package with version ranges in dependencies (older mops CLI versions cannot install such packages)

## 2.12.3
- Fix `mops install --lock update` silently no-op'ing on a corrupt lockfile (#515)
Expand Down
5 changes: 5 additions & 0 deletions cli/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ export let copyCache = (cacheName: string, dest: string) => {
});
};

export function listCachedPackages(): string[] {
let packagesDir = path.join(getGlobalCacheDir(), "packages");
return fs.existsSync(packagesDir) ? fs.readdirSync(packagesDir) : [];
}

export let cacheSize = async () => {
let dir = path.join(getGlobalCacheDir());
fs.mkdirSync(dir, { recursive: true });
Expand Down
2 changes: 1 addition & 1 deletion cli/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export async function add(
console.log(chalk.red("Error: ") + versionRes.err);
return;
}
ver = versionRes.ok;
ver = "^" + versionRes.ok;
}

pkgDetails = {
Expand Down
67 changes: 50 additions & 17 deletions cli/commands/available-updates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { mainActor } from "../api/actors.js";
import { Config } from "../types.js";
import { getDepName, getDepPinnedVersion } from "../helpers/get-dep-name.js";
import { SemverPart } from "../declarations/main/main.did.js";
import { isRange, stripRangePrefix, rangeToSemverPart } from "../semver.js";
import { checkLockFileLight, readLockFile } from "../integrity.js";

// [pkg, oldVersion, newVersion]
export async function getAvailableUpdates(
Expand Down Expand Up @@ -35,28 +37,59 @@ export async function getAvailableUpdates(
return "";
};

let actor = await mainActor();
let res = await actor.getHighestSemverBatch(
depsToUpdate.map((dep) => {
let semverPart: SemverPart = { major: null };
let name = getDepName(dep.name);
let pinnedVersion = getDepPinnedVersion(dep.name);
if (pinnedVersion) {
semverPart =
pinnedVersion.split(".").length === 1
? { minor: null }
: { patch: null };
let lockedDeps: Record<string, string> = {};
if (checkLockFileLight()) {
let lockFileJson = readLockFile();
if (lockFileJson && lockFileJson.version === 3) {
lockedDeps = lockFileJson.deps;
}
}

let batchItems: Array<[string, string, SemverPart]> = [];
for (let dep of depsToUpdate) {
let name = getDepName(dep.name);
let version = dep.version || "";

if (isRange(version)) {
let part = rangeToSemverPart(version);
if (part) {
batchItems.push([name, stripRangePrefix(version), part]);
}
return [name, dep.version || "", semverPart];
}),
);
continue;
}

let semverPart: SemverPart = { major: null };
let pinnedVersion = getDepPinnedVersion(dep.name);
if (pinnedVersion) {
semverPart =
pinnedVersion.split(".").length === 1
? { minor: null }
: { patch: null };
}
batchItems.push([name, version, semverPart]);
}

if (batchItems.length === 0) {
return [];
}

let actor = await mainActor();
let res = await actor.getHighestSemverBatch(batchItems);

if ("err" in res) {
console.log(chalk.red("Error:"), res.err);
process.exit(1);
}

return res.ok
.filter((dep) => dep[1] !== getCurrentVersion(dep[0], dep[1]))
.map((dep) => [dep[0], getCurrentVersion(dep[0], dep[1]), dep[1]]);
let updates: Array<[string, string, string]> = [];
for (let [name, highestVersion] of res.ok) {
let currentConfigVer = getCurrentVersion(name, highestVersion);
let currentResolved = isRange(currentConfigVer)
? lockedDeps[name] || stripRangePrefix(currentConfigVer)
: currentConfigVer;
if (currentResolved !== highestVersion) {
updates.push([name, currentConfigVer, highestVersion]);
}
}
return updates;
}
26 changes: 25 additions & 1 deletion cli/commands/install/install-mops-dep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import chalk from "chalk";
import { deleteSync } from "del";
import { checkConfigFile, progressBar, readConfig } from "../../mops.js";
import { getHighestVersion } from "../../api/getHighestVersion.js";
import { storageActor } from "../../api/actors.js";
import { mainActor, storageActor } from "../../api/actors.js";
import { parallel } from "../../parallel.js";
import {
getDepCacheDir,
Expand All @@ -20,6 +20,7 @@ import {
} from "../../api/downloadPackageFiles.js";
import { installDeps } from "./install-deps.js";
import { getDepName } from "../../helpers/get-dep-name.js";
import { isRange, stripRangePrefix, rangeToSemverPart } from "../../semver.js";

type InstallMopsDepOptions = {
verbose?: boolean;
Expand Down Expand Up @@ -67,6 +68,29 @@ export async function installMopsDep(
return false;
}
version = versionRes.ok;
} else if (isRange(version)) {
let part = rangeToSemverPart(version);
if (part) {
let actor = await mainActor();
let res = await actor.getHighestSemverBatch([
[depName, stripRangePrefix(version), part],
]);
if ("err" in res) {
console.log(chalk.red("Error: ") + res.err);
return false;
}
let resolved = res.ok[0]?.[1];
if (!resolved) {
console.log(
chalk.red("Error: ") +
`No version of "${depName}" satisfies ${version}`,
);
return false;
}
version = resolved;
} else {
version = stripRangePrefix(version);
}
}

let cacheName = getMopsDepCacheName(depName, version);
Expand Down
15 changes: 15 additions & 0 deletions cli/commands/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
Requirement,
} from "../declarations/main/main.did.js";
import { Dependency } from "../types.js";
import { isRange } from "../semver.js";
import { testWithReporter } from "./test/test.js";
import { SilentReporter } from "./test/reporters/silent-reporter.js";
import { findChangelogEntry } from "../helpers/find-changelog-entry.js";
Expand Down Expand Up @@ -163,6 +164,20 @@ export async function publish(
}
}

let rangeDeps = [
...Object.values(config.dependencies || {}),
...Object.values(config["dev-dependencies"] || {}),
].filter((dep) => isRange(dep.version || ""));
if (rangeDeps.length > 0) {
console.log(
chalk.yellow("Warning: ") +
"version ranges in published dependencies are only understood by recent mops CLI versions. Older clients will fail to install this package.",
);
for (let dep of rangeDeps) {
console.log(` ${dep.name} = "${dep.version}"`);
}
}

if (config.package.keywords) {
for (let keyword of config.package.keywords) {
if (keyword.length > 20) {
Expand Down
38 changes: 15 additions & 23 deletions cli/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { add } from "./add.js";
import { getAvailableUpdates } from "./available-updates.js";
import { checkIntegrity } from "../integrity.js";
import { getDepName, getDepPinnedVersion } from "../helpers/get-dep-name.js";
import { isRange, stripRangePrefix } from "../semver.js";

type UpdateOptions = {
verbose?: boolean;
Expand Down Expand Up @@ -68,32 +69,23 @@ export async function update(pkg?: string, { lock }: UpdateOptions = {}) {
console.log(chalk.green("All dependencies are up to date!"));
}
} else {
for (let dep of available) {
let devDeps = Object.keys(config["dev-dependencies"] || {});
let allDeps = [...Object.keys(config.dependencies || {}), ...devDeps];
let devDepKeys = Object.keys(config["dev-dependencies"] || {});
let allDepKeys = [...Object.keys(config.dependencies || {}), ...devDepKeys];

let dev = false;
for (let d of devDeps) {
for (let [name, oldVersion, newVersion] of available) {
let bareOld = stripRangePrefix(oldVersion);
let matchesName = (d: string) => {
let pinnedVersion = getDepPinnedVersion(d);
if (
getDepName(d) === dep[0] &&
(!pinnedVersion || dep[1].startsWith(pinnedVersion))
) {
dev = true;
break;
}
}

let asName =
allDeps.find((d) => {
let pinnedVersion = getDepPinnedVersion(d);
return (
getDepName(d) === dep[0] &&
(!pinnedVersion || dep[1].startsWith(pinnedVersion))
);
}) || dep[0];
return (
getDepName(d) === name &&
(!pinnedVersion || bareOld.startsWith(pinnedVersion))
);
};

await add(`${dep[0]}@${dep[2]}`, { dev, lock }, asName);
let dev = devDepKeys.some(matchesName);
let asName = allDepKeys.find(matchesName) || name;
let rangePrefix = isRange(oldVersion) ? oldVersion[0] : "";
await add(`${name}@${rangePrefix}${newVersion}`, { dev, lock }, asName);
}
}

Expand Down
Loading
Loading