ic-mops 2.9.0 → 2.11.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/bundle/cli.tgz +0 -0
  3. package/cli.ts +34 -10
  4. package/commands/build.ts +16 -22
  5. package/commands/check-stable.ts +141 -17
  6. package/commands/check.ts +195 -101
  7. package/commands/migrate.ts +165 -0
  8. package/declarations/main/main.did +38 -0
  9. package/declarations/main/main.did.d.ts +36 -0
  10. package/declarations/main/main.did.js +36 -0
  11. package/dist/cli.js +28 -10
  12. package/dist/commands/build.js +7 -13
  13. package/dist/commands/check-stable.d.ts +8 -1
  14. package/dist/commands/check-stable.js +101 -19
  15. package/dist/commands/check.d.ts +1 -1
  16. package/dist/commands/check.js +136 -78
  17. package/dist/commands/migrate.d.ts +2 -0
  18. package/dist/commands/migrate.js +104 -0
  19. package/dist/declarations/main/main.did +38 -0
  20. package/dist/declarations/main/main.did.d.ts +36 -0
  21. package/dist/declarations/main/main.did.js +36 -0
  22. package/dist/helpers/migrations.d.ts +10 -0
  23. package/dist/helpers/migrations.js +109 -0
  24. package/dist/helpers/resolve-canisters.d.ts +3 -1
  25. package/dist/helpers/resolve-canisters.js +34 -5
  26. package/dist/package.json +1 -1
  27. package/dist/tests/check-stable.test.js +18 -0
  28. package/dist/tests/check.test.js +23 -5
  29. package/dist/tests/migrate.test.d.ts +1 -0
  30. package/dist/tests/migrate.test.js +160 -0
  31. package/dist/types.d.ts +7 -0
  32. package/helpers/migrations.ts +166 -0
  33. package/helpers/resolve-canisters.ts +53 -5
  34. package/package.json +1 -1
  35. package/tests/__snapshots__/check.test.ts.snap +2 -2
  36. package/tests/__snapshots__/migrate.test.ts.snap +119 -0
  37. package/tests/check/canisters-canister-args/Warning.mo +5 -0
  38. package/tests/check/canisters-canister-args/mops.toml +9 -0
  39. package/tests/check-stable/canister-args/migrations/20250101_000000_Init.mo +8 -0
  40. package/tests/check-stable/canister-args/migrations/20250201_000000_AddField.mo +9 -0
  41. package/tests/check-stable/canister-args/mops.toml +9 -0
  42. package/tests/check-stable/canister-args/old.most +8 -0
  43. package/tests/check-stable/canister-args/src/main.mo +11 -0
  44. package/tests/check-stable.test.ts +21 -0
  45. package/tests/check.test.ts +26 -5
  46. package/tests/migrate/basic/deployed.most +12 -0
  47. package/tests/migrate/basic/migrations/20250101_000000_Init.mo +5 -0
  48. package/tests/migrate/basic/migrations/20250201_000000_AddName.mo +5 -0
  49. package/tests/migrate/basic/migrations/20250301_000000_AddEmail.mo +9 -0
  50. package/tests/migrate/basic/mops.toml +15 -0
  51. package/tests/migrate/basic/next-migration/.gitkeep +0 -0
  52. package/tests/migrate/basic/src/main.mo +11 -0
  53. package/tests/migrate/with-next/deployed.most +12 -0
  54. package/tests/migrate/with-next/migrations/20250101_000000_Init.mo +5 -0
  55. package/tests/migrate/with-next/migrations/20250201_000000_AddName.mo +5 -0
  56. package/tests/migrate/with-next/migrations/20250301_000000_AddEmail.mo +9 -0
  57. package/tests/migrate/with-next/mops.toml +15 -0
  58. package/tests/migrate/with-next/next-migration/20250401_000000_RenameId.mo +9 -0
  59. package/tests/migrate/with-next/src/main.mo +11 -0
  60. package/tests/migrate.test.ts +228 -0
  61. package/types.ts +8 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## Next
4
4
 
