ic-mops 2.15.0 → 2.15.2
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 +12 -0
- package/bundle/cli.tgz +0 -0
- package/cli.ts +31 -11
- package/commands/bench-replica.ts +2 -1
- package/commands/bench.ts +32 -9
- package/commands/check-stable.ts +20 -3
- package/commands/check.ts +6 -1
- package/dist/cli.js +18 -10
- package/dist/commands/bench-replica.js +3 -1
- package/dist/commands/bench.js +21 -4
- package/dist/commands/check-stable.d.ts +2 -1
- package/dist/commands/check-stable.js +8 -2
- package/dist/commands/check.js +6 -1
- package/dist/helpers/autofix-motoko.js +19 -2
- package/dist/helpers/migrations.d.ts +16 -0
- package/dist/helpers/migrations.js +71 -1
- package/dist/helpers/parse-most.d.ts +5 -0
- package/dist/helpers/parse-most.js +28 -0
- package/dist/helpers/pocket-ic-client.d.ts +2 -2
- package/dist/helpers/pocket-ic-client.js +8 -4
- package/dist/package.json +1 -1
- package/dist/tests/check-fix.test.js +14 -2
- package/dist/tests/check-stable.test.js +15 -0
- package/dist/tests/check.test.js +5 -0
- package/dist/tests/lint.test.js +1 -1
- package/dist/tests/migrations-most.test.d.ts +1 -0
- package/dist/tests/migrations-most.test.js +47 -0
- package/helpers/autofix-motoko.ts +23 -2
- package/helpers/migrations.ts +117 -0
- package/helpers/parse-most.ts +32 -0
- package/helpers/pocket-ic-client.ts +11 -5
- package/package.json +1 -1
- package/tests/__snapshots__/check-stable.test.ts.snap +11 -0
- package/tests/__snapshots__/lint.test.ts.snap +29 -1
- package/tests/check-fix.test.ts +26 -2
- package/tests/check-stable/check-limit-warning/deployed.most +8 -0
- package/tests/check-stable/check-limit-warning/migrations/20250101_000000_Init.mo +5 -0
- package/tests/check-stable/check-limit-warning/migrations/20250201_000000_AddField.mo +9 -0
- package/tests/check-stable/check-limit-warning/migrations/20250301_000000_AddD.mo +10 -0
- package/tests/check-stable/check-limit-warning/mops.toml +15 -0
- package/tests/check-stable/check-limit-warning/src/main.mo +12 -0
- package/tests/check-stable.test.ts +24 -0
- package/tests/check.test.ts +9 -0
- package/tests/lint-extra-example-rules/lint/migration-self-contained/migration-self-contained.toml +7 -0
- package/tests/lint-extra-example-rules/migrations/20250101_000000_Init.mo +11 -0
- package/tests/lint-extra-example-rules/mops.toml +1 -0
- package/tests/lint.test.ts +1 -1
- package/tests/migrations-most.test.ts +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Next
|
|
4
4
|
|
|
5
|
+
## 2.15.2
|
|
6
|
+
|
|
7
|
+
- `mops check-stable` (and the stable check inside `mops check`) reports when `[canisters.<name>.migrations].check-limit` is set but more migrations are pending than the limit allows. If the compatibility check failed, the check-limit diagnostic replaces the misleading `moc` error; if it passed anyway, a warning is shown. Compares the deployed `.most` baseline against the local chain; use `--no-check-limit` to suppress.
|
|
8
|
+
|
|
9
|
+
## 2.15.1
|
|
10
|
+
|
|
11
|
+
- `mops check --fix` no longer aborts when a fixable file is read-only (e.g. a frozen migration chain file deliberately `chmod`'d to remove write access). The autofixer now skips such files with a warning and continues fixing the rest, instead of crashing the whole run on `EACCES`/`EPERM`.
|
|
12
|
+
|
|
13
|
+
- Load the PocketIC client lazily, only when a command actually starts a replica. Commands like `mops check`, `mops build`, and `mops install` no longer pay to load it (and its `@icp-sdk/core` dependency) at startup. This also unblocks running the CLI via `tsx` in local dev, where `pic-js-mops` (shipped as ESM without `"type": "module"`) fails to resolve as a static import.
|
|
14
|
+
|
|
15
|
+
- `mops bench --verbose` is now actually verbose. It prints the benchmark pipeline up front — compiler version, replica + version, GC, profile, and whether the wasm is optimized (`dfx` post-optimizes with `optimize: "cycles"` via ic-wasm on deploy; `pocket-ic` runs the raw `moc` output) — logs the full `moc` build command, and streams the compiler and `dfx` output instead of capturing and discarding it. Notably this surfaces dfx's `WARNING: Failed to optimize the Wasm module`, which dfx prints (and then silently deploys the unoptimized module) when `optimize: "cycles"` fails — e.g. on multi-value modules that the bundled ic-wasm can't process. Previously all of this was hidden even with `--verbose`.
|
|
16
|
+
|
|
5
17
|
## 2.15.0
|
|
6
18
|
|
|
7
19
|
- Fix `mops check --fix` corrupting source on lines containing multi-byte UTF-8 characters (e.g. `Char.toNat32('京')` dropping its trailing `)`). The autofixer was feeding moc's UTF-8 byte columns into LSP's UTF-16 position API, mis-applying every edit past the first non-ASCII byte on the line. When moc emits `byte_start`/`byte_end` (1.10.0 and newer) the fixer now applies edits byte-accurately; older moc still falls back to the line+column path (unchanged behavior, still ASCII-only).
|
package/bundle/cli.tgz
CHANGED
|
Binary file
|
package/cli.ts
CHANGED
|
@@ -102,14 +102,26 @@ function parseExtraArgs(variadicArgs?: string[]): {
|
|
|
102
102
|
// Shared `--help` section describing the enhanced migration `check-limit`
|
|
103
103
|
// trimming and its override flag, so the limit behaviour is discoverable
|
|
104
104
|
// from `--help`. `withFix` appends the `--fix` hint for commands that support it.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
// `withPendingWarning` adds the stable-check pending-migration warning (check / check-stable only).
|
|
106
|
+
function enhancedMigrationHelp(
|
|
107
|
+
options: {
|
|
108
|
+
withFix?: boolean;
|
|
109
|
+
withPendingWarning?: boolean;
|
|
110
|
+
} = {},
|
|
111
|
+
): string {
|
|
112
|
+
const example = options.withFix ? " (e.g. to --fix older migrations)" : "";
|
|
113
|
+
let text =
|
|
108
114
|
"\nEnhanced migration ([canisters.<name>.migrations]):\n" +
|
|
109
115
|
" The canister is checked against its migration chain. [migrations].check-limit\n" +
|
|
110
116
|
" trims it to the last N migrations (older ones are skipped). Pass --no-check-limit\n" +
|
|
111
|
-
` to use the full chain${example}
|
|
112
|
-
)
|
|
117
|
+
` to use the full chain${example}.`;
|
|
118
|
+
if (options.withPendingWarning) {
|
|
119
|
+
text +=
|
|
120
|
+
"\n When check-limit is set, the stable check reports if more migrations are pending\n" +
|
|
121
|
+
" (relative to the deployed .most baseline) than the limit allows — as an error if\n" +
|
|
122
|
+
" compat failed, otherwise a warning.";
|
|
123
|
+
}
|
|
124
|
+
return text;
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
program.name("mops");
|
|
@@ -372,14 +384,17 @@ program
|
|
|
372
384
|
.addOption(
|
|
373
385
|
new Option(
|
|
374
386
|
"--no-check-limit",
|
|
375
|
-
"
|
|
387
|
+
"Use the full migration chain, ignoring [migrations].check-limit; also suppresses the pending-migration warning",
|
|
376
388
|
),
|
|
377
389
|
)
|
|
378
390
|
.addHelpText(
|
|
379
391
|
"after",
|
|
380
392
|
"\nArguments after -- are forwarded directly to moc, e.g.:\n $ mops check -- -Werror",
|
|
381
393
|
)
|
|
382
|
-
.addHelpText(
|
|
394
|
+
.addHelpText(
|
|
395
|
+
"after",
|
|
396
|
+
enhancedMigrationHelp({ withFix: true, withPendingWarning: true }),
|
|
397
|
+
)
|
|
383
398
|
.allowUnknownOption(true)
|
|
384
399
|
.action(async (args, options) => {
|
|
385
400
|
checkConfigFile(true);
|
|
@@ -419,14 +434,14 @@ program
|
|
|
419
434
|
.addOption(
|
|
420
435
|
new Option(
|
|
421
436
|
"--no-check-limit",
|
|
422
|
-
"
|
|
437
|
+
"Use the full migration chain, ignoring [migrations].check-limit; also suppresses the pending-migration warning",
|
|
423
438
|
),
|
|
424
439
|
)
|
|
425
440
|
.addHelpText(
|
|
426
441
|
"after",
|
|
427
442
|
"\nArguments after -- are forwarded directly to moc, e.g.:\n $ mops check-stable -- -Werror",
|
|
428
443
|
)
|
|
429
|
-
.addHelpText("after", enhancedMigrationHelp(
|
|
444
|
+
.addHelpText("after", enhancedMigrationHelp({ withPendingWarning: true }))
|
|
430
445
|
.allowUnknownOption(true)
|
|
431
446
|
.action(async (args, options) => {
|
|
432
447
|
checkConfigFile(true);
|
|
@@ -543,7 +558,12 @@ program
|
|
|
543
558
|
),
|
|
544
559
|
)
|
|
545
560
|
// .addOption(new Option('--force-gc', 'Force GC'))
|
|
546
|
-
.addOption(
|
|
561
|
+
.addOption(
|
|
562
|
+
new Option(
|
|
563
|
+
"--verbose",
|
|
564
|
+
"Print the benchmark pipeline (compiler, replica, GC, optimization) and stream compiler/replica output, including dfx optimization warnings",
|
|
565
|
+
),
|
|
566
|
+
)
|
|
547
567
|
.action(async (filter, options) => {
|
|
548
568
|
checkConfigFile(true);
|
|
549
569
|
await installAll({
|
|
@@ -974,7 +994,7 @@ program
|
|
|
974
994
|
"after",
|
|
975
995
|
"\nArguments after -- are forwarded directly to lintoko, e.g.:\n $ mops lint -- --severity warning",
|
|
976
996
|
)
|
|
977
|
-
.addHelpText("after", enhancedMigrationHelp(true))
|
|
997
|
+
.addHelpText("after", enhancedMigrationHelp({ withFix: true }))
|
|
978
998
|
.allowUnknownOption(true)
|
|
979
999
|
.action(async (filter, options) => {
|
|
980
1000
|
checkConfigFile(true);
|
|
@@ -74,7 +74,8 @@ export class BenchReplica {
|
|
|
74
74
|
if (this.type === "dfx" || this.type === "dfx-pocket-ic") {
|
|
75
75
|
await execaCommand(
|
|
76
76
|
`dfx deploy ${name} --mode reinstall --yes --identity anonymous`,
|
|
77
|
-
|
|
77
|
+
// `inherit` so dfx output is streamed under --verbose (incl. its `Failed to optimize` warning)
|
|
78
|
+
{ cwd, stdio: this.verbose ? "inherit" : ["pipe", "ignore", "pipe"] },
|
|
78
79
|
);
|
|
79
80
|
let canisterId = execSync(`dfx canister id ${name}`, { cwd })
|
|
80
81
|
.toString()
|
package/commands/bench.ts
CHANGED
|
@@ -101,7 +101,28 @@ export async function bench(
|
|
|
101
101
|
|
|
102
102
|
warnIfDfxReplica(replicaType, optionsArg.replica === "dfx");
|
|
103
103
|
|
|
104
|
-
options.verbose
|
|
104
|
+
if (options.verbose) {
|
|
105
|
+
// `dfx` post-optimizes the wasm on deploy (`optimize: "cycles"`, via ic-wasm);
|
|
106
|
+
// `pocket-ic` runs the raw moc output. This changes instruction counts, so surface it.
|
|
107
|
+
let optimize =
|
|
108
|
+
replicaType === "dfx" || replicaType === "dfx-pocket-ic"
|
|
109
|
+
? 'dfx `optimize: "cycles"` (ic-wasm) on deploy'
|
|
110
|
+
: "none (raw moc output)";
|
|
111
|
+
console.log(chalk.gray("Benchmark pipeline:"));
|
|
112
|
+
console.log(chalk.gray(` compiler: moc ${options.compilerVersion}`));
|
|
113
|
+
console.log(
|
|
114
|
+
chalk.gray(
|
|
115
|
+
` replica: ${replicaType}${options.replicaVersion ? ` ${options.replicaVersion}` : ""}`,
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
console.log(
|
|
119
|
+
chalk.gray(
|
|
120
|
+
` gc: ${options.gc}${options.forceGc ? " (forced)" : ""}`,
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
console.log(chalk.gray(` profile: ${options.profile}`));
|
|
124
|
+
console.log(chalk.gray(` optimize: ${optimize}`));
|
|
125
|
+
}
|
|
105
126
|
|
|
106
127
|
let replica = new BenchReplica(replicaType, options.verbose);
|
|
107
128
|
|
|
@@ -303,14 +324,16 @@ async function deployBenchFile(
|
|
|
303
324
|
// build canister
|
|
304
325
|
let mocPath = getMocPath();
|
|
305
326
|
let mocArgs = getMocArgs(options);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
{
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
327
|
+
let buildCmd = `${mocPath} -c --idl canister.mo ${globalMocArgs.join(" ")} ${mocArgs} ${(await sources({ cwd: tempDir })).join(" ")}`;
|
|
328
|
+
if (options.verbose) {
|
|
329
|
+
console.log(chalk.gray(`[${canisterName}] ${buildCmd}`));
|
|
330
|
+
console.time(`build ${canisterName}`);
|
|
331
|
+
}
|
|
332
|
+
await execaCommand(buildCmd, {
|
|
333
|
+
cwd: tempDir,
|
|
334
|
+
// `inherit` so the compiler output (warnings/errors) is streamed under --verbose
|
|
335
|
+
stdio: options.verbose ? "inherit" : ["pipe", "ignore", "pipe"],
|
|
336
|
+
});
|
|
314
337
|
options.verbose && console.timeEnd(`build ${canisterName}`);
|
|
315
338
|
|
|
316
339
|
// deploy canister
|
package/commands/check-stable.ts
CHANGED
|
@@ -4,9 +4,13 @@ import { rm } from "node:fs/promises";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { execa } from "execa";
|
|
6
6
|
import { cliError } from "../error.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
getCheckLimitPendingIssue,
|
|
9
|
+
prepareMigrationArgs,
|
|
10
|
+
reportCheckLimitPendingIssue,
|
|
11
|
+
} from "../helpers/migrations.js";
|
|
8
12
|
import { getGlobalMocArgs, readConfig, resolveConfigPath } from "../mops.js";
|
|
9
|
-
import { CanisterConfig } from "../types.js";
|
|
13
|
+
import { CanisterConfig, MigrationsConfig } from "../types.js";
|
|
10
14
|
import {
|
|
11
15
|
filterCanisters,
|
|
12
16
|
looksLikeFile,
|
|
@@ -103,6 +107,7 @@ export async function checkStable(
|
|
|
103
107
|
mocPath,
|
|
104
108
|
globalMocArgs,
|
|
105
109
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
110
|
+
migrations: canister.migrations,
|
|
106
111
|
options,
|
|
107
112
|
});
|
|
108
113
|
} finally {
|
|
@@ -146,6 +151,7 @@ export async function checkStable(
|
|
|
146
151
|
globalMocArgs,
|
|
147
152
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
148
153
|
sources,
|
|
154
|
+
migrations: canister.migrations,
|
|
149
155
|
options,
|
|
150
156
|
});
|
|
151
157
|
} finally {
|
|
@@ -173,6 +179,7 @@ export interface RunStableCheckParams {
|
|
|
173
179
|
globalMocArgs: string[];
|
|
174
180
|
canisterArgs: string[];
|
|
175
181
|
sources?: string[];
|
|
182
|
+
migrations?: MigrationsConfig;
|
|
176
183
|
options?: Partial<CheckStableOptions>;
|
|
177
184
|
}
|
|
178
185
|
|
|
@@ -240,7 +247,17 @@ export async function runStableCheck(
|
|
|
240
247
|
reject: false,
|
|
241
248
|
});
|
|
242
249
|
|
|
243
|
-
|
|
250
|
+
const issue = getCheckLimitPendingIssue(
|
|
251
|
+
params.migrations,
|
|
252
|
+
canisterName,
|
|
253
|
+
oldMostPath,
|
|
254
|
+
options.checkLimit === false,
|
|
255
|
+
isOldMostFile,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
if (issue) {
|
|
259
|
+
reportCheckLimitPendingIssue(issue, result.exitCode !== 0);
|
|
260
|
+
} else if (result.exitCode !== 0) {
|
|
244
261
|
if (result.stderr) {
|
|
245
262
|
console.error(result.stderr);
|
|
246
263
|
}
|
package/commands/check.ts
CHANGED
|
@@ -230,7 +230,12 @@ async function checkCanisters(
|
|
|
230
230
|
globalMocArgs,
|
|
231
231
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
232
232
|
sources,
|
|
233
|
-
|
|
233
|
+
migrations: canister.migrations,
|
|
234
|
+
options: {
|
|
235
|
+
verbose: options.verbose,
|
|
236
|
+
extraArgs: options.extraArgs,
|
|
237
|
+
checkLimit: options.checkLimit,
|
|
238
|
+
},
|
|
234
239
|
});
|
|
235
240
|
}
|
|
236
241
|
} finally {
|
package/dist/cli.js
CHANGED
|
@@ -65,12 +65,20 @@ function parseExtraArgs(variadicArgs) {
|
|
|
65
65
|
// Shared `--help` section describing the enhanced migration `check-limit`
|
|
66
66
|
// trimming and its override flag, so the limit behaviour is discoverable
|
|
67
67
|
// from `--help`. `withFix` appends the `--fix` hint for commands that support it.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
// `withPendingWarning` adds the stable-check pending-migration warning (check / check-stable only).
|
|
69
|
+
function enhancedMigrationHelp(options = {}) {
|
|
70
|
+
const example = options.withFix ? " (e.g. to --fix older migrations)" : "";
|
|
71
|
+
let text = "\nEnhanced migration ([canisters.<name>.migrations]):\n" +
|
|
71
72
|
" The canister is checked against its migration chain. [migrations].check-limit\n" +
|
|
72
73
|
" trims it to the last N migrations (older ones are skipped). Pass --no-check-limit\n" +
|
|
73
|
-
` to use the full chain${example}
|
|
74
|
+
` to use the full chain${example}.`;
|
|
75
|
+
if (options.withPendingWarning) {
|
|
76
|
+
text +=
|
|
77
|
+
"\n When check-limit is set, the stable check reports if more migrations are pending\n" +
|
|
78
|
+
" (relative to the deployed .most baseline) than the limit allows — as an error if\n" +
|
|
79
|
+
" compat failed, otherwise a warning.";
|
|
80
|
+
}
|
|
81
|
+
return text;
|
|
74
82
|
}
|
|
75
83
|
program.name("mops");
|
|
76
84
|
// --version
|
|
@@ -286,9 +294,9 @@ program
|
|
|
286
294
|
.description("Check Motoko canisters or files for syntax errors and type issues. Arguments can be canister names or file paths. If no arguments are given, checks all canisters from mops.toml. Also runs stable compatibility checks for canisters with [check-stable] configured, and runs linting if lintoko is configured in [toolchain]")
|
|
287
295
|
.option("--verbose", "Verbose console output")
|
|
288
296
|
.addOption(new Option("--fix", "Apply autofixes to all files, including transitively imported ones"))
|
|
289
|
-
.addOption(new Option("--no-check-limit", "
|
|
297
|
+
.addOption(new Option("--no-check-limit", "Use the full migration chain, ignoring [migrations].check-limit; also suppresses the pending-migration warning"))
|
|
290
298
|
.addHelpText("after", "\nArguments after -- are forwarded directly to moc, e.g.:\n $ mops check -- -Werror")
|
|
291
|
-
.addHelpText("after", enhancedMigrationHelp(true))
|
|
299
|
+
.addHelpText("after", enhancedMigrationHelp({ withFix: true, withPendingWarning: true }))
|
|
292
300
|
.allowUnknownOption(true)
|
|
293
301
|
.action(async (args, options) => {
|
|
294
302
|
checkConfigFile(true);
|
|
@@ -321,9 +329,9 @@ program
|
|
|
321
329
|
.command("check-stable [args...]")
|
|
322
330
|
.description("Check stable variable compatibility. With no arguments, checks all canisters with [check-stable] configured. Arguments can be canister names or an old file path followed by an optional canister name")
|
|
323
331
|
.option("--verbose", "Verbose console output")
|
|
324
|
-
.addOption(new Option("--no-check-limit", "
|
|
332
|
+
.addOption(new Option("--no-check-limit", "Use the full migration chain, ignoring [migrations].check-limit; also suppresses the pending-migration warning"))
|
|
325
333
|
.addHelpText("after", "\nArguments after -- are forwarded directly to moc, e.g.:\n $ mops check-stable -- -Werror")
|
|
326
|
-
.addHelpText("after", enhancedMigrationHelp(
|
|
334
|
+
.addHelpText("after", enhancedMigrationHelp({ withPendingWarning: true }))
|
|
327
335
|
.allowUnknownOption(true)
|
|
328
336
|
.action(async (args, options) => {
|
|
329
337
|
checkConfigFile(true);
|
|
@@ -393,7 +401,7 @@ program
|
|
|
393
401
|
.addOption(new Option("--save", "Save benchmark results to .bench/<filename>.json"))
|
|
394
402
|
.addOption(new Option("--compare", "Run benchmark and compare results with .bench/<filename>.json"))
|
|
395
403
|
// .addOption(new Option('--force-gc', 'Force GC'))
|
|
396
|
-
.addOption(new Option("--verbose", "
|
|
404
|
+
.addOption(new Option("--verbose", "Print the benchmark pipeline (compiler, replica, GC, optimization) and stream compiler/replica output, including dfx optimization warnings"))
|
|
397
405
|
.action(async (filter, options) => {
|
|
398
406
|
checkConfigFile(true);
|
|
399
407
|
await installAll({
|
|
@@ -693,7 +701,7 @@ program
|
|
|
693
701
|
.addOption(new Option("-r, --rules <directory...>", "Directories containing rules (can be used multiple times)"))
|
|
694
702
|
.addOption(new Option("--no-check-limit", "Lint the full migration chain, ignoring [migrations].check-limit"))
|
|
695
703
|
.addHelpText("after", "\nArguments after -- are forwarded directly to lintoko, e.g.:\n $ mops lint -- --severity warning")
|
|
696
|
-
.addHelpText("after", enhancedMigrationHelp(true))
|
|
704
|
+
.addHelpText("after", enhancedMigrationHelp({ withFix: true }))
|
|
697
705
|
.allowUnknownOption(true)
|
|
698
706
|
.action(async (filter, options) => {
|
|
699
707
|
checkConfigFile(true);
|
|
@@ -55,7 +55,9 @@ export class BenchReplica {
|
|
|
55
55
|
}
|
|
56
56
|
async deploy(name, wasm, cwd = process.cwd()) {
|
|
57
57
|
if (this.type === "dfx" || this.type === "dfx-pocket-ic") {
|
|
58
|
-
await execaCommand(`dfx deploy ${name} --mode reinstall --yes --identity anonymous`,
|
|
58
|
+
await execaCommand(`dfx deploy ${name} --mode reinstall --yes --identity anonymous`,
|
|
59
|
+
// `inherit` so dfx output is streamed under --verbose (incl. its `Failed to optimize` warning)
|
|
60
|
+
{ cwd, stdio: this.verbose ? "inherit" : ["pipe", "ignore", "pipe"] });
|
|
59
61
|
let canisterId = execSync(`dfx canister id ${name}`, { cwd })
|
|
60
62
|
.toString()
|
|
61
63
|
.trim();
|
package/dist/commands/bench.js
CHANGED
|
@@ -61,7 +61,19 @@ export async function bench(filter = "", optionsArg = {}) {
|
|
|
61
61
|
options.replicaVersion = config.toolchain?.["pocket-ic"] || "";
|
|
62
62
|
}
|
|
63
63
|
warnIfDfxReplica(replicaType, optionsArg.replica === "dfx");
|
|
64
|
-
options.verbose
|
|
64
|
+
if (options.verbose) {
|
|
65
|
+
// `dfx` post-optimizes the wasm on deploy (`optimize: "cycles"`, via ic-wasm);
|
|
66
|
+
// `pocket-ic` runs the raw moc output. This changes instruction counts, so surface it.
|
|
67
|
+
let optimize = replicaType === "dfx" || replicaType === "dfx-pocket-ic"
|
|
68
|
+
? 'dfx `optimize: "cycles"` (ic-wasm) on deploy'
|
|
69
|
+
: "none (raw moc output)";
|
|
70
|
+
console.log(chalk.gray("Benchmark pipeline:"));
|
|
71
|
+
console.log(chalk.gray(` compiler: moc ${options.compilerVersion}`));
|
|
72
|
+
console.log(chalk.gray(` replica: ${replicaType}${options.replicaVersion ? ` ${options.replicaVersion}` : ""}`));
|
|
73
|
+
console.log(chalk.gray(` gc: ${options.gc}${options.forceGc ? " (forced)" : ""}`));
|
|
74
|
+
console.log(chalk.gray(` profile: ${options.profile}`));
|
|
75
|
+
console.log(chalk.gray(` optimize: ${optimize}`));
|
|
76
|
+
}
|
|
65
77
|
let replica = new BenchReplica(replicaType, options.verbose);
|
|
66
78
|
let rootDir = getRootDir();
|
|
67
79
|
let globStr = "**/bench?(mark)/**/*.bench.mo";
|
|
@@ -207,10 +219,15 @@ async function deployBenchFile(file, options, replica, globalMocArgs) {
|
|
|
207
219
|
// build canister
|
|
208
220
|
let mocPath = getMocPath();
|
|
209
221
|
let mocArgs = getMocArgs(options);
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
let buildCmd = `${mocPath} -c --idl canister.mo ${globalMocArgs.join(" ")} ${mocArgs} ${(await sources({ cwd: tempDir })).join(" ")}`;
|
|
223
|
+
if (options.verbose) {
|
|
224
|
+
console.log(chalk.gray(`[${canisterName}] ${buildCmd}`));
|
|
225
|
+
console.time(`build ${canisterName}`);
|
|
226
|
+
}
|
|
227
|
+
await execaCommand(buildCmd, {
|
|
212
228
|
cwd: tempDir,
|
|
213
|
-
|
|
229
|
+
// `inherit` so the compiler output (warnings/errors) is streamed under --verbose
|
|
230
|
+
stdio: options.verbose ? "inherit" : ["pipe", "ignore", "pipe"],
|
|
214
231
|
});
|
|
215
232
|
options.verbose && console.timeEnd(`build ${canisterName}`);
|
|
216
233
|
// deploy canister
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CanisterConfig } from "../types.js";
|
|
1
|
+
import { CanisterConfig, MigrationsConfig } from "../types.js";
|
|
2
2
|
export interface CheckStableOptions {
|
|
3
3
|
verbose: boolean;
|
|
4
4
|
extraArgs: string[];
|
|
@@ -17,6 +17,7 @@ export interface RunStableCheckParams {
|
|
|
17
17
|
globalMocArgs: string[];
|
|
18
18
|
canisterArgs: string[];
|
|
19
19
|
sources?: string[];
|
|
20
|
+
migrations?: MigrationsConfig;
|
|
20
21
|
options?: Partial<CheckStableOptions>;
|
|
21
22
|
}
|
|
22
23
|
export declare function runStableCheck(params: RunStableCheckParams): Promise<void>;
|
|
@@ -4,7 +4,7 @@ import { rm } from "node:fs/promises";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { execa } from "execa";
|
|
6
6
|
import { cliError } from "../error.js";
|
|
7
|
-
import { prepareMigrationArgs } from "../helpers/migrations.js";
|
|
7
|
+
import { getCheckLimitPendingIssue, prepareMigrationArgs, reportCheckLimitPendingIssue, } from "../helpers/migrations.js";
|
|
8
8
|
import { getGlobalMocArgs, readConfig, resolveConfigPath } from "../mops.js";
|
|
9
9
|
import { filterCanisters, looksLikeFile, resolveCanisterConfigs, resolveSingleCanister, validateCanisterArgs, } from "../helpers/resolve-canisters.js";
|
|
10
10
|
import { sourcesArgs } from "./sources.js";
|
|
@@ -61,6 +61,7 @@ export async function checkStable(args, options = {}) {
|
|
|
61
61
|
mocPath,
|
|
62
62
|
globalMocArgs,
|
|
63
63
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
64
|
+
migrations: canister.migrations,
|
|
64
65
|
options,
|
|
65
66
|
});
|
|
66
67
|
}
|
|
@@ -95,6 +96,7 @@ export async function checkStable(args, options = {}) {
|
|
|
95
96
|
globalMocArgs,
|
|
96
97
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
97
98
|
sources,
|
|
99
|
+
migrations: canister.migrations,
|
|
98
100
|
options,
|
|
99
101
|
});
|
|
100
102
|
}
|
|
@@ -136,7 +138,11 @@ export async function runStableCheck(params) {
|
|
|
136
138
|
stdio: "pipe",
|
|
137
139
|
reject: false,
|
|
138
140
|
});
|
|
139
|
-
|
|
141
|
+
const issue = getCheckLimitPendingIssue(params.migrations, canisterName, oldMostPath, options.checkLimit === false, isOldMostFile);
|
|
142
|
+
if (issue) {
|
|
143
|
+
reportCheckLimitPendingIssue(issue, result.exitCode !== 0);
|
|
144
|
+
}
|
|
145
|
+
else if (result.exitCode !== 0) {
|
|
140
146
|
if (result.stderr) {
|
|
141
147
|
console.error(result.stderr);
|
|
142
148
|
}
|
package/dist/commands/check.js
CHANGED
|
@@ -144,7 +144,12 @@ async function checkCanisters(config, canisters, options) {
|
|
|
144
144
|
globalMocArgs,
|
|
145
145
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
146
146
|
sources,
|
|
147
|
-
|
|
147
|
+
migrations: canister.migrations,
|
|
148
|
+
options: {
|
|
149
|
+
verbose: options.verbose,
|
|
150
|
+
extraArgs: options.extraArgs,
|
|
151
|
+
checkLimit: options.checkLimit,
|
|
152
|
+
},
|
|
148
153
|
});
|
|
149
154
|
}
|
|
150
155
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { relative, resolve } from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
3
4
|
import { execa } from "execa";
|
|
4
5
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
5
6
|
export function parseDiagnostics(stdout) {
|
|
@@ -110,6 +111,9 @@ function applyDiagnosticFixes(content, fixes) {
|
|
|
110
111
|
const MAX_FIX_ITERATIONS = 10;
|
|
111
112
|
export async function autofixMotoko(mocPath, files, mocArgs) {
|
|
112
113
|
const fixedFilesCodes = new Map();
|
|
114
|
+
// Frozen migration files are often chmod'd read-only; warn once and skip
|
|
115
|
+
// them rather than aborting the whole run on EACCES/EPERM.
|
|
116
|
+
const readOnlySkipped = new Set();
|
|
113
117
|
for (let iteration = 0; iteration < MAX_FIX_ITERATIONS; iteration++) {
|
|
114
118
|
const fixesByFile = new Map();
|
|
115
119
|
for (const file of files) {
|
|
@@ -126,12 +130,25 @@ export async function autofixMotoko(mocPath, files, mocArgs) {
|
|
|
126
130
|
}
|
|
127
131
|
let progress = false;
|
|
128
132
|
for (const [file, fixes] of fixesByFile) {
|
|
133
|
+
if (readOnlySkipped.has(file)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
129
136
|
const original = await readFile(file, "utf-8");
|
|
130
137
|
const { text: result, appliedCodes } = applyDiagnosticFixes(original, fixes);
|
|
131
138
|
if (result === original) {
|
|
132
139
|
continue;
|
|
133
140
|
}
|
|
134
|
-
|
|
141
|
+
try {
|
|
142
|
+
await writeFile(file, result, "utf-8");
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
146
|
+
readOnlySkipped.add(file);
|
|
147
|
+
console.warn(chalk.yellow(`Skipped read-only file ${relative(process.cwd(), file)} (${appliedCodes.length} fix(es) not applied)`));
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
135
152
|
progress = true;
|
|
136
153
|
const existing = fixedFilesCodes.get(file) ?? [];
|
|
137
154
|
existing.push(...appliedCodes);
|
|
@@ -8,6 +8,22 @@ 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, ignoreLimit?: boolean): Promise<MigrationArgsResult>;
|
|
11
|
+
/**
|
|
12
|
+
* Local chain (+ next) files not yet recorded in the deployed `.most` baseline.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getPendingMigrationFiles(migrations: MigrationsConfig, canisterName: string, appliedNames: string[]): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Pending migrations exceed `check-limit` relative to the deployed `.most` baseline.
|
|
17
|
+
*/
|
|
18
|
+
export interface CheckLimitPendingIssue {
|
|
19
|
+
canisterName: string;
|
|
20
|
+
pending: string[];
|
|
21
|
+
checkLimit: number;
|
|
22
|
+
latestApplied: string | null;
|
|
23
|
+
}
|
|
24
|
+
export declare function getCheckLimitPendingIssue(migrations: MigrationsConfig | undefined, canisterName: string, oldMostPath: string, ignoreCheckLimit: boolean, baselineIsMostFile: boolean): CheckLimitPendingIssue | null;
|
|
25
|
+
/** Warn on accidental compat pass; fail with this diagnostic when compat already failed. */
|
|
26
|
+
export declare function reportCheckLimitPendingIssue(issue: CheckLimitPendingIssue, compatFailed: boolean): void;
|
|
11
27
|
/**
|
|
12
28
|
* Absolute paths of chain migration files that `mops lint` should skip,
|
|
13
29
|
* mirroring the `check-limit` trimming applied to `moc` during `mops check`.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, mkdtempSync, readdirSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
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
7
|
import { resolveCanisterConfigs } from "./resolve-canisters.js";
|
|
8
|
+
import { latestAppliedMigrationName, migrationBasename, parseMostAppliedMigrationNames, } from "./parse-most.js";
|
|
8
9
|
function stagedMigrationsDir(chainDir, canisterName) {
|
|
9
10
|
return join(dirname(chainDir), `.migrations-${canisterName}`);
|
|
10
11
|
}
|
|
@@ -151,6 +152,75 @@ export async function prepareMigrationArgs(migrations, canisterName, mode, verbo
|
|
|
151
152
|
},
|
|
152
153
|
};
|
|
153
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Local chain (+ next) files not yet recorded in the deployed `.most` baseline.
|
|
157
|
+
*/
|
|
158
|
+
export function getPendingMigrationFiles(migrations, canisterName, appliedNames) {
|
|
159
|
+
validateMigrationsConfig(migrations, canisterName);
|
|
160
|
+
const chainDir = resolveConfigPath(migrations.chain);
|
|
161
|
+
const nextDir = migrations.next
|
|
162
|
+
? resolveConfigPath(migrations.next)
|
|
163
|
+
: undefined;
|
|
164
|
+
const nextFile = nextDir ? getNextMigrationFile(nextDir) : null;
|
|
165
|
+
const all = getMigrationFiles(chainDir);
|
|
166
|
+
if (nextFile) {
|
|
167
|
+
all.push(nextFile);
|
|
168
|
+
}
|
|
169
|
+
const highWaterMark = appliedNames.length > 0 ? latestAppliedMigrationName(appliedNames) : "";
|
|
170
|
+
return all.filter((file) => migrationBasename(file) > highWaterMark);
|
|
171
|
+
}
|
|
172
|
+
export function getCheckLimitPendingIssue(migrations, canisterName, oldMostPath, ignoreCheckLimit, baselineIsMostFile) {
|
|
173
|
+
if (!migrations || ignoreCheckLimit || !baselineIsMostFile) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const checkLimit = migrations["check-limit"];
|
|
177
|
+
if (checkLimit === undefined) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
let appliedNames;
|
|
181
|
+
try {
|
|
182
|
+
appliedNames = parseMostAppliedMigrationNames(readFileSync(oldMostPath, "utf8"));
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
if (appliedNames === null) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const pending = getPendingMigrationFiles(migrations, canisterName, appliedNames);
|
|
191
|
+
if (pending.length <= checkLimit) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
canisterName,
|
|
196
|
+
pending,
|
|
197
|
+
checkLimit,
|
|
198
|
+
latestApplied: appliedNames.length > 0 ? latestAppliedMigrationName(appliedNames) : null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function formatCheckLimitPendingLines(issue) {
|
|
202
|
+
const { canisterName, pending, checkLimit } = issue;
|
|
203
|
+
return [
|
|
204
|
+
`Canister '${canisterName}' has ${pending.length} pending migration(s) but check-limit=${checkLimit}. ` +
|
|
205
|
+
`Fold all changes into the latest pending migration: ${pending[pending.length - 1]}`,
|
|
206
|
+
` Pending: ${pending.join(", ")}`,
|
|
207
|
+
...(issue.latestApplied
|
|
208
|
+
? [` Latest already applied migration: ${issue.latestApplied}`]
|
|
209
|
+
: []),
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
/** Warn on accidental compat pass; fail with this diagnostic when compat already failed. */
|
|
213
|
+
export function reportCheckLimitPendingIssue(issue, compatFailed) {
|
|
214
|
+
const lines = formatCheckLimitPendingLines(issue);
|
|
215
|
+
if (compatFailed) {
|
|
216
|
+
cliError(`✗ Stable compatibility check failed for canister '${issue.canisterName}': too many pending migrations for check-limit=${issue.checkLimit}\n` +
|
|
217
|
+
lines.join("\n"));
|
|
218
|
+
}
|
|
219
|
+
console.warn(chalk.yellow(`WARN: ${lines[0]}`));
|
|
220
|
+
for (const line of lines.slice(1)) {
|
|
221
|
+
console.warn(chalk.yellow(line));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
154
224
|
/**
|
|
155
225
|
* Absolute paths of chain migration files that `mops lint` should skip,
|
|
156
226
|
* mirroring the `check-limit` trimming applied to `moc` during `mops check`.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Enhanced-migration names recorded in a Version 4.0.0 `.most` chain block. */
|
|
2
|
+
export declare function parseMostAppliedMigrationNames(content: string): string[] | null;
|
|
3
|
+
export declare function migrationBasename(file: string): string;
|
|
4
|
+
/** Lexicographic max of applied migration names (matches chain file sort order). */
|
|
5
|
+
export declare function latestAppliedMigrationName(appliedNames: string[]): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Enhanced-migration names recorded in a Version 4.0.0 `.most` chain block. */
|
|
2
|
+
export function parseMostAppliedMigrationNames(content) {
|
|
3
|
+
const version = content.match(/^\/\/ Version: ([^\n]+)/m)?.[1]?.trim();
|
|
4
|
+
if (!version) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
if (version !== "4.0.0") {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
// Safe: actor types in type aliases are always indented (col ≥ 2); the main actor is at col 0.
|
|
11
|
+
const actorIdx = content.search(/\nactor\b/);
|
|
12
|
+
if (actorIdx < 0) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const chainBlock = content.slice(0, actorIdx);
|
|
16
|
+
const names = [];
|
|
17
|
+
for (const match of chainBlock.matchAll(/[{;]\s*"([^"]+)"\s*:/gs)) {
|
|
18
|
+
names.push(match[1]);
|
|
19
|
+
}
|
|
20
|
+
return names;
|
|
21
|
+
}
|
|
22
|
+
export function migrationBasename(file) {
|
|
23
|
+
return file.endsWith(".mo") ? file.slice(0, -3) : file;
|
|
24
|
+
}
|
|
25
|
+
/** Lexicographic max of applied migration names (matches chain file sort order). */
|
|
26
|
+
export function latestAppliedMigrationName(appliedNames) {
|
|
27
|
+
return appliedNames.reduce((max, name) => (name > max ? name : max), "");
|
|
28
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { PocketIc, PocketIcServer } from "pic-ic";
|
|
2
|
-
import { PocketIc as PocketIcModern, PocketIcServer as PocketIcServerModern,
|
|
1
|
+
import type { PocketIc, PocketIcServer } from "pic-ic";
|
|
2
|
+
import type { PocketIc as PocketIcModern, PocketIcServer as PocketIcServerModern, StartServerOptions } from "pic-js-mops";
|
|
3
3
|
export type AnyPocketIcServer = PocketIcServer | PocketIcServerModern;
|
|
4
4
|
export type AnyPocketIc = PocketIc | PocketIcModern;
|
|
5
5
|
export type AnySetupCanister = PocketIc["setupCanister"] & PocketIcModern["setupCanister"];
|