ic-mops 2.12.3 → 2.13.1
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 +6 -0
- package/bundle/cli.tgz +0 -0
- package/cli.ts +14 -2
- package/commands/available-updates.ts +9 -1
- package/commands/lint.ts +40 -7
- package/commands/outdated.ts +6 -2
- 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/lint.js +19 -1
- package/dist/commands/outdated.d.ts +3 -1
- package/dist/commands/outdated.js +2 -2
- package/dist/commands/update.d.ts +2 -1
- package/dist/commands/update.js +2 -2
- package/dist/helpers/migrations.d.ts +8 -1
- package/dist/helpers/migrations.js +53 -21
- package/dist/package.json +1 -1
- package/dist/tests/cli.test.js +76 -1
- package/dist/tests/lint.test.js +63 -1
- package/helpers/migrations.ts +84 -31
- package/package.json +1 -1
- package/tests/cli.test.ts +86 -1
- package/tests/install/update-bound/mops.toml +3 -0
- package/tests/lint.test.ts +85 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Next
|
|
4
4
|
|
|
5
|
+
## 2.13.1
|
|
6
|
+
- `mops lint` now honors `[canisters.<name>.migrations].check-limit`, skipping trimmed chain migrations so projects with large migration histories lint as fast as they type-check. Pass an explicit filter (`mops lint <name>`) to opt back in for a one-off lint of a trimmed file.
|
|
7
|
+
|
|
8
|
+
## 2.13.0
|
|
9
|
+
- 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.
|
|
10
|
+
|
|
5
11
|
## 2.12.3
|
|
6
12
|
- Fix `mops install --lock update` silently no-op'ing on a corrupt lockfile (#515)
|
|
7
13
|
- `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)
|
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/lint.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { toolchain } from "./toolchain/index.js";
|
|
|
15
15
|
import { MOTOKO_GLOB_CONFIG } from "../constants.js";
|
|
16
16
|
import { existsSync } from "node:fs";
|
|
17
17
|
import { Config } from "../types.js";
|
|
18
|
+
import { getTrimmedMigrationFiles } from "../helpers/migrations.js";
|
|
18
19
|
|
|
19
20
|
async function resolveDepRules(
|
|
20
21
|
config: Config,
|
|
@@ -128,6 +129,17 @@ function buildCommonArgs(
|
|
|
128
129
|
return args;
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
function dropTrimmedMigrations(
|
|
133
|
+
files: string[],
|
|
134
|
+
rootDir: string,
|
|
135
|
+
excluded: Set<string>,
|
|
136
|
+
): string[] {
|
|
137
|
+
if (excluded.size === 0) {
|
|
138
|
+
return files;
|
|
139
|
+
}
|
|
140
|
+
return files.filter((f) => !excluded.has(path.resolve(rootDir, f)));
|
|
141
|
+
}
|
|
142
|
+
|
|
131
143
|
async function runLintoko(
|
|
132
144
|
lintokoBinPath: string,
|
|
133
145
|
rootDir: string,
|
|
@@ -169,6 +181,11 @@ export async function lint(
|
|
|
169
181
|
? await toolchain.bin("lintoko")
|
|
170
182
|
: "lintoko";
|
|
171
183
|
|
|
184
|
+
const isExplicit = !!filter || !!(options.files && options.files.length > 0);
|
|
185
|
+
const trimmedMigrations = isExplicit
|
|
186
|
+
? new Set<string>()
|
|
187
|
+
: getTrimmedMigrationFiles(config);
|
|
188
|
+
|
|
172
189
|
let filesToLint: string[];
|
|
173
190
|
if (options.files && options.files.length > 0) {
|
|
174
191
|
filesToLint = options.files;
|
|
@@ -185,6 +202,20 @@ export async function lint(
|
|
|
185
202
|
: "No .mo files found in the project",
|
|
186
203
|
);
|
|
187
204
|
}
|
|
205
|
+
const before = filesToLint.length;
|
|
206
|
+
filesToLint = dropTrimmedMigrations(
|
|
207
|
+
filesToLint,
|
|
208
|
+
rootDir,
|
|
209
|
+
trimmedMigrations,
|
|
210
|
+
);
|
|
211
|
+
if (options.verbose && before !== filesToLint.length) {
|
|
212
|
+
console.log(
|
|
213
|
+
chalk.blue("lint"),
|
|
214
|
+
chalk.gray(
|
|
215
|
+
`Trimmed ${before - filesToLint.length} migration file(s) (check-limit)`,
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
188
219
|
}
|
|
189
220
|
|
|
190
221
|
const commonArgs = buildCommonArgs(options, config);
|
|
@@ -198,13 +229,9 @@ export async function lint(
|
|
|
198
229
|
rules.forEach((rule) => baseArgs.push("--rules", rule));
|
|
199
230
|
baseArgs.push(...filesToLint);
|
|
200
231
|
|
|
201
|
-
let failed =
|
|
202
|
-
|
|
203
|
-
rootDir,
|
|
204
|
-
baseArgs,
|
|
205
|
-
options,
|
|
206
|
-
"base",
|
|
207
|
-
));
|
|
232
|
+
let failed =
|
|
233
|
+
filesToLint.length > 0 &&
|
|
234
|
+
!(await runLintoko(lintokoBinPath, rootDir, baseArgs, options, "base"));
|
|
208
235
|
|
|
209
236
|
// --- extra runs ---
|
|
210
237
|
const extraEntries = config.lint?.extra;
|
|
@@ -243,6 +270,12 @@ export async function lint(
|
|
|
243
270
|
);
|
|
244
271
|
}
|
|
245
272
|
|
|
273
|
+
matchedFiles = dropTrimmedMigrations(
|
|
274
|
+
matchedFiles,
|
|
275
|
+
rootDir,
|
|
276
|
+
trimmedMigrations,
|
|
277
|
+
);
|
|
278
|
+
|
|
246
279
|
if (matchedFiles.length === 0) {
|
|
247
280
|
console.warn(
|
|
248
281
|
chalk.yellow(
|
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/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);
|
package/dist/commands/lint.js
CHANGED
|
@@ -8,6 +8,7 @@ import { resolvePackages } from "../resolve-packages.js";
|
|
|
8
8
|
import { toolchain } from "./toolchain/index.js";
|
|
9
9
|
import { MOTOKO_GLOB_CONFIG } from "../constants.js";
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
|
+
import { getTrimmedMigrationFiles } from "../helpers/migrations.js";
|
|
11
12
|
async function resolveDepRules(config, rootDir) {
|
|
12
13
|
const ext = config.lint?.extends;
|
|
13
14
|
if (!ext) {
|
|
@@ -83,6 +84,12 @@ function buildCommonArgs(options, config) {
|
|
|
83
84
|
}
|
|
84
85
|
return args;
|
|
85
86
|
}
|
|
87
|
+
function dropTrimmedMigrations(files, rootDir, excluded) {
|
|
88
|
+
if (excluded.size === 0) {
|
|
89
|
+
return files;
|
|
90
|
+
}
|
|
91
|
+
return files.filter((f) => !excluded.has(path.resolve(rootDir, f)));
|
|
92
|
+
}
|
|
86
93
|
async function runLintoko(lintokoBinPath, rootDir, args, options, label) {
|
|
87
94
|
try {
|
|
88
95
|
if (options.verbose) {
|
|
@@ -107,6 +114,10 @@ export async function lint(filter, options) {
|
|
|
107
114
|
let lintokoBinPath = config.toolchain?.lintoko
|
|
108
115
|
? await toolchain.bin("lintoko")
|
|
109
116
|
: "lintoko";
|
|
117
|
+
const isExplicit = !!filter || !!(options.files && options.files.length > 0);
|
|
118
|
+
const trimmedMigrations = isExplicit
|
|
119
|
+
? new Set()
|
|
120
|
+
: getTrimmedMigrationFiles(config);
|
|
110
121
|
let filesToLint;
|
|
111
122
|
if (options.files && options.files.length > 0) {
|
|
112
123
|
filesToLint = options.files;
|
|
@@ -122,6 +133,11 @@ export async function lint(filter, options) {
|
|
|
122
133
|
? `No files found for filter '${filter}'`
|
|
123
134
|
: "No .mo files found in the project");
|
|
124
135
|
}
|
|
136
|
+
const before = filesToLint.length;
|
|
137
|
+
filesToLint = dropTrimmedMigrations(filesToLint, rootDir, trimmedMigrations);
|
|
138
|
+
if (options.verbose && before !== filesToLint.length) {
|
|
139
|
+
console.log(chalk.blue("lint"), chalk.gray(`Trimmed ${before - filesToLint.length} migration file(s) (check-limit)`));
|
|
140
|
+
}
|
|
125
141
|
}
|
|
126
142
|
const commonArgs = buildCommonArgs(options, config);
|
|
127
143
|
// --- base run ---
|
|
@@ -131,7 +147,8 @@ export async function lint(filter, options) {
|
|
|
131
147
|
: await collectLintRules(config, rootDir);
|
|
132
148
|
rules.forEach((rule) => baseArgs.push("--rules", rule));
|
|
133
149
|
baseArgs.push(...filesToLint);
|
|
134
|
-
let failed =
|
|
150
|
+
let failed = filesToLint.length > 0 &&
|
|
151
|
+
!(await runLintoko(lintokoBinPath, rootDir, baseArgs, options, "base"));
|
|
135
152
|
// --- extra runs ---
|
|
136
153
|
const extraEntries = config.lint?.extra;
|
|
137
154
|
if (extraEntries) {
|
|
@@ -156,6 +173,7 @@ export async function lint(filter, options) {
|
|
|
156
173
|
if (baseFileSet) {
|
|
157
174
|
matchedFiles = matchedFiles.filter((f) => baseFileSet.has(path.resolve(rootDir, f)));
|
|
158
175
|
}
|
|
176
|
+
matchedFiles = dropTrimmedMigrations(matchedFiles, rootDir, trimmedMigrations);
|
|
159
177
|
if (matchedFiles.length === 0) {
|
|
160
178
|
console.warn(chalk.yellow(`[lint.extra] no files matched glob '${globPattern}', skipping`));
|
|
161
179
|
continue;
|
|
@@ -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
|
}
|
|
@@ -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!`));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MigrationsConfig } from "../types.js";
|
|
1
|
+
import { Config, MigrationsConfig } from "../types.js";
|
|
2
2
|
export interface MigrationArgsResult {
|
|
3
3
|
migrationArgs: string[];
|
|
4
4
|
cleanup: () => Promise<void>;
|
|
@@ -8,3 +8,10 @@ export declare function getNextMigrationFile(nextDir: string): string | null;
|
|
|
8
8
|
export declare function validateNextMigrationOrder(chainDirOrFiles: string | string[], nextFile: string): void;
|
|
9
9
|
export declare function validateMigrationsConfig(migrations: MigrationsConfig, canisterName: string): void;
|
|
10
10
|
export declare function prepareMigrationArgs(migrations: MigrationsConfig | undefined, canisterName: string, mode: "check" | "build", verbose?: boolean): Promise<MigrationArgsResult>;
|
|
11
|
+
/**
|
|
12
|
+
* Absolute paths of chain migration files that `mops lint` should skip,
|
|
13
|
+
* mirroring the `check-limit` trimming applied to `moc` during `mops check`.
|
|
14
|
+
* Validates the migrations config along the way, so misconfig surfaces here
|
|
15
|
+
* just as it does in `mops check` (consistent failure across commands).
|
|
16
|
+
*/
|
|
17
|
+
export declare function getTrimmedMigrationFiles(config: Config): Set<string>;
|
|
@@ -4,6 +4,7 @@ import { rm } from "node:fs/promises";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { cliError } from "../error.js";
|
|
6
6
|
import { getRootDir, resolveConfigPath } from "../mops.js";
|
|
7
|
+
import { resolveCanisterConfigs } from "./resolve-canisters.js";
|
|
7
8
|
function stagedMigrationsDir(chainDir, canisterName) {
|
|
8
9
|
return join(dirname(chainDir), `.migrations-${canisterName}`);
|
|
9
10
|
}
|
|
@@ -60,14 +61,14 @@ export function validateMigrationsConfig(migrations, canisterName) {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the active migration chain for a canister: validate config, discover
|
|
66
|
+
* files, and apply `check-limit` / `build-limit`. Single source of truth for
|
|
67
|
+
* the trim semantics shared by `prepareMigrationArgs` (which stages `included`
|
|
68
|
+
* for moc) and `getTrimmedMigrationFiles` (which feeds `excludedChainFiles`
|
|
69
|
+
* to lint).
|
|
70
|
+
*/
|
|
71
|
+
function resolveMigrationChain(migrations, canisterName, mode) {
|
|
71
72
|
validateMigrationsConfig(migrations, canisterName);
|
|
72
73
|
const chainDir = resolveConfigPath(migrations.chain);
|
|
73
74
|
const nextDir = migrations.next
|
|
@@ -82,25 +83,39 @@ export async function prepareMigrationArgs(migrations, canisterName, mode, verbo
|
|
|
82
83
|
if (nextFile) {
|
|
83
84
|
validateNextMigrationOrder(chainFiles, nextFile);
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
+
// Treat chain + next as one virtual merged list; `next` is always last.
|
|
87
|
+
const all = chainFiles.map((f) => ({
|
|
86
88
|
file: f,
|
|
87
89
|
dir: chainDir,
|
|
88
90
|
}));
|
|
89
91
|
if (nextFile && nextDir) {
|
|
90
|
-
|
|
92
|
+
all.push({ file: nextFile, dir: nextDir });
|
|
91
93
|
}
|
|
92
94
|
const limit = mode === "check" ? migrations["check-limit"] : migrations["build-limit"];
|
|
93
|
-
const isTrimming = limit !== undefined && limit <
|
|
94
|
-
const
|
|
95
|
+
const isTrimming = limit !== undefined && limit < all.length;
|
|
96
|
+
const included = isTrimming ? all.slice(-limit) : all;
|
|
97
|
+
// Dropped entries are always a chain-only prefix (next sorts last).
|
|
98
|
+
const excludedChainFiles = all
|
|
99
|
+
.slice(0, all.length - included.length)
|
|
100
|
+
.map((e) => resolve(e.dir, e.file));
|
|
101
|
+
return { chainDir, nextDir, included, excludedChainFiles, isTrimming };
|
|
102
|
+
}
|
|
103
|
+
export async function prepareMigrationArgs(migrations, canisterName, mode, verbose) {
|
|
104
|
+
if (!migrations) {
|
|
105
|
+
return { migrationArgs: [], cleanup: async () => { } };
|
|
106
|
+
}
|
|
107
|
+
const { chainDir, nextDir, included, excludedChainFiles, isTrimming } = resolveMigrationChain(migrations, canisterName, mode);
|
|
108
|
+
const hasNext = included.some((e) => e.dir === nextDir);
|
|
109
|
+
const needsTempDir = hasNext || isTrimming;
|
|
95
110
|
if (!needsTempDir) {
|
|
96
111
|
return {
|
|
97
112
|
migrationArgs: [`--enhanced-migration=${chainDir}`],
|
|
98
113
|
cleanup: async () => { },
|
|
99
114
|
};
|
|
100
115
|
}
|
|
101
|
-
// Shortcut:
|
|
102
|
-
//
|
|
103
|
-
if (
|
|
116
|
+
// Shortcut: only the pending next migration is included → point moc at
|
|
117
|
+
// next-migration/ so diagnostics use the real path instead of the temp dir.
|
|
118
|
+
if (nextDir && included.length === 1 && included[0].dir === nextDir) {
|
|
104
119
|
const migrationArgs = [`--enhanced-migration=${nextDir}`];
|
|
105
120
|
if (isTrimming) {
|
|
106
121
|
migrationArgs.push("-A=M0254");
|
|
@@ -111,15 +126,13 @@ export async function prepareMigrationArgs(migrations, canisterName, mode, verbo
|
|
|
111
126
|
await rm(tempDir, { recursive: true, force: true });
|
|
112
127
|
mkdirSync(tempDir, { recursive: true });
|
|
113
128
|
writeFileSync(join(tempDir, ".gitignore"), "*\n");
|
|
114
|
-
const
|
|
115
|
-
? allMigrations.slice(-limit)
|
|
116
|
-
: allMigrations;
|
|
117
|
-
for (const { file, dir } of filesToInclude) {
|
|
129
|
+
for (const { file, dir } of included) {
|
|
118
130
|
symlinkSync(resolve(dir, file), join(tempDir, file));
|
|
119
131
|
}
|
|
120
132
|
if (verbose) {
|
|
121
|
-
|
|
122
|
-
|
|
133
|
+
const totalCount = included.length + excludedChainFiles.length;
|
|
134
|
+
console.log(chalk.blue("migrations"), chalk.gray(`Prepared ${included.length} migration(s) for ${canisterName}` +
|
|
135
|
+
(isTrimming ? ` (trimmed from ${totalCount})` : "")));
|
|
123
136
|
}
|
|
124
137
|
const migrationArgs = [`--enhanced-migration=${tempDir}`];
|
|
125
138
|
if (isTrimming) {
|
|
@@ -132,3 +145,22 @@ export async function prepareMigrationArgs(migrations, canisterName, mode, verbo
|
|
|
132
145
|
},
|
|
133
146
|
};
|
|
134
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Absolute paths of chain migration files that `mops lint` should skip,
|
|
150
|
+
* mirroring the `check-limit` trimming applied to `moc` during `mops check`.
|
|
151
|
+
* Validates the migrations config along the way, so misconfig surfaces here
|
|
152
|
+
* just as it does in `mops check` (consistent failure across commands).
|
|
153
|
+
*/
|
|
154
|
+
export function getTrimmedMigrationFiles(config) {
|
|
155
|
+
const excluded = new Set();
|
|
156
|
+
for (const [name, canister] of Object.entries(resolveCanisterConfigs(config))) {
|
|
157
|
+
if (!canister.migrations) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const { excludedChainFiles } = resolveMigrationChain(canister.migrations, name, "check");
|
|
161
|
+
for (const f of excludedChainFiles) {
|
|
162
|
+
excluded.add(f);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return excluded;
|
|
166
|
+
}
|
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
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+/);
|
|
@@ -117,3 +117,78 @@ describe("install", () => {
|
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
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
|
+
});
|
|
194
|
+
});
|
package/dist/tests/lint.test.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { describe, expect, test } from "@jest/globals";
|
|
1
|
+
import { describe, expect, test, afterEach } from "@jest/globals";
|
|
2
|
+
import { cp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
2
4
|
import path from "path";
|
|
3
5
|
import { cli, cliSnapshot } from "./helpers";
|
|
4
6
|
describe("lint", () => {
|
|
@@ -71,4 +73,64 @@ describe("lint", () => {
|
|
|
71
73
|
await cliSnapshot(["lint"], { cwd }, 1);
|
|
72
74
|
});
|
|
73
75
|
});
|
|
76
|
+
describe("migration trimming via check-limit", () => {
|
|
77
|
+
const migrateFixturesDir = path.join(import.meta.dirname, "migrate");
|
|
78
|
+
const tempDirs = [];
|
|
79
|
+
afterEach(async () => {
|
|
80
|
+
for (const dir of tempDirs) {
|
|
81
|
+
await rm(dir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
tempDirs.length = 0;
|
|
84
|
+
});
|
|
85
|
+
async function makeWithNextLintFixture(checkLimit) {
|
|
86
|
+
const dest = path.join(migrateFixturesDir, `_tmp_lint_with-next_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`);
|
|
87
|
+
await cp(path.join(migrateFixturesDir, "with-next"), dest, {
|
|
88
|
+
recursive: true,
|
|
89
|
+
});
|
|
90
|
+
tempDirs.push(dest);
|
|
91
|
+
// Empty lints/ → collectLintRules picks it up so lintoko runs cleanly
|
|
92
|
+
// (no rules → no violations → exit 0), preventing assertions from
|
|
93
|
+
// passing by coincidence on an unrelated lintoko failure.
|
|
94
|
+
await mkdir(path.join(dest, "lints"), { recursive: true });
|
|
95
|
+
let toml = readFileSync(path.join(dest, "mops.toml"), "utf-8").replace('moc = "1.5.0"', 'moc = "1.5.0"\nlintoko = "0.7.0"');
|
|
96
|
+
if (checkLimit !== undefined) {
|
|
97
|
+
toml = toml.replace('next = "next-migration"', `next = "next-migration"\ncheck-limit = ${checkLimit}`);
|
|
98
|
+
}
|
|
99
|
+
await writeFile(path.join(dest, "mops.toml"), toml);
|
|
100
|
+
return dest;
|
|
101
|
+
}
|
|
102
|
+
test("check-limit=1 trims old chain migrations from lint", async () => {
|
|
103
|
+
// with-next has 3 chain files + 1 next file. check-limit=1 keeps only
|
|
104
|
+
// the next file → 3 chain files trimmed from lint.
|
|
105
|
+
const cwd = await makeWithNextLintFixture(1);
|
|
106
|
+
const result = await cli(["lint", "--verbose"], { cwd });
|
|
107
|
+
expect(result.exitCode).toBe(0);
|
|
108
|
+
expect(result.stdout).toMatch(/Trimmed 3 migration file\(s\) \(check-limit\)/);
|
|
109
|
+
expect(result.stdout).not.toMatch(/20250101_000000_Init\.mo/);
|
|
110
|
+
expect(result.stdout).not.toMatch(/20250201_000000_AddName\.mo/);
|
|
111
|
+
expect(result.stdout).not.toMatch(/20250301_000000_AddEmail\.mo/);
|
|
112
|
+
expect(result.stdout).toMatch(/20250401_000000_RenameId\.mo/);
|
|
113
|
+
});
|
|
114
|
+
test("no check-limit → all migration files are linted", async () => {
|
|
115
|
+
const cwd = await makeWithNextLintFixture();
|
|
116
|
+
const result = await cli(["lint", "--verbose"], { cwd });
|
|
117
|
+
expect(result.exitCode).toBe(0);
|
|
118
|
+
expect(result.stdout).not.toMatch(/Trimmed \d+ migration file/);
|
|
119
|
+
expect(result.stdout).toMatch(/20250101_000000_Init\.mo/);
|
|
120
|
+
expect(result.stdout).toMatch(/20250401_000000_RenameId\.mo/);
|
|
121
|
+
});
|
|
122
|
+
test("explicit filter bypasses trimming so user can target a chain file", async () => {
|
|
123
|
+
const cwd = await makeWithNextLintFixture(1);
|
|
124
|
+
const result = await cli(["lint", "Init", "--verbose"], { cwd });
|
|
125
|
+
expect(result.exitCode).toBe(0);
|
|
126
|
+
expect(result.stdout).not.toMatch(/Trimmed \d+ migration file/);
|
|
127
|
+
expect(result.stdout).toMatch(/20250101_000000_Init\.mo/);
|
|
128
|
+
});
|
|
129
|
+
test("invalid check-limit fails `mops lint` (consistent with `mops check`)", async () => {
|
|
130
|
+
const cwd = await makeWithNextLintFixture(0);
|
|
131
|
+
const result = await cli(["lint"], { cwd });
|
|
132
|
+
expect(result.exitCode).toBe(1);
|
|
133
|
+
expect(result.stderr).toMatch(/check-limit must be a positive integer/);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
74
136
|
});
|
package/helpers/migrations.ts
CHANGED
|
@@ -10,7 +10,8 @@ import { rm } from "node:fs/promises";
|
|
|
10
10
|
import chalk from "chalk";
|
|
11
11
|
import { cliError } from "../error.js";
|
|
12
12
|
import { getRootDir, resolveConfigPath } from "../mops.js";
|
|
13
|
-
import {
|
|
13
|
+
import { resolveCanisterConfigs } from "./resolve-canisters.js";
|
|
14
|
+
import { Config, MigrationsConfig } from "../types.js";
|
|
14
15
|
|
|
15
16
|
function stagedMigrationsDir(chainDir: string, canisterName: string): string {
|
|
16
17
|
return join(dirname(chainDir), `.migrations-${canisterName}`);
|
|
@@ -95,21 +96,29 @@ export function validateMigrationsConfig(
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
interface MigrationChain {
|
|
100
|
+
chainDir: string;
|
|
101
|
+
nextDir?: string;
|
|
102
|
+
/** Entries to pass to moc, in order, after `*-limit` trimming. */
|
|
103
|
+
included: { file: string; dir: string }[];
|
|
104
|
+
/** Absolute paths of chain files dropped by trimming (next is never dropped). */
|
|
105
|
+
excludedChainFiles: string[];
|
|
106
|
+
/** True when `*-limit` excluded any entries. */
|
|
107
|
+
isTrimming: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Resolve the active migration chain for a canister: validate config, discover
|
|
112
|
+
* files, and apply `check-limit` / `build-limit`. Single source of truth for
|
|
113
|
+
* the trim semantics shared by `prepareMigrationArgs` (which stages `included`
|
|
114
|
+
* for moc) and `getTrimmedMigrationFiles` (which feeds `excludedChainFiles`
|
|
115
|
+
* to lint).
|
|
116
|
+
*/
|
|
117
|
+
function resolveMigrationChain(
|
|
118
|
+
migrations: MigrationsConfig,
|
|
100
119
|
canisterName: string,
|
|
101
120
|
mode: "check" | "build",
|
|
102
|
-
|
|
103
|
-
): Promise<MigrationArgsResult> {
|
|
104
|
-
const noOp: MigrationArgsResult = {
|
|
105
|
-
migrationArgs: [],
|
|
106
|
-
cleanup: async () => {},
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
if (!migrations) {
|
|
110
|
-
return noOp;
|
|
111
|
-
}
|
|
112
|
-
|
|
121
|
+
): MigrationChain {
|
|
113
122
|
validateMigrationsConfig(migrations, canisterName);
|
|
114
123
|
|
|
115
124
|
const chainDir = resolveConfigPath(migrations.chain);
|
|
@@ -126,25 +135,46 @@ export async function prepareMigrationArgs(
|
|
|
126
135
|
}
|
|
127
136
|
|
|
128
137
|
const chainFiles = getMigrationFiles(chainDir);
|
|
129
|
-
|
|
130
138
|
if (nextFile) {
|
|
131
139
|
validateNextMigrationOrder(chainFiles, nextFile);
|
|
132
140
|
}
|
|
133
141
|
|
|
134
|
-
// Treat chain + next as one virtual merged list
|
|
135
|
-
|
|
136
|
-
const allMigrations: MigrationEntry[] = chainFiles.map((f) => ({
|
|
142
|
+
// Treat chain + next as one virtual merged list; `next` is always last.
|
|
143
|
+
const all: { file: string; dir: string }[] = chainFiles.map((f) => ({
|
|
137
144
|
file: f,
|
|
138
145
|
dir: chainDir,
|
|
139
146
|
}));
|
|
140
147
|
if (nextFile && nextDir) {
|
|
141
|
-
|
|
148
|
+
all.push({ file: nextFile, dir: nextDir });
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
const limit =
|
|
145
152
|
mode === "check" ? migrations["check-limit"] : migrations["build-limit"];
|
|
146
|
-
const isTrimming = limit !== undefined && limit <
|
|
147
|
-
const
|
|
153
|
+
const isTrimming = limit !== undefined && limit < all.length;
|
|
154
|
+
const included = isTrimming ? all.slice(-limit!) : all;
|
|
155
|
+
// Dropped entries are always a chain-only prefix (next sorts last).
|
|
156
|
+
const excludedChainFiles = all
|
|
157
|
+
.slice(0, all.length - included.length)
|
|
158
|
+
.map((e) => resolve(e.dir, e.file));
|
|
159
|
+
|
|
160
|
+
return { chainDir, nextDir, included, excludedChainFiles, isTrimming };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function prepareMigrationArgs(
|
|
164
|
+
migrations: MigrationsConfig | undefined,
|
|
165
|
+
canisterName: string,
|
|
166
|
+
mode: "check" | "build",
|
|
167
|
+
verbose?: boolean,
|
|
168
|
+
): Promise<MigrationArgsResult> {
|
|
169
|
+
if (!migrations) {
|
|
170
|
+
return { migrationArgs: [], cleanup: async () => {} };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { chainDir, nextDir, included, excludedChainFiles, isTrimming } =
|
|
174
|
+
resolveMigrationChain(migrations, canisterName, mode);
|
|
175
|
+
|
|
176
|
+
const hasNext = included.some((e) => e.dir === nextDir);
|
|
177
|
+
const needsTempDir = hasNext || isTrimming;
|
|
148
178
|
|
|
149
179
|
if (!needsTempDir) {
|
|
150
180
|
return {
|
|
@@ -153,9 +183,9 @@ export async function prepareMigrationArgs(
|
|
|
153
183
|
};
|
|
154
184
|
}
|
|
155
185
|
|
|
156
|
-
// Shortcut:
|
|
157
|
-
//
|
|
158
|
-
if (
|
|
186
|
+
// Shortcut: only the pending next migration is included → point moc at
|
|
187
|
+
// next-migration/ so diagnostics use the real path instead of the temp dir.
|
|
188
|
+
if (nextDir && included.length === 1 && included[0]!.dir === nextDir) {
|
|
159
189
|
const migrationArgs = [`--enhanced-migration=${nextDir}`];
|
|
160
190
|
if (isTrimming) {
|
|
161
191
|
migrationArgs.push("-A=M0254");
|
|
@@ -168,20 +198,17 @@ export async function prepareMigrationArgs(
|
|
|
168
198
|
mkdirSync(tempDir, { recursive: true });
|
|
169
199
|
writeFileSync(join(tempDir, ".gitignore"), "*\n");
|
|
170
200
|
|
|
171
|
-
const
|
|
172
|
-
? allMigrations.slice(-limit)
|
|
173
|
-
: allMigrations;
|
|
174
|
-
|
|
175
|
-
for (const { file, dir } of filesToInclude) {
|
|
201
|
+
for (const { file, dir } of included) {
|
|
176
202
|
symlinkSync(resolve(dir, file), join(tempDir, file));
|
|
177
203
|
}
|
|
178
204
|
|
|
179
205
|
if (verbose) {
|
|
206
|
+
const totalCount = included.length + excludedChainFiles.length;
|
|
180
207
|
console.log(
|
|
181
208
|
chalk.blue("migrations"),
|
|
182
209
|
chalk.gray(
|
|
183
|
-
`Prepared ${
|
|
184
|
-
(isTrimming ? ` (trimmed from ${
|
|
210
|
+
`Prepared ${included.length} migration(s) for ${canisterName}` +
|
|
211
|
+
(isTrimming ? ` (trimmed from ${totalCount})` : ""),
|
|
185
212
|
),
|
|
186
213
|
);
|
|
187
214
|
}
|
|
@@ -198,3 +225,29 @@ export async function prepareMigrationArgs(
|
|
|
198
225
|
},
|
|
199
226
|
};
|
|
200
227
|
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Absolute paths of chain migration files that `mops lint` should skip,
|
|
231
|
+
* mirroring the `check-limit` trimming applied to `moc` during `mops check`.
|
|
232
|
+
* Validates the migrations config along the way, so misconfig surfaces here
|
|
233
|
+
* just as it does in `mops check` (consistent failure across commands).
|
|
234
|
+
*/
|
|
235
|
+
export function getTrimmedMigrationFiles(config: Config): Set<string> {
|
|
236
|
+
const excluded = new Set<string>();
|
|
237
|
+
for (const [name, canister] of Object.entries(
|
|
238
|
+
resolveCanisterConfigs(config),
|
|
239
|
+
)) {
|
|
240
|
+
if (!canister.migrations) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const { excludedChainFiles } = resolveMigrationChain(
|
|
244
|
+
canister.migrations,
|
|
245
|
+
name,
|
|
246
|
+
"check",
|
|
247
|
+
);
|
|
248
|
+
for (const f of excludedChainFiles) {
|
|
249
|
+
excluded.add(f);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return excluded;
|
|
253
|
+
}
|
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
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 () => {
|
|
@@ -129,3 +129,88 @@ describe("install", () => {
|
|
|
129
129
|
}
|
|
130
130
|
});
|
|
131
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
|
+
});
|
|
216
|
+
});
|
package/tests/lint.test.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { describe, expect, test } from "@jest/globals";
|
|
1
|
+
import { describe, expect, test, afterEach } from "@jest/globals";
|
|
2
|
+
import { cp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
2
4
|
import path from "path";
|
|
3
5
|
import { cli, cliSnapshot } from "./helpers";
|
|
4
6
|
|
|
@@ -86,4 +88,86 @@ describe("lint", () => {
|
|
|
86
88
|
await cliSnapshot(["lint"], { cwd }, 1);
|
|
87
89
|
});
|
|
88
90
|
});
|
|
91
|
+
|
|
92
|
+
describe("migration trimming via check-limit", () => {
|
|
93
|
+
const migrateFixturesDir = path.join(import.meta.dirname, "migrate");
|
|
94
|
+
const tempDirs: string[] = [];
|
|
95
|
+
|
|
96
|
+
afterEach(async () => {
|
|
97
|
+
for (const dir of tempDirs) {
|
|
98
|
+
await rm(dir, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
tempDirs.length = 0;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
async function makeWithNextLintFixture(
|
|
104
|
+
checkLimit?: number,
|
|
105
|
+
): Promise<string> {
|
|
106
|
+
const dest = path.join(
|
|
107
|
+
migrateFixturesDir,
|
|
108
|
+
`_tmp_lint_with-next_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
109
|
+
);
|
|
110
|
+
await cp(path.join(migrateFixturesDir, "with-next"), dest, {
|
|
111
|
+
recursive: true,
|
|
112
|
+
});
|
|
113
|
+
tempDirs.push(dest);
|
|
114
|
+
|
|
115
|
+
// Empty lints/ → collectLintRules picks it up so lintoko runs cleanly
|
|
116
|
+
// (no rules → no violations → exit 0), preventing assertions from
|
|
117
|
+
// passing by coincidence on an unrelated lintoko failure.
|
|
118
|
+
await mkdir(path.join(dest, "lints"), { recursive: true });
|
|
119
|
+
|
|
120
|
+
let toml = readFileSync(path.join(dest, "mops.toml"), "utf-8").replace(
|
|
121
|
+
'moc = "1.5.0"',
|
|
122
|
+
'moc = "1.5.0"\nlintoko = "0.7.0"',
|
|
123
|
+
);
|
|
124
|
+
if (checkLimit !== undefined) {
|
|
125
|
+
toml = toml.replace(
|
|
126
|
+
'next = "next-migration"',
|
|
127
|
+
`next = "next-migration"\ncheck-limit = ${checkLimit}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
await writeFile(path.join(dest, "mops.toml"), toml);
|
|
131
|
+
return dest;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
test("check-limit=1 trims old chain migrations from lint", async () => {
|
|
135
|
+
// with-next has 3 chain files + 1 next file. check-limit=1 keeps only
|
|
136
|
+
// the next file → 3 chain files trimmed from lint.
|
|
137
|
+
const cwd = await makeWithNextLintFixture(1);
|
|
138
|
+
const result = await cli(["lint", "--verbose"], { cwd });
|
|
139
|
+
expect(result.exitCode).toBe(0);
|
|
140
|
+
expect(result.stdout).toMatch(
|
|
141
|
+
/Trimmed 3 migration file\(s\) \(check-limit\)/,
|
|
142
|
+
);
|
|
143
|
+
expect(result.stdout).not.toMatch(/20250101_000000_Init\.mo/);
|
|
144
|
+
expect(result.stdout).not.toMatch(/20250201_000000_AddName\.mo/);
|
|
145
|
+
expect(result.stdout).not.toMatch(/20250301_000000_AddEmail\.mo/);
|
|
146
|
+
expect(result.stdout).toMatch(/20250401_000000_RenameId\.mo/);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("no check-limit → all migration files are linted", async () => {
|
|
150
|
+
const cwd = await makeWithNextLintFixture();
|
|
151
|
+
const result = await cli(["lint", "--verbose"], { cwd });
|
|
152
|
+
expect(result.exitCode).toBe(0);
|
|
153
|
+
expect(result.stdout).not.toMatch(/Trimmed \d+ migration file/);
|
|
154
|
+
expect(result.stdout).toMatch(/20250101_000000_Init\.mo/);
|
|
155
|
+
expect(result.stdout).toMatch(/20250401_000000_RenameId\.mo/);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("explicit filter bypasses trimming so user can target a chain file", async () => {
|
|
159
|
+
const cwd = await makeWithNextLintFixture(1);
|
|
160
|
+
const result = await cli(["lint", "Init", "--verbose"], { cwd });
|
|
161
|
+
expect(result.exitCode).toBe(0);
|
|
162
|
+
expect(result.stdout).not.toMatch(/Trimmed \d+ migration file/);
|
|
163
|
+
expect(result.stdout).toMatch(/20250101_000000_Init\.mo/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("invalid check-limit fails `mops lint` (consistent with `mops check`)", async () => {
|
|
167
|
+
const cwd = await makeWithNextLintFixture(0);
|
|
168
|
+
const result = await cli(["lint"], { cwd });
|
|
169
|
+
expect(result.exitCode).toBe(1);
|
|
170
|
+
expect(result.stderr).toMatch(/check-limit must be a positive integer/);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
89
173
|
});
|