5
+ ## 2.11.0
6
+ - Add `mops migrate new <Name>` and `mops migrate freeze` commands for managing enhanced migration chains
7
+ - Add `[canisters.<name>.migrations]` config section with `chain`, `next`, `check-limit`, and `build-limit` fields
8
+ - `mops check`, `mops build`, and `mops check-stable` now auto-inject `--enhanced-migration` when `[migrations]` is configured
9
+ - `mops check` and `mops check-stable` emit a hint to create a migration when a stable compatibility check fails and `[migrations]` is configured
10
+ - Migration chain trimming: only the last N migrations are passed to `moc` based on `check-limit`/`build-limit` settings
11
+
12
+ ## 2.10.0
13
+ - `mops check` and `mops check-stable` now apply per-canister `[canisters.<name>].args` (previously only `mops build` applied them)
14
+ - `mops check` now accepts canister names as arguments (e.g. `mops check backend`) to check a specific canister
15
+ - `mops check-stable` now works without arguments, checking all canisters with `[check-stable]` configured
16
+ - `mops check-stable` now accepts canister names as arguments (e.g. `mops check-stable backend`)
17
+
5
18
  ## 2.9.0
6
19
  - Add `mops info <pkg>` command to show detailed package metadata from the registry
7
20
  - Add `[lint.extra]` config for applying additional lint rules to specific files via glob patterns
package/bundle/cli.tgz CHANGED
Binary file
package/cli.ts CHANGED
@@ -43,6 +43,7 @@ import {
43
43
  importPem,
44
44
  setUserProp,
45
45
  } from "./commands/user.js";
46
+ import { migrateNew, migrateFreeze } from "./commands/migrate.js";
46
47
  import { watch } from "./commands/watch/watch.js";
47
48
  import {
48
49
  apiVersion,
@@ -330,9 +331,9 @@ program
330
331
 
331
332
  // check
332
333
  program
333
- .command("check [files...]")
334
+ .command("check [args...]")
334
335
  .description(
335
- "Check Motoko files for syntax errors and type issues. If no files are specified, checks all canister entrypoints from mops.toml. Also runs stable compatibility checks for canisters with [check-stable] configured, and runs linting if lintoko is configured in [toolchain] and rule directories are present",
336
+ "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]",
336
337
  )
337
338
  .option("--verbose", "Verbose console output")
338
339
  .addOption(
@@ -342,15 +343,15 @@ program
342
343
  ),
343
344
  )
344
345
  .allowUnknownOption(true)
