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
@@ -0,0 +1,104 @@
1
+ import { existsSync, mkdirSync, renameSync } from "node:fs";
2
+ import { writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import chalk from "chalk";
5
+ import { cliError } from "../error.js";
6
+ import { getNextMigrationFile, validateMigrationsConfig, validateNextMigrationOrder, } from "../helpers/migrations.js";
7
+ import { resolveCanisterConfigs } from "../helpers/resolve-canisters.js";
8
+ import { readConfig, resolveConfigPath } from "../mops.js";
9
+ function resolveMigrationCanister(canisterName) {
10
+ const config = readConfig();
11
+ const canisters = resolveCanisterConfigs(config);
12
+ const withMigrations = Object.entries(canisters).filter(([, c]) => c.migrations);
13
+ if (withMigrations.length === 0) {
14
+ cliError("No canisters with [migrations] config found in mops.toml.\n" +
15
+ "Add a [canisters.<name>.migrations] section first:\n\n" +
16
+ " [canisters.backend.migrations]\n" +
17
+ ' chain = "migrations"\n' +
18
+ ' next = "next-migration" # required for migrate new/freeze');
19
+ }
20
+ if (canisterName) {
21
+ const canister = canisters[canisterName];
22
+ if (!canister) {
23
+ cliError(`Canister '${canisterName}' not found in mops.toml. Available: ${Object.keys(canisters).join(", ")}`);
24
+ }
25
+ if (!canister.migrations) {
26
+ cliError(`Canister '${canisterName}' has no [canisters.${canisterName}.migrations] config in mops.toml`);
27
+ }
28
+ return { name: canisterName, canister };
29
+ }
30
+ if (withMigrations.length > 1) {
31
+ cliError(`Multiple canisters with [migrations] config. Please specify one: ${withMigrations.map(([n]) => n).join(", ")}`);
32
+ }
33
+ return { name: withMigrations[0][0], canister: withMigrations[0][1] };
34
+ }
35
+ const VALID_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
36
+ function generateTimestamp() {
37
+ const now = new Date();
38
+ const pad = (n) => String(n).padStart(2, "0");
39
+ return (`${now.getUTCFullYear()}${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())}` +
40
+ `_${pad(now.getUTCHours())}${pad(now.getUTCMinutes())}${pad(now.getUTCSeconds())}`);
41
+ }
42
+ const MIGRATION_TEMPLATE = `module {
43
+ public func migration(old : {}) : {} {
44
+ {}
45
+ }
46
+ }
47
+ `;
48
+ export async function migrateNew(name, canisterName) {
49
+ if (!VALID_NAME_RE.test(name)) {
50
+ cliError(`Invalid migration name: "${name}"\n` +
51
+ "Name must start with a letter and contain only letters, digits, and underscores.");
52
+ }
53
+ const { name: resolvedName, canister } = resolveMigrationCanister(canisterName);
54
+ const migrations = canister.migrations;
55
+ validateMigrationsConfig(migrations, resolvedName);
56
+ if (!migrations.next) {
57
+ cliError(`[canisters.${resolvedName}.migrations] is missing the "next" field.\n` +
58
+ 'Add next = "next-migration" to use `mops migrate new/freeze`.');
59
+ }
60
+ const chainDir = resolveConfigPath(migrations.chain);
61
+ const nextDir = resolveConfigPath(migrations.next);
62
+ const existingNext = existsSync(nextDir)
63
+ ? getNextMigrationFile(nextDir)
64
+ : null;
65
+ if (existingNext) {
66
+ cliError(`A next migration already exists: ${existingNext}\n` +
67
+ "Freeze it first with `mops migrate freeze`.");
68
+ }
69
+ const timestamp = generateTimestamp();
70
+ const fileName = `${timestamp}_${name}.mo`;
71
+ validateNextMigrationOrder(chainDir, fileName);
72
+ if (!existsSync(chainDir)) {
73
+ mkdirSync(chainDir, { recursive: true });
74
+ }
75
+ if (!existsSync(nextDir)) {
76
+ mkdirSync(nextDir, { recursive: true });
77
+ }
78
+ const filePath = join(nextDir, fileName);
79
+ await writeFile(filePath, MIGRATION_TEMPLATE);
80
+ console.log(chalk.green(`✓ Created migration: ${filePath}`));
81
+ }
82
+ export async function migrateFreeze(canisterName) {
83
+ const { name: resolvedName, canister } = resolveMigrationCanister(canisterName);
84
+ const migrations = canister.migrations;
85
+ validateMigrationsConfig(migrations, resolvedName);
86
+ if (!migrations.next) {
87
+ cliError(`[canisters.${resolvedName}.migrations] is missing the "next" field.\n` +
88
+ 'Add next = "next-migration" to use `mops migrate new/freeze`.');
89
+ }
90
+ const chainDir = resolveConfigPath(migrations.chain);
91
+ const nextDir = resolveConfigPath(migrations.next);
92
+ const nextFile = existsSync(nextDir) ? getNextMigrationFile(nextDir) : null;
93
+ if (!nextFile) {
94
+ cliError("No next migration to freeze. Create one with `mops migrate new <Name>`.");
95
+ }
96
+ validateNextMigrationOrder(chainDir, nextFile);
97
+ if (!existsSync(chainDir)) {
98
+ mkdirSync(chainDir, { recursive: true });
99
+ }
100
+ const src = join(nextDir, nextFile);
101
+ const dest = join(chainDir, nextFile);
102
+ renameSync(src, dest);
103
+ console.log(chalk.green(`✓ Frozen migration: ${nextFile} → ${chainDir}/`));
104
+ }
@@ -42,6 +42,11 @@ type StreamingCallbackResponse =
42
42
  };
