ic-mops 2.12.2 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/bundle/cli.tgz +0 -0
- package/cli.ts +14 -2
- package/commands/available-updates.ts +9 -1
- package/commands/outdated.ts +6 -2
- package/commands/publish.ts +0 -48
- package/commands/update.ts +10 -2
- package/dist/cli.js +4 -2
- package/dist/commands/available-updates.d.ts +2 -1
- package/dist/commands/available-updates.js +8 -2
- package/dist/commands/outdated.d.ts +3 -1
- package/dist/commands/outdated.js +2 -2
- package/dist/commands/publish.js +0 -43
- package/dist/commands/update.d.ts +2 -1
- package/dist/commands/update.js +2 -2
- package/dist/integrity.d.ts +3 -1
- package/dist/integrity.js +8 -3
- package/dist/package.json +1 -1
- package/dist/tests/cli.test.js +107 -2
- package/integrity.ts +12 -3
- package/package.json +1 -1
- package/tests/cli.test.ts +123 -2
- package/tests/install/update-bound/mops.toml +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Next
|
|
4
4
|
|
|
5
|
+
## 2.13.0
|
|
6
|
+
- Fix `mops update` and `mops outdated` jumping across major versions (or pre-1.0 minor versions) — they are now caret-bound by default, matching `cargo update`. For example, `core = "2.0.0"` now updates within `2.x.y` instead of jumping to a future `3.0.0`. Use `--major` to opt into cross-major updates.
|
|
7
|
+
|
|
8
|
+
## 2.12.3
|
|
9
|
+
- Fix `mops install --lock update` silently no-op'ing on a corrupt lockfile (#515)
|
|
10
|
+
- `mops publish` no longer rejects unknown `mops.toml` sections, `package.*` keys, or `requirements.*` entries — these typo guards were the only place in the CLI that complained about unknown keys, drifted from the docs/types, and blocked publish on harmless local-only config like `[moc]`, `[canisters]`, `[build]`, and `[lint]` (#512)
|
|
11
|
+
|
|
5
12
|
## 2.12.2
|
|
6
13
|
- Fix `mops install` (and any `--lock check` flow) failing with "Mismatched number of resolved packages" when a project's resolved dependencies include multiple aliases (e.g. `base`, `base@0`, `base@0.16`) that pin to the same `name@version`
|
|
7
14
|
|
package/bundle/cli.tgz
CHANGED
|
Binary file
|
package/cli.ts
CHANGED
|
@@ -627,14 +627,26 @@ program
|
|
|
627
627
|
program
|
|
628
628
|
.command("outdated")
|
|
629
629
|
.description("Print outdated dependencies specified in mops.toml")
|
|
630
|
-
.
|
|
631
|
-
|
|
630
|
+
.addOption(
|
|
631
|
+
new Option(
|
|
632
|
+
"--major",
|
|
633
|
+
"Allow updates that cross the caret bound (major versions, or for 0.x.y packages, minor versions)",
|
|
634
|
+
),
|
|
635
|
+
)
|
|
636
|
+
.action(async (options) => {
|
|
637
|
+
await outdated(options);
|
|
632
638
|
});
|
|
633
639
|
|
|
634
640
|
// update
|
|
635
641
|
program
|
|
636
642
|
.command("update [pkg]")
|
|
637
643
|
.description("Update dependencies specified in mops.toml")
|
|
644
|
+
.addOption(
|
|
645
|
+
new Option(
|
|
646
|
+
"--major",
|
|
647
|
+
"Allow updates that cross the caret bound (major versions, or for 0.x.y packages, minor versions)",
|
|
648
|
+
),
|
|
649
|
+
)
|
|
638
650
|
.addOption(
|
|
639
651
|
new Option("--lock <action>", "Lockfile action").choices([
|
|
640
652
|
"update",
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
+
import semver from "semver";
|
|
3
4
|
import { mainActor } from "../api/actors.js";
|
|
4
5
|
import { Config } from "../types.js";
|
|
5
6
|
import { getDepName, getDepPinnedVersion } from "../helpers/get-dep-name.js";
|
|
6
7
|
import { SemverPart } from "../declarations/main/main.did.js";
|
|
7
8
|
|
|
9
|
+
export type UpdateBound = "caret" | "major";
|
|
10
|
+
|
|
8
11
|
// [pkg, oldVersion, newVersion]
|
|
9
12
|
export async function getAvailableUpdates(
|
|
10
13
|
config: Config,
|
|
11
14
|
pkg?: string,
|
|
15
|
+
bound: UpdateBound = "caret",
|
|
12
16
|
): Promise<Array<[string, string, string]>> {
|
|
13
17
|
let deps = Object.values(config.dependencies || {});
|
|
14
18
|
let devDeps = Object.values(config["dev-dependencies"] || {});
|
|
@@ -46,8 +50,12 @@ export async function getAvailableUpdates(
|
|
|
46
50
|
pinnedVersion.split(".").length === 1
|
|
47
51
|
? { minor: null }
|
|
48
52
|
: { patch: null };
|
|
53
|
+
} else if (bound === "caret") {
|
|
54
|
+
// Caret (cargo-style): ^0.x.y -> 0.x.* (patch only); ^1+ -> same major (minor+patch)
|
|
55
|
+
let major = semver.major(dep.version!);
|
|
56
|
+
semverPart = major === 0 ? { patch: null } : { minor: null };
|
|
49
57
|
}
|
|
50
|
-
return [name, dep.version
|
|
58
|
+
return [name, dep.version!, semverPart];
|
|
51
59
|
}),
|
|
52
60
|
);
|
|
53
61
|
|
package/commands/outdated.ts
CHANGED
|
@@ -3,13 +3,17 @@ import { checkConfigFile, readConfig } from "../mops.js";
|
|
|
3
3
|
import { getAvailableUpdates } from "./available-updates.js";
|
|
4
4
|
import { getDepName, getDepPinnedVersion } from "../helpers/get-dep-name.js";
|
|
5
5
|
|
|
6
|
-
export async function outdated() {
|
|
6
|
+
export async function outdated({ major }: { major?: boolean } = {}) {
|
|
7
7
|
if (!checkConfigFile()) {
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
10
|
let config = readConfig();
|
|
11
11
|
|
|
12
|
-
let available = await getAvailableUpdates(
|
|
12
|
+
let available = await getAvailableUpdates(
|
|
13
|
+
config,
|
|
14
|
+
undefined,
|
|
15
|
+
major ? "major" : "caret",
|
|
16
|
+
);
|
|
13
17
|
|
|
14
18
|
if (available.length === 0) {
|
|
15
19
|
console.log(chalk.green("All dependencies are up to date!"));
|
package/commands/publish.ts
CHANGED
|
@@ -47,22 +47,6 @@ export async function publish(
|
|
|
47
47
|
|
|
48
48
|
console.log(`Publishing ${config.package?.name}@${config.package?.version}`);
|
|
49
49
|
|
|
50
|
-
// validate
|
|
51
|
-
for (let key of Object.keys(config)) {
|
|
52
|
-
if (
|
|
53
|
-
![
|
|
54
|
-
"package",
|
|
55
|
-
"dependencies",
|
|
56
|
-
"dev-dependencies",
|
|
57
|
-
"toolchain",
|
|
58
|
-
"requirements",
|
|
59
|
-
].includes(key)
|
|
60
|
-
) {
|
|
61
|
-
console.log(chalk.red("Error: ") + `Unknown config section [${key}]`);
|
|
62
|
-
process.exit(1);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
50
|
// required fields
|
|
67
51
|
if (!config.package) {
|
|
68
52
|
console.log(
|
|
@@ -97,29 +81,6 @@ export async function publish(
|
|
|
97
81
|
}
|
|
98
82
|
}
|
|
99
83
|
|
|
100
|
-
let packageKeys = [
|
|
101
|
-
"name",
|
|
102
|
-
"version",
|
|
103
|
-
"keywords",
|
|
104
|
-
"description",
|
|
105
|
-
"repository",
|
|
106
|
-
"documentation",
|
|
107
|
-
"homepage",
|
|
108
|
-
"baseDir",
|
|
109
|
-
"readme",
|
|
110
|
-
"license",
|
|
111
|
-
"files",
|
|
112
|
-
"dfx",
|
|
113
|
-
"moc",
|
|
114
|
-
"donation",
|
|
115
|
-
];
|
|
116
|
-
for (let key of Object.keys(config.package)) {
|
|
117
|
-
if (!packageKeys.includes(key)) {
|
|
118
|
-
console.log(chalk.red("Error: ") + `Unknown config key 'package.${key}'`);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
84
|
// disabled fields
|
|
124
85
|
for (let key of ["dfx", "moc", "homepage", "documentation", "donation"]) {
|
|
125
86
|
if ((config.package as any)[key]) {
|
|
@@ -222,15 +183,6 @@ export async function publish(
|
|
|
222
183
|
}
|
|
223
184
|
}
|
|
224
185
|
|
|
225
|
-
if (config.requirements) {
|
|
226
|
-
Object.keys(config.requirements).forEach((name) => {
|
|
227
|
-
if (name !== "moc") {
|
|
228
|
-
console.log(chalk.red("Error: ") + `Unknown requirement "${name}"`);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
186
|
let toBackendDep = (dep: Dependency): DependencyV2 => {
|
|
235
187
|
return {
|
|
236
188
|
...dep,
|
package/commands/update.ts
CHANGED
|
@@ -14,9 +14,13 @@ type UpdateOptions = {
|
|
|
14
14
|
verbose?: boolean;
|
|
15
15
|
dev?: boolean;
|
|
16
16
|
lock?: "update" | "ignore";
|
|
17
|
+
major?: boolean;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
|
-
export async function update(
|
|
20
|
+
export async function update(
|
|
21
|
+
pkg?: string,
|
|
22
|
+
{ lock, major }: UpdateOptions = {},
|
|
23
|
+
) {
|
|
20
24
|
if (!checkConfigFile()) {
|
|
21
25
|
return;
|
|
22
26
|
}
|
|
@@ -59,7 +63,11 @@ export async function update(pkg?: string, { lock }: UpdateOptions = {}) {
|
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
// update mops packages
|
|
62
|
-
let available = await getAvailableUpdates(
|
|
66
|
+
let available = await getAvailableUpdates(
|
|
67
|
+
config,
|
|
68
|
+
pkg,
|
|
69
|
+
major ? "major" : "caret",
|
|
70
|
+
);
|
|
63
71
|
|
|
64
72
|
if (available.length === 0) {
|
|
65
73
|
if (pkg) {
|
package/dist/cli.js
CHANGED
|
@@ -492,13 +492,15 @@ program
|
|
|
492
492
|
program
|
|
493
493
|
.command("outdated")
|
|
494
494
|
.description("Print outdated dependencies specified in mops.toml")
|
|
495
|
-
.
|
|
496
|
-
|
|
495
|
+
.addOption(new Option("--major", "Allow updates that cross the caret bound (major versions, or for 0.x.y packages, minor versions)"))
|
|
496
|
+
.action(async (options) => {
|
|
497
|
+
await outdated(options);
|
|
497
498
|
});
|
|
498
499
|
// update
|
|
499
500
|
program
|
|
500
501
|
.command("update [pkg]")
|
|
501
502
|
.description("Update dependencies specified in mops.toml")
|
|
503
|
+
.addOption(new Option("--major", "Allow updates that cross the caret bound (major versions, or for 0.x.y packages, minor versions)"))
|
|
502
504
|
.addOption(new Option("--lock <action>", "Lockfile action").choices([
|
|
503
505
|
"update",
|
|
504
506
|
"ignore",
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { Config } from "../types.js";
|
|
2
|
-
export
|
|
2
|
+
export type UpdateBound = "caret" | "major";
|
|
3
|
+
export declare function getAvailableUpdates(config: Config, pkg?: string, bound?: UpdateBound): Promise<Array<[string, string, string]>>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
+
import semver from "semver";
|
|
3
4
|
import { mainActor } from "../api/actors.js";
|
|
4
5
|
import { getDepName, getDepPinnedVersion } from "../helpers/get-dep-name.js";
|
|
5
6
|
// [pkg, oldVersion, newVersion]
|
|
6
|
-
export async function getAvailableUpdates(config, pkg) {
|
|
7
|
+
export async function getAvailableUpdates(config, pkg, bound = "caret") {
|
|
7
8
|
let deps = Object.values(config.dependencies || {});
|
|
8
9
|
let devDeps = Object.values(config["dev-dependencies"] || {});
|
|
9
10
|
let allDeps = [...deps, ...devDeps].filter((dep) => dep.version);
|
|
@@ -34,7 +35,12 @@ export async function getAvailableUpdates(config, pkg) {
|
|
|
34
35
|
? { minor: null }
|
|
35
36
|
: { patch: null };
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
+
else if (bound === "caret") {
|
|
39
|
+
// Caret (cargo-style): ^0.x.y -> 0.x.* (patch only); ^1+ -> same major (minor+patch)
|
|
40
|
+
let major = semver.major(dep.version);
|
|
41
|
+
semverPart = major === 0 ? { patch: null } : { minor: null };
|
|
42
|
+
}
|
|
43
|
+
return [name, dep.version, semverPart];
|
|
38
44
|
}));
|
|
39
45
|
if ("err" in res) {
|
|
40
46
|
console.log(chalk.red("Error:"), res.err);
|
|
@@ -2,12 +2,12 @@ import chalk from "chalk";
|
|
|
2
2
|
import { checkConfigFile, readConfig } from "../mops.js";
|
|
3
3
|
import { getAvailableUpdates } from "./available-updates.js";
|
|
4
4
|
import { getDepName, getDepPinnedVersion } from "../helpers/get-dep-name.js";
|
|
5
|
-
export async function outdated() {
|
|
5
|
+
export async function outdated({ major } = {}) {
|
|
6
6
|
if (!checkConfigFile()) {
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
9
9
|
let config = readConfig();
|
|
10
|
-
let available = await getAvailableUpdates(config);
|
|
10
|
+
let available = await getAvailableUpdates(config, undefined, major ? "major" : "caret");
|
|
11
11
|
if (available.length === 0) {
|
|
12
12
|
console.log(chalk.green("All dependencies are up to date!"));
|
|
13
13
|
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -22,19 +22,6 @@ export async function publish(options = {}) {
|
|
|
22
22
|
let rootDir = getRootDir();
|
|
23
23
|
let config = readConfig();
|
|
24
24
|
console.log(`Publishing ${config.package?.name}@${config.package?.version}`);
|
|
25
|
-
// validate
|
|
26
|
-
for (let key of Object.keys(config)) {
|
|
27
|
-
if (![
|
|
28
|
-
"package",
|
|
29
|
-
"dependencies",
|
|
30
|
-
"dev-dependencies",
|
|
31
|
-
"toolchain",
|
|
32
|
-
"requirements",
|
|
33
|
-
].includes(key)) {
|
|
34
|
-
console.log(chalk.red("Error: ") + `Unknown config section [${key}]`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
25
|
// required fields
|
|
39
26
|
if (!config.package) {
|
|
40
27
|
console.log(chalk.red("Error: ") +
|
|
@@ -63,28 +50,6 @@ export async function publish(options = {}) {
|
|
|
63
50
|
}
|
|
64
51
|
}
|
|
65
52
|
}
|
|
66
|
-
let packageKeys = [
|
|
67
|
-
"name",
|
|
68
|
-
"version",
|
|
69
|
-
"keywords",
|
|
70
|
-
"description",
|
|
71
|
-
"repository",
|
|
72
|
-
"documentation",
|
|
73
|
-
"homepage",
|
|
74
|
-
"baseDir",
|
|
75
|
-
"readme",
|
|
76
|
-
"license",
|
|
77
|
-
"files",
|
|
78
|
-
"dfx",
|
|
79
|
-
"moc",
|
|
80
|
-
"donation",
|
|
81
|
-
];
|
|
82
|
-
for (let key of Object.keys(config.package)) {
|
|
83
|
-
if (!packageKeys.includes(key)) {
|
|
84
|
-
console.log(chalk.red("Error: ") + `Unknown config key 'package.${key}'`);
|
|
85
|
-
process.exit(1);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
53
|
// disabled fields
|
|
89
54
|
for (let key of ["dfx", "moc", "homepage", "documentation", "donation"]) {
|
|
90
55
|
if (config.package[key]) {
|
|
@@ -167,14 +132,6 @@ export async function publish(options = {}) {
|
|
|
167
132
|
}
|
|
168
133
|
}
|
|
169
134
|
}
|
|
170
|
-
if (config.requirements) {
|
|
171
|
-
Object.keys(config.requirements).forEach((name) => {
|
|
172
|
-
if (name !== "moc") {
|
|
173
|
-
console.log(chalk.red("Error: ") + `Unknown requirement "${name}"`);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
135
|
let toBackendDep = (dep) => {
|
|
179
136
|
return {
|
|
180
137
|
...dep,
|
|
@@ -2,6 +2,7 @@ type UpdateOptions = {
|
|
|
2
2
|
verbose?: boolean;
|
|
3
3
|
dev?: boolean;
|
|
4
4
|
lock?: "update" | "ignore";
|
|
5
|
+
major?: boolean;
|
|
5
6
|
};
|
|
6
|
-
export declare function update(pkg?: string, { lock }?: UpdateOptions): Promise<void>;
|
|
7
|
+
export declare function update(pkg?: string, { lock, major }?: UpdateOptions): Promise<void>;
|
|
7
8
|
export {};
|
package/dist/commands/update.js
CHANGED
|
@@ -4,7 +4,7 @@ import { add } from "./add.js";
|
|
|
4
4
|
import { getAvailableUpdates } from "./available-updates.js";
|
|
5
5
|
import { checkIntegrity } from "../integrity.js";
|
|
6
6
|
import { getDepName, getDepPinnedVersion } from "../helpers/get-dep-name.js";
|
|
7
|
-
export async function update(pkg, { lock } = {}) {
|
|
7
|
+
export async function update(pkg, { lock, major } = {}) {
|
|
8
8
|
if (!checkConfigFile()) {
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
@@ -36,7 +36,7 @@ export async function update(pkg, { lock } = {}) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
// update mops packages
|
|
39
|
-
let available = await getAvailableUpdates(config, pkg);
|
|
39
|
+
let available = await getAvailableUpdates(config, pkg, major ? "major" : "caret");
|
|
40
40
|
if (available.length === 0) {
|
|
41
41
|
if (pkg) {
|
|
42
42
|
console.log(chalk.green(`Package "${pkg}" is up to date!`));
|
package/dist/integrity.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export declare function getLocalFileHash(fileId: string): string;
|
|
|
20
20
|
export declare function checkRemote(): Promise<void>;
|
|
21
21
|
export declare function readLockFile(): LockFile | null;
|
|
22
22
|
export declare function checkLockFileLight(): boolean;
|
|
23
|
-
export declare function updateLockFile(
|
|
23
|
+
export declare function updateLockFile({ force, }?: {
|
|
24
|
+
force?: boolean;
|
|
25
|
+
}): Promise<void>;
|
|
24
26
|
export declare function checkLockFile(force?: boolean): Promise<void>;
|
|
25
27
|
export {};
|
package/dist/integrity.js
CHANGED
|
@@ -13,7 +13,7 @@ export async function checkIntegrity(lock) {
|
|
|
13
13
|
lock = process.env["CI"] ? "check" : "update";
|
|
14
14
|
}
|
|
15
15
|
if (lock === "update") {
|
|
16
|
-
await updateLockFile();
|
|
16
|
+
await updateLockFile({ force });
|
|
17
17
|
await checkLockFile(force);
|
|
18
18
|
}
|
|
19
19
|
else if (lock === "check") {
|
|
@@ -108,9 +108,11 @@ export function checkLockFileLight() {
|
|
|
108
108
|
}
|
|
109
109
|
return false;
|
|
110
110
|
}
|
|
111
|
-
export async function updateLockFile() {
|
|
111
|
+
export async function updateLockFile({ force = false, } = {}) {
|
|
112
112
|
// if lock file exists and mops.toml hasn't changed, don't update it
|
|
113
|
-
|
|
113
|
+
// (unless forced: `--lock update` must unconditionally regenerate so users
|
|
114
|
+
// can recover from a corrupt lockfile without `rm mops.lock`)
|
|
115
|
+
if (!force && checkLockFileLight()) {
|
|
114
116
|
return;
|
|
115
117
|
}
|
|
116
118
|
let resolvedDeps = await resolvePackages();
|
|
@@ -228,6 +230,9 @@ export async function checkLockFile(force = false) {
|
|
|
228
230
|
console.error(`Mismatched hash for ${fileId}`);
|
|
229
231
|
console.error(`Locked hash: ${lockedHash}`);
|
|
230
232
|
console.error(`Actual hash: ${localHash}`);
|
|
233
|
+
console.error("");
|
|
234
|
+
console.error("If you have not modified files under .mops/, your lockfile may be stale or corrupt.");
|
|
235
|
+
console.error("Run `mops install --lock update` to regenerate it.");
|
|
231
236
|
process.exit(1);
|
|
232
237
|
}
|
|
233
238
|
}
|
package/dist/package.json
CHANGED
package/dist/tests/cli.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, jest, test } from "@jest/globals";
|
|
2
|
-
import { existsSync, rmSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { cli } from "./helpers";
|
|
4
|
+
import { cli, normalizePaths } from "./helpers";
|
|
5
5
|
describe("cli", () => {
|
|
6
6
|
test("--version", async () => {
|
|
7
7
|
expect((await cli(["--version"])).stdout).toMatch(/CLI \d+\.\d+\.\d+/);
|
|
@@ -86,4 +86,109 @@ describe("install", () => {
|
|
|
86
86
|
rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
|
+
// Regression: `install --lock update` used to early-return if mops.toml's
|
|
90
|
+
// deps hash was unchanged, even when the lockfile's per-file hashes were
|
|
91
|
+
// stale/corrupt. The subsequent checkLockFile would then fail and exit 1,
|
|
92
|
+
// so `--lock update` could never recover a broken lock — the only escape
|
|
93
|
+
// was `rm mops.lock`. See issue #514.
|
|
94
|
+
test("--lock update rewrites a lockfile with a corrupt file hash", async () => {
|
|
95
|
+
const cwd = path.join(import.meta.dirname, "install/success");
|
|
96
|
+
const lockFile = path.join(cwd, "mops.lock");
|
|
97
|
+
rmSync(lockFile, { force: true });
|
|
98
|
+
try {
|
|
99
|
+
const first = await cli(["install"], { cwd, env: { CI: undefined } });
|
|
100
|
+
expect(first.exitCode).toBe(0);
|
|
101
|
+
expect(existsSync(lockFile)).toBe(true);
|
|
102
|
+
const bad = "BAD0000000000000000000000000000000000000000000000000000000000BAD";
|
|
103
|
+
const original = readFileSync(lockFile, "utf8");
|
|
104
|
+
const corrupted = original.replace(/"core@1\.0\.0\/mops\.toml":\s*"[0-9a-f]{64}"/, `"core@1.0.0/mops.toml": "${bad}"`);
|
|
105
|
+
expect(corrupted).not.toBe(original);
|
|
106
|
+
writeFileSync(lockFile, corrupted);
|
|
107
|
+
const result = await cli(["install", "--lock", "update"], {
|
|
108
|
+
cwd,
|
|
109
|
+
env: { CI: undefined },
|
|
110
|
+
});
|
|
111
|
+
expect(result.exitCode).toBe(0);
|
|
112
|
+
expect(readFileSync(lockFile, "utf8")).not.toContain(bad);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
rmSync(lockFile, { force: true });
|
|
116
|
+
rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
// `mops update` and `mops outdated` default to caret-bound resolution: stay
|
|
121
|
+
// within `0.x.y` (or `1.x.y`) and never cross majors. Fixture pins:
|
|
122
|
+
// base = "0.14.5" -> caret bumps within 0.14.x; --major jumps past it
|
|
123
|
+
// core = "1.0.0" -> caret stays put (no 1.x.y > 1.0.0); --major jumps to 2.x
|
|
124
|
+
describe("update / outdated bounds", () => {
|
|
125
|
+
jest.setTimeout(120_000);
|
|
126
|
+
const cwd = path.join(import.meta.dirname, "install/update-bound");
|
|
127
|
+
const tomlFile = path.join(cwd, "mops.toml");
|
|
128
|
+
const original = readFileSync(tomlFile, "utf8");
|
|
129
|
+
const cleanup = () => {
|
|
130
|
+
rmSync(path.join(cwd, "mops.lock"), { force: true });
|
|
131
|
+
rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
|
|
132
|
+
writeFileSync(tomlFile, original);
|
|
133
|
+
};
|
|
134
|
+
const baseVersion = (toml) => toml.match(/base = "(0\.\d+\.\d+)"/)?.[1];
|
|
135
|
+
const coreMajor = (toml) => parseInt(toml.match(/core = "(\d+)\./)?.[1] ?? "0");
|
|
136
|
+
test("mops update stays within the caret bound by default", async () => {
|
|
137
|
+
cleanup();
|
|
138
|
+
try {
|
|
139
|
+
await cli(["install"], { cwd, env: { CI: undefined } });
|
|
140
|
+
const result = await cli(["update"], { cwd, env: { CI: undefined } });
|
|
141
|
+
expect(result.exitCode).toBe(0);
|
|
142
|
+
const after = readFileSync(tomlFile, "utf8");
|
|
143
|
+
// base (pre-1.0): bumped within 0.14.x (patch bumps allowed)
|
|
144
|
+
expect(baseVersion(after)).toMatch(/^0\.14\./);
|
|
145
|
+
expect(baseVersion(after)).not.toBe("0.14.5");
|
|
146
|
+
// core (1.x): no 1.x.y > 1.0.0 published, so no bump across majors
|
|
147
|
+
expect(coreMajor(after)).toBe(1);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
cleanup();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
test("mops update --major crosses the caret bound", async () => {
|
|
154
|
+
cleanup();
|
|
155
|
+
try {
|
|
156
|
+
await cli(["install"], { cwd, env: { CI: undefined } });
|
|
157
|
+
const result = await cli(["update", "--major"], {
|
|
158
|
+
cwd,
|
|
159
|
+
env: { CI: undefined },
|
|
160
|
+
});
|
|
161
|
+
expect(result.exitCode).toBe(0);
|
|
162
|
+
const after = readFileSync(tomlFile, "utf8");
|
|
163
|
+
// base: jumps past 0.14.x (next minor or major)
|
|
164
|
+
const baseMinor = parseInt(after.match(/base = "0\.(\d+)\./)?.[1] ?? "0");
|
|
165
|
+
expect(baseMinor).toBeGreaterThanOrEqual(15);
|
|
166
|
+
// core: jumps to 2.x or later
|
|
167
|
+
expect(coreMajor(after)).toBeGreaterThanOrEqual(2);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
cleanup();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
test("mops outdated honors --major flag", async () => {
|
|
174
|
+
cleanup();
|
|
175
|
+
try {
|
|
176
|
+
await cli(["install"], { cwd, env: { CI: undefined } });
|
|
177
|
+
const caret = normalizePaths((await cli(["outdated"], { cwd, env: { CI: undefined } })).stdout);
|
|
178
|
+
const major = normalizePaths((await cli(["outdated", "--major"], { cwd, env: { CI: undefined } }))
|
|
179
|
+
.stdout);
|
|
180
|
+
// caret-bound: base bumps within 0.14.x; core (if reported) stays in 1.x
|
|
181
|
+
expect(caret).toMatch(/base 0\.14\.5 -> 0\.14\./);
|
|
182
|
+
const caretCore = caret.match(/core 1\.0\.0 -> (\d+)\./)?.[1];
|
|
183
|
+
if (caretCore) {
|
|
184
|
+
expect(parseInt(caretCore)).toBe(1);
|
|
185
|
+
}
|
|
186
|
+
// --major: both bump across their major bounds
|
|
187
|
+
expect(major).toMatch(/base 0\.14\.5 -> 0\.(1[5-9]|[2-9]\d)/);
|
|
188
|
+
expect(major).toMatch(/core 1\.0\.0 -> [2-9]/);
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
cleanup();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
89
194
|
});
|
package/integrity.ts
CHANGED
|
@@ -41,7 +41,7 @@ export async function checkIntegrity(lock?: "check" | "update" | "ignore") {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
if (lock === "update") {
|
|
44
|
-
await updateLockFile();
|
|
44
|
+
await updateLockFile({ force });
|
|
45
45
|
await checkLockFile(force);
|
|
46
46
|
} else if (lock === "check") {
|
|
47
47
|
await checkLockFile(force);
|
|
@@ -159,9 +159,13 @@ export function checkLockFileLight(): boolean {
|
|
|
159
159
|
return false;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
export async function updateLockFile(
|
|
162
|
+
export async function updateLockFile({
|
|
163
|
+
force = false,
|
|
164
|
+
}: { force?: boolean } = {}) {
|
|
163
165
|
// if lock file exists and mops.toml hasn't changed, don't update it
|
|
164
|
-
|
|
166
|
+
// (unless forced: `--lock update` must unconditionally regenerate so users
|
|
167
|
+
// can recover from a corrupt lockfile without `rm mops.lock`)
|
|
168
|
+
if (!force && checkLockFileLight()) {
|
|
165
169
|
return;
|
|
166
170
|
}
|
|
167
171
|
|
|
@@ -313,6 +317,11 @@ export async function checkLockFile(force = false) {
|
|
|
313
317
|
console.error(`Mismatched hash for ${fileId}`);
|
|
314
318
|
console.error(`Locked hash: ${lockedHash}`);
|
|
315
319
|
console.error(`Actual hash: ${localHash}`);
|
|
320
|
+
console.error("");
|
|
321
|
+
console.error(
|
|
322
|
+
"If you have not modified files under .mops/, your lockfile may be stale or corrupt.",
|
|
323
|
+
);
|
|
324
|
+
console.error("Run `mops install --lock update` to regenerate it.");
|
|
316
325
|
process.exit(1);
|
|
317
326
|
}
|
|
318
327
|
}
|
package/package.json
CHANGED
package/tests/cli.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, jest, test } from "@jest/globals";
|
|
2
|
-
import { existsSync, rmSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { cli } from "./helpers";
|
|
4
|
+
import { cli, normalizePaths } from "./helpers";
|
|
5
5
|
|
|
6
6
|
describe("cli", () => {
|
|
7
7
|
test("--version", async () => {
|
|
@@ -92,4 +92,125 @@ describe("install", () => {
|
|
|
92
92
|
rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
|
|
93
93
|
}
|
|
94
94
|
});
|
|
95
|
+
|
|
96
|
+
// Regression: `install --lock update` used to early-return if mops.toml's
|
|
97
|
+
// deps hash was unchanged, even when the lockfile's per-file hashes were
|
|
98
|
+
// stale/corrupt. The subsequent checkLockFile would then fail and exit 1,
|
|
99
|
+
// so `--lock update` could never recover a broken lock — the only escape
|
|
100
|
+
// was `rm mops.lock`. See issue #514.
|
|
101
|
+
test("--lock update rewrites a lockfile with a corrupt file hash", async () => {
|
|
102
|
+
const cwd = path.join(import.meta.dirname, "install/success");
|
|
103
|
+
const lockFile = path.join(cwd, "mops.lock");
|
|
104
|
+
rmSync(lockFile, { force: true });
|
|
105
|
+
try {
|
|
106
|
+
const first = await cli(["install"], { cwd, env: { CI: undefined } });
|
|
107
|
+
expect(first.exitCode).toBe(0);
|
|
108
|
+
expect(existsSync(lockFile)).toBe(true);
|
|
109
|
+
|
|
110
|
+
const bad =
|
|
111
|
+
"BAD0000000000000000000000000000000000000000000000000000000000BAD";
|
|
112
|
+
const original = readFileSync(lockFile, "utf8");
|
|
113
|
+
const corrupted = original.replace(
|
|
114
|
+
/"core@1\.0\.0\/mops\.toml":\s*"[0-9a-f]{64}"/,
|
|
115
|
+
`"core@1.0.0/mops.toml": "${bad}"`,
|
|
116
|
+
);
|
|
117
|
+
expect(corrupted).not.toBe(original);
|
|
118
|
+
writeFileSync(lockFile, corrupted);
|
|
119
|
+
|
|
120
|
+
const result = await cli(["install", "--lock", "update"], {
|
|
121
|
+
cwd,
|
|
122
|
+
env: { CI: undefined },
|
|
123
|
+
});
|
|
124
|
+
expect(result.exitCode).toBe(0);
|
|
125
|
+
expect(readFileSync(lockFile, "utf8")).not.toContain(bad);
|
|
126
|
+
} finally {
|
|
127
|
+
rmSync(lockFile, { force: true });
|
|
128
|
+
rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// `mops update` and `mops outdated` default to caret-bound resolution: stay
|
|
134
|
+
// within `0.x.y` (or `1.x.y`) and never cross majors. Fixture pins:
|
|
135
|
+
// base = "0.14.5" -> caret bumps within 0.14.x; --major jumps past it
|
|
136
|
+
// core = "1.0.0" -> caret stays put (no 1.x.y > 1.0.0); --major jumps to 2.x
|
|
137
|
+
describe("update / outdated bounds", () => {
|
|
138
|
+
jest.setTimeout(120_000);
|
|
139
|
+
|
|
140
|
+
const cwd = path.join(import.meta.dirname, "install/update-bound");
|
|
141
|
+
const tomlFile = path.join(cwd, "mops.toml");
|
|
142
|
+
const original = readFileSync(tomlFile, "utf8");
|
|
143
|
+
|
|
144
|
+
const cleanup = () => {
|
|
145
|
+
rmSync(path.join(cwd, "mops.lock"), { force: true });
|
|
146
|
+
rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
|
|
147
|
+
writeFileSync(tomlFile, original);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const baseVersion = (toml: string) =>
|
|
151
|
+
toml.match(/base = "(0\.\d+\.\d+)"/)?.[1];
|
|
152
|
+
const coreMajor = (toml: string) =>
|
|
153
|
+
parseInt(toml.match(/core = "(\d+)\./)?.[1] ?? "0");
|
|
154
|
+
|
|
155
|
+
test("mops update stays within the caret bound by default", async () => {
|
|
156
|
+
cleanup();
|
|
157
|
+
try {
|
|
158
|
+
await cli(["install"], { cwd, env: { CI: undefined } });
|
|
159
|
+
const result = await cli(["update"], { cwd, env: { CI: undefined } });
|
|
160
|
+
expect(result.exitCode).toBe(0);
|
|
161
|
+
const after = readFileSync(tomlFile, "utf8");
|
|
162
|
+
// base (pre-1.0): bumped within 0.14.x (patch bumps allowed)
|
|
163
|
+
expect(baseVersion(after)).toMatch(/^0\.14\./);
|
|
164
|
+
expect(baseVersion(after)).not.toBe("0.14.5");
|
|
165
|
+
// core (1.x): no 1.x.y > 1.0.0 published, so no bump across majors
|
|
166
|
+
expect(coreMajor(after)).toBe(1);
|
|
167
|
+
} finally {
|
|
168
|
+
cleanup();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("mops update --major crosses the caret bound", async () => {
|
|
173
|
+
cleanup();
|
|
174
|
+
try {
|
|
175
|
+
await cli(["install"], { cwd, env: { CI: undefined } });
|
|
176
|
+
const result = await cli(["update", "--major"], {
|
|
177
|
+
cwd,
|
|
178
|
+
env: { CI: undefined },
|
|
179
|
+
});
|
|
180
|
+
expect(result.exitCode).toBe(0);
|
|
181
|
+
const after = readFileSync(tomlFile, "utf8");
|
|
182
|
+
// base: jumps past 0.14.x (next minor or major)
|
|
183
|
+
const baseMinor = parseInt(after.match(/base = "0\.(\d+)\./)?.[1] ?? "0");
|
|
184
|
+
expect(baseMinor).toBeGreaterThanOrEqual(15);
|
|
185
|
+
// core: jumps to 2.x or later
|
|
186
|
+
expect(coreMajor(after)).toBeGreaterThanOrEqual(2);
|
|
187
|
+
} finally {
|
|
188
|
+
cleanup();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("mops outdated honors --major flag", async () => {
|
|
193
|
+
cleanup();
|
|
194
|
+
try {
|
|
195
|
+
await cli(["install"], { cwd, env: { CI: undefined } });
|
|
196
|
+
const caret = normalizePaths(
|
|
197
|
+
(await cli(["outdated"], { cwd, env: { CI: undefined } })).stdout,
|
|
198
|
+
);
|
|
199
|
+
const major = normalizePaths(
|
|
200
|
+
(await cli(["outdated", "--major"], { cwd, env: { CI: undefined } }))
|
|
201
|
+
.stdout,
|
|
202
|
+
);
|
|
203
|
+
// caret-bound: base bumps within 0.14.x; core (if reported) stays in 1.x
|
|
204
|
+
expect(caret).toMatch(/base 0\.14\.5 -> 0\.14\./);
|
|
205
|
+
const caretCore = caret.match(/core 1\.0\.0 -> (\d+)\./)?.[1];
|
|
206
|
+
if (caretCore) {
|
|
207
|
+
expect(parseInt(caretCore)).toBe(1);
|
|
208
|
+
}
|
|
209
|
+
// --major: both bump across their major bounds
|
|
210
|
+
expect(major).toMatch(/base 0\.14\.5 -> 0\.(1[5-9]|[2-9]\d)/);
|
|
211
|
+
expect(major).toMatch(/core 1\.0\.0 -> [2-9]/);
|
|
212
|
+
} finally {
|
|
213
|
+
cleanup();
|
|
214
|
+
}
|
|
215
|
+
});
|
|
95
216
|
});
|