345
- .action(async (files, options) => {
346
+ .action(async (args, options) => {
346
347
  checkConfigFile(true);
347
- const { extraArgs, args: fileList } = parseExtraArgs(files);
348
+ const { extraArgs, args: argList } = parseExtraArgs(args);
348
349
  await installAll({
349
350
  silent: true,
350
351
  lock: "ignore",
351
352
  installFromLockFile: true,
352
353
  });
353
- await check(fileList, {
354
+ await check(argList, {
354
355
  ...options,
355
356
  extraArgs,
356
357
  });
@@ -372,21 +373,21 @@ program
372
373
 
373
374
  // check-stable
374
375
  program
375
- .command("check-stable <old-file> [canister]")
376
+ .command("check-stable [args...]")
376
377
  .description(
377
- "Check stable variable compatibility between an old version (.mo or .most file) and the current canister entrypoint",
378
+ "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",
378
379
  )
379
380
  .option("--verbose", "Verbose console output")
380
381
  .allowUnknownOption(true)
381
- .action(async (oldFile, canister, options) => {
382
+ .action(async (args, options) => {
382
383
  checkConfigFile(true);
383
- const { extraArgs } = parseExtraArgs();
384
+ const { extraArgs, args: argList } = parseExtraArgs(args);
384
385
  await installAll({
385
386
  silent: true,
386
387
  lock: "ignore",
387
388
  installFromLockFile: true,
388
389
  });
389
- await checkStable(oldFile, canister, {
390
+ await checkStable(argList, {
390
391
  ...options,
391
392
  extraArgs,
392
393
  });
@@ -707,6 +708,29 @@ toolchainCommand
707
708
 
708
709
  program.addCommand(toolchainCommand);
709
710
 
711
+ // migrate
712
+ const migrateCommand = new Command("migrate").description(
713
+ "Manage enhanced migration chains",
714
+ );
715
+
716
+ migrateCommand
717
+ .command("new <name> [canister]")
718
+ .description("Create a new migration file in the next-migration directory")
719
+ .action(async (name, canister) => {
720
+ checkConfigFile(true);
721
+ await migrateNew(name, canister);
722
+ });
723
+
724
+ migrateCommand
725
+ .command("freeze [canister]")
726
+ .description("Move the next migration into the frozen chain")
727
+ .action(async (canister) => {
728
+ checkConfigFile(true);
729
+ await migrateFreeze(canister);
730
+ });
731
+
732
+ program.addCommand(migrateCommand);
733
+
710
734
  // self
711
735
  const selfCommand = new Command("self").description("Mops CLI management");
712
736
 
package/commands/build.ts CHANGED
@@ -6,7 +6,12 @@ import { join } from "node:path";
6
6
  import { lock, unlockSync } from "proper-lockfile";
7
7
  import { cliError } from "../error.js";
8
8
  import { isCandidCompatible } from "../helpers/is-candid-compatible.js";
9
- import { resolveCanisterConfigs } from "../helpers/resolve-canisters.js";
9
+ import {
10
+ filterCanisters,
11
+ resolveCanisterConfigs,
12
+ validateCanisterArgs,
13
+ } from "../helpers/resolve-canisters.js";
14
+ import { prepareMigrationArgs } from "../helpers/migrations.js";
10
15
  import { CanisterConfig, Config } from "../types.js";
11
16
  import { CustomSection, getWasmBindings } from "../wasm.js";
12
17
  import { getGlobalMocArgs, readConfig, resolveConfigPath } from "../mops.js";
@@ -41,26 +46,11 @@ export async function build(
41
46
  cliError(`No Motoko canisters found in mops.toml configuration`);
42
47
  }
43
48
 
44
- if (canisterNames) {
45
- let invalidNames = canisterNames.filter((name) => !(name in canisters));
46
- if (invalidNames.length) {
47
- cliError(
48
- `Motoko canister(s) not found in mops.toml configuration: ${invalidNames.join(", ")}`,
49
- );
50
- }
51
- }
52
-
53
49
  if (!(await exists(outputDir))) {
54
50
  await mkdir(outputDir, { recursive: true });
55
51
  }
56
52
 
57
- const filteredCanisters = canisterNames
58
- ? Object.fromEntries(
59
- Object.entries(canisters).filter(([name]) =>
60
- canisterNames.includes(name),
61
- ),
62
- )
63
- : canisters;
53
+ const filteredCanisters = filterCanisters(canisters, canisterNames);
64
54
 
65
55
  for (let [canisterName, canister] of Object.entries(filteredCanisters)) {
66
56
  console.log(chalk.blue("build canister"), chalk.bold(canisterName));
@@ -98,6 +88,12 @@ export async function build(
98
88
  };
99
89
  process.on("exit", exitCleanup);
100
90
 
91
+ const migration = await prepareMigrationArgs(
92
+ canister.migrations,
93
+ canisterName,
94
+ "build",
95
+ options.verbose,
96
+ );
101
97
  try {
102
98
  let args = [
103
99
  "-c",
@@ -108,6 +104,7 @@ export async function build(
108
104
  motokoPath,
109
105
  ...(await sourcesArgs()).flat(),
110
106
  ...getGlobalMocArgs(config),
107
+ ...migration.migrationArgs,
111
108
  ];
112
109
  args.push(
113
110
  ...collectExtraArgs(config, canister, canisterName, options.extraArgs),
@@ -210,6 +207,7 @@ export async function build(
210
207
  );
211
208
  }
212
209
  } finally {
210
+ await migration.cleanup();
213
211
  process.removeListener("exit", exitCleanup);
214
212
  try {
215
213
  await release?.();
@@ -249,11 +247,7 @@ function collectExtraArgs(
249
247
  args.push(...config.build.args);
250
248
  }
251
249
  if (canister.args) {
252
- if (typeof canister.args === "string") {
253
- cliError(
254
- `Canister config 'args' should be an array of strings for canister ${canisterName}`,
255
- );
256
- }
250
+ validateCanisterArgs(canister, canisterName, config);
257
251
  args.push(...canister.args);
258
252
  }
259
253
  if (extraArgs) {
@@ -4,8 +4,16 @@ 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
8
  import { getGlobalMocArgs, readConfig, resolveConfigPath } from "../mops.js";
8
- import { resolveSingleCanister } from "../helpers/resolve-canisters.js";
9
+ import { CanisterConfig } from "../types.js";
10
+ import {
11
+ filterCanisters,
12
+ looksLikeFile,
13
+ resolveCanisterConfigs,
14
+ resolveSingleCanister,
15
+ validateCanisterArgs,
16
+ } from "../helpers/resolve-canisters.js";
9
17
  import { sourcesArgs } from "./sources.js";
10
18
  import { toolchain } from "./toolchain/index.js";
11
19
 
@@ -16,29 +24,130 @@ export interface CheckStableOptions {
16
24
  extraArgs: string[];
17
25
  }
18
26
 
27
+ export function resolveStablePath(
28
+ canister: CanisterConfig,
29
+ canisterName: string,
30
+ options?: { required?: boolean },
31
+ ): string | null {
32
+ const stableConfig = canister["check-stable"];
33
+ if (!stableConfig) {
34
+ if (options?.required) {
35
+ cliError(
36
+ `Canister '${canisterName}' has no [canisters.${canisterName}.check-stable] configuration in mops.toml`,
37
+ );
38
+ }
39
+ return null;
40
+ }
41
+ const stablePath = resolveConfigPath(stableConfig.path);
42
+ if (!existsSync(stablePath)) {
43
+ if (stableConfig.skipIfMissing) {
44
+ return null;
45
+ }
46
+ cliError(
47
+ `Deployed file not found: ${stablePath} (canister '${canisterName}')\n` +
48
+ "Set skipIfMissing = true in [canisters." +
49
+ canisterName +
50
+ ".check-stable] to skip this check when the file is missing.",
51
+ );
52
+ }
53
+ return stablePath;
54
+ }
55
+
19
56
  export async function checkStable(
20
- oldFile: string,
21
- canisterName: string | undefined,
57
+ args: string[],
22
58
  options: Partial<CheckStableOptions> = {},
23
59
  ): Promise<void> {
24
60
  const config = readConfig();
25
- const { name, canister } = resolveSingleCanister(config, canisterName);
61
+ const mocPath = await toolchain.bin("moc", { fallback: true });
62
+ const globalMocArgs = getGlobalMocArgs(config);
63
+
64
+ const firstArg = args[0];
65
+ if (firstArg && looksLikeFile(firstArg)) {
66
+ const oldFile = firstArg;
67
+ const canisterName = args[1];
68
+ const { name, canister } = resolveSingleCanister(config, canisterName);
69
+
70
+ if (!canister.main) {
71
+ cliError(`No main file specified for canister '${name}' in mops.toml`);
72
+ }
26
73
 
27
- if (!canister.main) {
28
- cliError(`No main file specified for canister '${name}' in mops.toml`);
74
+ validateCanisterArgs(canister, name, config);
75
+
76
+ const migration = await prepareMigrationArgs(
77
+ canister.migrations,
78
+ name,
79
+ "check",
80
+ options.verbose,
81
+ );
82
+ try {
83
+ await runStableCheck({
84
+ oldFile,
85
+ canisterMain: resolveConfigPath(canister.main),
86
+ canisterName: name,
87
+ mocPath,
88
+ globalMocArgs,
89
+ canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
90
+ options,
91
+ hasMigrations: !!canister.migrations,
92
+ });
93
+ } finally {
94
+ await migration.cleanup();
95
+ }
96
+ return;
29
97
  }
30
98
 
31
- const mocPath = await toolchain.bin("moc", { fallback: true });
32
- const globalMocArgs = getGlobalMocArgs(config);
99
+ const canisters = resolveCanisterConfigs(config);
100
+ const canisterNames = args.length > 0 ? args : undefined;
101
+ const filteredCanisters = filterCanisters(canisters, canisterNames);
102
+ const sources = (await sourcesArgs()).flat();
33
103
 
34
- await runStableCheck({
35
- oldFile,
36
- canisterMain: resolveConfigPath(canister.main),
37
- canisterName: name,
38
- mocPath,
39
- globalMocArgs,
40
- options,
41
- });
104
+ let checked = 0;
105
+ for (const [name, canister] of Object.entries(filteredCanisters)) {
106
+ if (!canister.main) {
107
+ cliError(`No main file specified for canister '${name}' in mops.toml`);
108
+ }
109
+
110
+ validateCanisterArgs(canister, name, config);
111
+ const stablePath = resolveStablePath(canister, name, {
112
+ required: !!canisterNames,
113
+ });
114
+ if (!stablePath) {
115
+ continue;
116
+ }
117
+
118
+ const migration = await prepareMigrationArgs(
119
+ canister.migrations,
120
+ name,
121
+ "check",
122
+ options.verbose,
123
+ );
124
+ try {
125
+ await runStableCheck({
126
+ oldFile: stablePath,
127
+ canisterMain: resolveConfigPath(canister.main),
128
+ canisterName: name,
129
+ mocPath,
130
+ globalMocArgs,
131
+ canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
132
+ sources,
133
+ options,
134
+ hasMigrations: !!canister.migrations,
135
+ });
136
+ } finally {
137
+ await migration.cleanup();
138
+ }
139
+ checked++;
140
+ }
141
+
142
+ if (checked === 0 && !canisterNames) {
143
+ cliError(
144
+ "No canisters with [check-stable] configuration found in mops.toml.\n" +
145
+ "Either pass an old file: mops check-stable <old-file> [canister]\n" +
146
+ "Or configure check-stable for a canister:\n\n" +
147
+ " [canisters.backend.check-stable]\n" +
148
+ ' path = "deployed.mo"',
149
+ );
150
+ }
42
151
  }
43
152
 
44
153
  export interface RunStableCheckParams {
@@ -47,7 +156,10 @@ export interface RunStableCheckParams {
47
156
  canisterName: string;
48
157
  mocPath: string;
49
158
  globalMocArgs: string[];
159
+ canisterArgs: string[];
160
+ sources?: string[];
50
161
  options?: Partial<CheckStableOptions>;
162
+ hasMigrations?: boolean;
51
163
  }
52
164
 
53
165
  export async function runStableCheck(
@@ -59,10 +171,11 @@ export async function runStableCheck(
59
171
  canisterName,
60
172
  mocPath,
61
173
  globalMocArgs,
174
+ canisterArgs,
62
175
  options = {},
63
176
  } = params;
64
177
 
65
- const sources = (await sourcesArgs()).flat();
178
+ const sources = params.sources ?? (await sourcesArgs()).flat();
66
179
  const isOldMostFile = oldFile.endsWith(".most");
67
180
 
68
181
  if (!existsSync(oldFile)) {
@@ -80,6 +193,7 @@ export async function runStableCheck(
80
193
  join(CHECK_STABLE_DIR, "old.most"),
81
194
  sources,
82
195
  globalMocArgs,
196
+ canisterArgs,
83
197
  options,
84
198
  );
85
199
 
@@ -89,6 +203,7 @@ export async function runStableCheck(
89
203
  join(CHECK_STABLE_DIR, "new.most"),
90
204
  sources,
91
205
  globalMocArgs,
206
+ canisterArgs,
92
207
  options,
93
208
  );
94
209
 
@@ -113,6 +228,13 @@ export async function runStableCheck(
113
228
  if (result.stderr) {
114
229
  console.error(result.stderr);
115
230
  }
231
+ if (params.hasMigrations) {
232
+ console.error(
233
+ chalk.yellow(
234
+ "Hint: You may need a migration. Run `mops migrate new <Name>` to create one.",
235
+ ),
236
+ );
237
+ }
116
238
  cliError(
117
239
  `✗ Stable compatibility check failed for canister '${canisterName}'`,
118
240
  );
@@ -134,6 +256,7 @@ async function generateStableTypes(
134
256
  outputPath: string,
135
257
  sources: string[],
136
258
  globalMocArgs: string[],
259
+ canisterArgs: string[],
137
260
  options: Partial<CheckStableOptions>,
138
261
  ): Promise<string> {
139
262
  const base = basename(outputPath, ".most");
@@ -145,6 +268,7 @@ async function generateStableTypes(
145
268
  moFile,
146
269
  ...sources,
147
270
  ...globalMocArgs,
271
+ ...canisterArgs,
148
272
  ...(options.extraArgs ?? []),
149
273
  ];
150
274