43
43
  type StreamingCallback = func (StreamingToken) ->
44
44
  (opt StreamingCallbackResponse) query;
45
+ type StructureStats =
46
+ record {
47
+ bytes: nat;
48
+ count: nat;
49
+ };
45
50
  type StorageStats =
46
51
  record {
47
52
  cyclesBalance: nat;
@@ -272,6 +277,38 @@ type PackageChanges =
272
277
  prevDocsCoverage: float64;
273
278
  tests: TestsChanges;
274
279
  };
280
+ type MemoryStats =
281
+ record {
282
+ dailySnapshots: StructureStats;
283
+ dailySnapshotsByPackageId: StructureStats;
284
+ dailySnapshotsByPackageName: StructureStats;
285
+ dailyTempRecords: StructureStats;
286
+ downloadsByPackageId: StructureStats;
287
+ downloadsByPackageName: StructureStats;
288
+ fileIdsByPackage: StructureStats;
289
+ hashByFileId: StructureStats;
290
+ highestConfigs: StructureStats;
291
+ maintainersByPackage: StructureStats;
292
+ ownersByPackage: StructureStats;
293
+ packageBenchmarks: StructureStats;
294
+ packageConfigs: StructureStats;
295
+ packageDocsCoverage: StructureStats;
296
+ packageFileStats: StructureStats;
297
+ packageNotes: StructureStats;
298
+ packagePublications: StructureStats;
299
+ packageTestStats: StructureStats;
300
+ packageVersions: StructureStats;
301
+ rtsHeapSize: nat;
302
+ rtsMemorySize: nat;
303
+ names: StructureStats;
304
+ storageByFileId: StructureStats;
305
+ storages: StructureStats;
306
+ users: StructureStats;
307
+ weeklySnapshots: StructureStats;
308
+ weeklySnapshotsByPackageId: StructureStats;
309
+ weeklySnapshotsByPackageName: StructureStats;
310
+ weeklyTempRecords: StructureStats;
311
+ };
275
312
  type Main =
276
313
  service {
277
314
  addMaintainer: (packageName: PackageName, newMaintainer: principal) ->
@@ -311,6 +348,7 @@ type Main =
311
348
  SemverPart;
312
349
  }) -> (Result_6) query;
313
350
  getHighestVersion: (name: PackageName) -> (Result_5) query;
351
+ getMemoryStats: () -> (MemoryStats) query;
314
352
  getMostDownloadedPackages: () -> (vec PackageSummary) query;
315
353
  getMostDownloadedPackagesIn7Days: () -> (vec PackageSummary) query;
316
354
  getNewPackages: () -> (vec PackageSummary) query;
@@ -45,6 +45,41 @@ export interface HttpRequestResult {
45
45
  'body' : Uint8Array | number[],
46
46
  'headers' : Array<HttpHeader>,
47
47
  }
48
+ export interface StructureStats {
49
+ 'count' : bigint,
50
+ 'bytes' : bigint,
51
+ }
52
+ export interface MemoryStats {
53
+ 'rtsHeapSize' : bigint,
54
+ 'rtsMemorySize' : bigint,
55
+ 'packageVersions' : StructureStats,
56
+ 'packageConfigs' : StructureStats,
57
+ 'highestConfigs' : StructureStats,
58
+ 'packagePublications' : StructureStats,
59
+ 'ownersByPackage' : StructureStats,
60
+ 'maintainersByPackage' : StructureStats,
61
+ 'fileIdsByPackage' : StructureStats,
62
+ 'hashByFileId' : StructureStats,
63
+ 'packageFileStats' : StructureStats,
64
+ 'packageTestStats' : StructureStats,
65
+ 'packageBenchmarks' : StructureStats,
66
+ 'packageNotes' : StructureStats,
67
+ 'packageDocsCoverage' : StructureStats,
68
+ 'downloadsByPackageName' : StructureStats,
69
+ 'downloadsByPackageId' : StructureStats,
70
+ 'dailySnapshots' : StructureStats,
71
+ 'weeklySnapshots' : StructureStats,
72
+ 'dailySnapshotsByPackageName' : StructureStats,
73
+ 'dailySnapshotsByPackageId' : StructureStats,
74
+ 'weeklySnapshotsByPackageName' : StructureStats,
75
+ 'weeklySnapshotsByPackageId' : StructureStats,
76
+ 'dailyTempRecords' : StructureStats,
77
+ 'weeklyTempRecords' : StructureStats,
78
+ 'storages' : StructureStats,
79
+ 'storageByFileId' : StructureStats,
80
+ 'users' : StructureStats,
81
+ 'names' : StructureStats,
82
+ }
48
83
  export interface Main {
49
84
  'addMaintainer' : ActorMethod<[PackageName, Principal], Result_3>,
50
85
  'addOwner' : ActorMethod<[PackageName, Principal], Result_3>,
@@ -77,6 +112,7 @@ export interface Main {
77
112
  Result_6
78
113
  >,
79
114
  'getHighestVersion' : ActorMethod<[PackageName], Result_5>,
115
+ 'getMemoryStats' : ActorMethod<[], MemoryStats, 'query'>,
80
116
  'getMostDownloadedPackages' : ActorMethod<[], Array<PackageSummary>>,
81
117
  'getMostDownloadedPackagesIn7Days' : ActorMethod<[], Array<PackageSummary>>,
82
118
  'getNewPackages' : ActorMethod<[], Array<PackageSummary>>,
@@ -196,6 +196,41 @@ export const idlFactory = ({ IDL }) => {
196
196
  'cyclesBalance' : IDL.Nat,
197
197
  'memorySize' : IDL.Nat,
198
198
  });
199
+ const StructureStats = IDL.Record({
200
+ 'count' : IDL.Nat,
201
+ 'bytes' : IDL.Nat,
202
+ });
203
+ const MemoryStats = IDL.Record({
204
+ 'rtsHeapSize' : IDL.Nat,
205
+ 'rtsMemorySize' : IDL.Nat,
206
+ 'packageVersions' : StructureStats,
207
+ 'packageConfigs' : StructureStats,
208
+ 'highestConfigs' : StructureStats,
209
+ 'packagePublications' : StructureStats,
210
+ 'ownersByPackage' : StructureStats,
211
+ 'maintainersByPackage' : StructureStats,
212
+ 'fileIdsByPackage' : StructureStats,
213
+ 'hashByFileId' : StructureStats,
214
+ 'packageFileStats' : StructureStats,
215
+ 'packageTestStats' : StructureStats,
216
+ 'packageBenchmarks' : StructureStats,
217
+ 'packageNotes' : StructureStats,
218
+ 'packageDocsCoverage' : StructureStats,
219
+ 'downloadsByPackageName' : StructureStats,
220
+ 'downloadsByPackageId' : StructureStats,
221
+ 'dailySnapshots' : StructureStats,
222
+ 'weeklySnapshots' : StructureStats,
223
+ 'dailySnapshotsByPackageName' : StructureStats,
224
+ 'dailySnapshotsByPackageId' : StructureStats,
225
+ 'weeklySnapshotsByPackageName' : StructureStats,
226
+ 'weeklySnapshotsByPackageId' : StructureStats,
227
+ 'dailyTempRecords' : StructureStats,
228
+ 'weeklyTempRecords' : StructureStats,
229
+ 'storages' : StructureStats,
230
+ 'storageByFileId' : StructureStats,
231
+ 'users' : StructureStats,
232
+ 'names' : StructureStats,
233
+ });
199
234
  const Header = IDL.Tuple(IDL.Text, IDL.Text);
200
235
  const Request = IDL.Record({
201
236
  'url' : IDL.Text,
@@ -308,6 +343,7 @@ export const idlFactory = ({ IDL }) => {
308
343
  ['query'],
309
344
  ),
310
345
  'getHighestVersion' : IDL.Func([PackageName], [Result_5], ['query']),
346
+ 'getMemoryStats' : IDL.Func([], [MemoryStats], ['query']),
311
347
  'getMostDownloadedPackages' : IDL.Func(
312
348
  [],
313
349
  [IDL.Vec(PackageSummary)],
@@ -0,0 +1,10 @@
1
+ import { MigrationsConfig } from "../types.js";
2
+ export interface MigrationArgsResult {
3
+ migrationArgs: string[];
4
+ cleanup: () => Promise<void>;
5
+ }
6
+ export declare function getMigrationFiles(dir: string): string[];
7
+ export declare function getNextMigrationFile(nextDir: string): string | null;
8
+ export declare function validateNextMigrationOrder(chainDirOrFiles: string | string[], nextFile: string): void;
9
+ export declare function validateMigrationsConfig(migrations: MigrationsConfig, canisterName: string): void;
10
+ export declare function prepareMigrationArgs(migrations: MigrationsConfig | undefined, canisterName: string, mode: "check" | "build", verbose?: boolean): Promise<MigrationArgsResult>;
@@ -0,0 +1,109 @@
1
+ import { existsSync, mkdirSync, readdirSync, symlinkSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { rm } from "node:fs/promises";
4
+ import chalk from "chalk";
5
+ import { cliError } from "../error.js";
6
+ import { resolveConfigPath } from "../mops.js";
7
+ const MIGRATIONS_TEMP_DIR = ".mops/.migrations";
8
+ export function getMigrationFiles(dir) {
9
+ if (!existsSync(dir)) {
10
+ return [];
11
+ }
12
+ return readdirSync(dir)
13
+ .filter((f) => f.endsWith(".mo"))
14
+ .sort();
15
+ }
16
+ export function getNextMigrationFile(nextDir) {
17
+ if (!existsSync(nextDir)) {
18
+ return null;
19
+ }
20
+ const files = readdirSync(nextDir).filter((f) => f.endsWith(".mo"));
21
+ if (files.length > 1) {
22
+ cliError(`next-migration directory must contain at most 1 .mo file, found ${files.length} in ${nextDir}`);
23
+ }
24
+ return files[0] ?? null;
25
+ }
26
+ export function validateNextMigrationOrder(chainDirOrFiles, nextFile) {
27
+ const chainFiles = typeof chainDirOrFiles === "string"
28
+ ? getMigrationFiles(chainDirOrFiles)
29
+ : chainDirOrFiles;
30
+ const lastChainFile = chainFiles[chainFiles.length - 1];
31
+ if (lastChainFile && nextFile <= lastChainFile) {
32
+ cliError(`Next migration "${nextFile}" must sort after all files in the chain.\n` +
33
+ `Last chain file: "${lastChainFile}".\n` +
34
+ "Use a timestamp prefix (e.g. YYYYMMDD_HHMMSS_Name.mo) to ensure correct ordering.");
35
+ }
36
+ }
37
+ export function validateMigrationsConfig(migrations, canisterName) {
38
+ if (!migrations.chain) {
39
+ cliError(`[canisters.${canisterName}.migrations] is missing required field "chain"`);
40
+ }
41
+ for (const field of ["check-limit", "build-limit"]) {
42
+ const value = migrations[field];
43
+ if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
44
+ cliError(`[canisters.${canisterName}.migrations] ${field} must be a positive integer`);
45
+ }
46
+ }
47
+ }
48
+ export async function prepareMigrationArgs(migrations, canisterName, mode, verbose) {
49
+ const noOp = {
50
+ migrationArgs: [],
51
+ cleanup: async () => { },
52
+ };
53
+ if (!migrations) {
54
+ return noOp;
55
+ }
56
+ validateMigrationsConfig(migrations, canisterName);
57
+ const chainDir = resolveConfigPath(migrations.chain);
58
+ const nextDir = migrations.next
59
+ ? resolveConfigPath(migrations.next)
60
+ : undefined;
61
+ const nextFile = nextDir ? getNextMigrationFile(nextDir) : null;
62
+ if (!existsSync(chainDir) && !nextFile) {
63
+ cliError(`Migration chain directory not found: ${chainDir}\n` +
64
+ "Run `mops migrate new <Name>` to initialize the migration chain.");
65
+ }
66
+ const chainFiles = getMigrationFiles(chainDir);
67
+ if (nextFile) {
68
+ validateNextMigrationOrder(chainFiles, nextFile);
69
+ }
70
+ const allMigrations = chainFiles.map((f) => ({
71
+ file: f,
72
+ dir: chainDir,
73
+ }));
74
+ if (nextFile && nextDir) {
75
+ allMigrations.push({ file: nextFile, dir: nextDir });
76
+ }
77
+ const limit = mode === "check" ? migrations["check-limit"] : migrations["build-limit"];
78
+ const isTrimming = limit !== undefined && limit < allMigrations.length;
79
+ const needsTempDir = nextFile !== null || isTrimming;
80
+ if (!needsTempDir) {
81
+ return {
82
+ migrationArgs: [`--enhanced-migration=${chainDir}`],
83
+ cleanup: async () => { },
84
+ };
85
+ }
86
+ const tempDir = join(MIGRATIONS_TEMP_DIR, canisterName);
87
+ await rm(tempDir, { recursive: true, force: true });
88
+ mkdirSync(tempDir, { recursive: true });
89
+ const filesToInclude = isTrimming
90
+ ? allMigrations.slice(-limit)
91
+ : allMigrations;
92
+ for (const { file, dir } of filesToInclude) {
93
+ symlinkSync(resolve(dir, file), join(tempDir, file));
94
+ }
95
+ if (verbose) {
96
+ console.log(chalk.blue("migrations"), chalk.gray(`Prepared ${filesToInclude.length} migration(s) for ${canisterName}` +
97
+ (isTrimming ? ` (trimmed from ${allMigrations.length})` : "")));
98
+ }
99
+ const migrationArgs = [`--enhanced-migration=${tempDir}`];
100
+ if (isTrimming) {
101
+ migrationArgs.push("-A=M0254");
102
+ }
103
+ return {
104
+ migrationArgs,
105
+ cleanup: async () => {
106
+ await rm(tempDir, { recursive: true, force: true });
107
+ },
108
+ };
109
+ }
@@ -1,7 +1,9 @@
1
1
  import { CanisterConfig, Config } from "../types.js";
2
2
  export declare function resolveCanisterConfigs(config: Config): Record<string, CanisterConfig>;
3
- export declare function resolveCanisterEntrypoints(config: Config): string[];
3
+ export declare function filterCanisters(canisters: Record<string, CanisterConfig>, names?: string[]): Record<string, CanisterConfig>;
4
4
  export declare function resolveSingleCanister(config: Config, canisterName?: string): {
5
5
  name: string;
6
6
  canister: CanisterConfig;
7
7
  };
8
+ export declare function looksLikeFile(arg: string): boolean;
9
+ export declare function validateCanisterArgs(canister: CanisterConfig, canisterName: string, config?: Config): void;
@@ -5,11 +5,15 @@ export function resolveCanisterConfigs(config) {
5
5
  }
6
6
  return Object.fromEntries(Object.entries(config.canisters).map(([name, c]) => typeof c === "string" ? [name, { main: c }] : [name, c]));
7
7
  }
8
- export function resolveCanisterEntrypoints(config) {
9
- const canisters = resolveCanisterConfigs(config);
10
- return Object.values(canisters)
11
- .map((c) => c.main)
12
- .filter((main) => Boolean(main));
8
+ export function filterCanisters(canisters, names) {
9
+ if (!names) {
10
+ return canisters;
11
+ }
12
+ const invalidNames = names.filter((name) => !(name in canisters));
13
+ if (invalidNames.length) {
14
+ cliError(`Canister(s) not found in mops.toml: ${invalidNames.join(", ")}. Available: ${Object.keys(canisters).join(", ")}`);
15
+ }
16
+ return Object.fromEntries(Object.entries(canisters).filter(([name]) => names.includes(name)));
13
17
  }
14
18
  export function resolveSingleCanister(config, canisterName) {
15
19
  const canisters = resolveCanisterConfigs(config);
@@ -29,3 +33,28 @@ export function resolveSingleCanister(config, canisterName) {
29
33
  }
30
34
  return { name: names[0], canister: canisters[names[0]] };
31
35
  }
36
+ export function looksLikeFile(arg) {
37
+ return (arg.endsWith(".mo") ||
38
+ arg.endsWith(".most") ||
39
+ arg.includes("/") ||
40
+ arg.includes("\\"));
41
+ }
42
+ export function validateCanisterArgs(canister, canisterName, config) {
43
+ if (canister.args && typeof canister.args === "string") {
44
+ cliError(`Canister config 'args' should be an array of strings for canister ${canisterName}`);
45
+ }
46
+ if (!canister.migrations) {
47
+ return;
48
+ }
49
+ const flagSources = [
50
+ [`[canisters.${canisterName}].args`, canister.args],
51
+ ["[moc].args", config?.moc?.args],
52
+ ["[build].args", config?.build?.args],
53
+ ];
54
+ for (const [section, args] of flagSources) {
55
+ if (args?.some((a) => a.startsWith("--enhanced-migration"))) {
56
+ cliError(`Canister '${canisterName}' has [migrations] config but --enhanced-migration in ${section}.\n` +
57
+ "Remove --enhanced-migration — it is managed automatically when [migrations] is configured.");
58
+ }
59
+ }
60
+ }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.9.0",
3
+ "version": "2.11.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "bin/mops.js",
@@ -53,6 +53,24 @@ describe("check-stable", () => {
53
53
  expect(existsSync(path.join(cwd, "new.most"))).toBe(false);
54
54
  expect(existsSync(path.join(cwd, "new.wasm"))).toBe(false);
55
55
  });
56
+ test("[canisters.X].args are passed to moc (enhanced migration)", async () => {
57
+ const cwd = path.join(import.meta.dirname, "check-stable/canister-args");
58
+ const result = await cli(["check-stable", "old.most"], { cwd });
59
+ expect(result.exitCode).toBe(0);
60
+ expect(result.stdout).toMatch(/Stable compatibility check passed/);
61
+ });
62
+ test("no args checks all canisters with [check-stable] config", async () => {
63
+ const cwd = path.join(import.meta.dirname, "check/deployed-compatible");
64
+ const result = await cli(["check-stable"], { cwd });
65
+ expect(result.exitCode).toBe(0);
66
+ expect(result.stdout).toMatch(/Stable compatibility check passed/);
67
+ });
68
+ test("canister name filters to specific canister", async () => {
69
+ const cwd = path.join(import.meta.dirname, "check/deployed-compatible");
70
+ const result = await cli(["check-stable", "backend"], { cwd });
71
+ expect(result.exitCode).toBe(0);
72
+ expect(result.stdout).toMatch(/Stable compatibility check passed/);
73
+ });
56
74
  test("errors when old file does not exist", async () => {
57
75
  const cwd = path.join(import.meta.dirname, "check-stable/compatible");
58
76
  const result = await cli(["check-stable", "nonexistent.mo"], { cwd });
@@ -34,30 +34,48 @@ describe("check", () => {
34
34
  const cwd = path.join(import.meta.dirname, "check/moc-args");
35
35
  await cliSnapshot(["check", "Warning.mo"], { cwd }, 1);
36
36
  });
37
- test("no args falls back to [canisters] entrypoints", async () => {
37
+ test("no args checks all canisters", async () => {
38
38
  const cwd = path.join(import.meta.dirname, "check/canisters");
39
39
  await cliSnapshot(["check"], { cwd }, 0);
40
40
  });
41
- test("canister entrypoint resolved relative to config root when run from subdirectory", async () => {
41
+ test("canister name filters to specific canister", async () => {
42
+ const cwd = path.join(import.meta.dirname, "check/canisters");
43
+ const result = await cli(["check", "backend"], { cwd });
44
+ expect(result.exitCode).toBe(0);
45
+ expect(result.stdout).toMatch(/✓ backend/);
46
+ });
47
+ test("canister resolved relative to config root when run from subdirectory", async () => {
42
48
  const fixtureRoot = path.join(import.meta.dirname, "check/canisters-subdir");
43
49
  const subdir = path.join(fixtureRoot, "src/backend");
44
50
  const result = await cli(["check"], { cwd: subdir });
45
51
  expect(result.exitCode).toBe(0);
46
52
  expect(result.stdout).toMatch(/✓/);
47
53
  });
48
- test("[moc] args applied when using canister fallback", async () => {
54
+ test("[moc] args applied to canister check", async () => {
49
55
  const cwd = path.join(import.meta.dirname, "check/canisters-moc-args");
50
56
  const result = await cli(["check"], { cwd });
51
57
  expect(result.exitCode).toBe(1);
52
58
  expect(result.stderr).toMatch(/warning \[M0194\]/);
53
59
  });
54
- test("canister entrypoint with errors", async () => {
60
+ test("[canisters.X].args applied to canister check", async () => {
61
+ const cwd = path.join(import.meta.dirname, "check/canisters-canister-args");
62
+ const result = await cli(["check"], { cwd });
63
+ expect(result.exitCode).toBe(1);
64
+ expect(result.stderr).toMatch(/warning \[M0194\]/);
65
+ });
66
+ test("canister with errors", async () => {
55
67
  const cwd = path.join(import.meta.dirname, "check/canisters-error");
56
68
  const result = await cli(["check"], { cwd });
57
69
  expect(result.exitCode).toBe(1);
58
70
  expect(result.stderr).toMatch(/error/i);
59
71
  });
60
- test("--fix with canister fallback", async () => {
72
+ test("invalid canister name errors", async () => {
73
+ const cwd = path.join(import.meta.dirname, "check/canisters");
74
+ const result = await cli(["check", "nonexistent"], { cwd });
75
+ expect(result.exitCode).toBe(1);
76
+ expect(result.stderr).toMatch(/not found in mops\.toml/);
77
+ });
78
+ test("--fix with canister", async () => {
61
79
  const cwd = path.join(import.meta.dirname, "check/canisters");
62
80
  const result = await cli(["check", "--fix"], { cwd });
63
81
  expect(result.exitCode).toBe(0);
@@ -0,0 +1 @@
1
+ export {};