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.
- package/CHANGELOG.md +13 -0
- package/bundle/cli.tgz +0 -0
- package/cli.ts +34 -10
- package/commands/build.ts +16 -22
- package/commands/check-stable.ts +141 -17
- package/commands/check.ts +195 -101
- package/commands/migrate.ts +165 -0
- package/declarations/main/main.did +38 -0
- package/declarations/main/main.did.d.ts +36 -0
- package/declarations/main/main.did.js +36 -0
- package/dist/cli.js +28 -10
- package/dist/commands/build.js +7 -13
- package/dist/commands/check-stable.d.ts +8 -1
- package/dist/commands/check-stable.js +101 -19
- package/dist/commands/check.d.ts +1 -1
- package/dist/commands/check.js +136 -78
- package/dist/commands/migrate.d.ts +2 -0
- package/dist/commands/migrate.js +104 -0
- package/dist/declarations/main/main.did +38 -0
- package/dist/declarations/main/main.did.d.ts +36 -0
- package/dist/declarations/main/main.did.js +36 -0
- package/dist/helpers/migrations.d.ts +10 -0
- package/dist/helpers/migrations.js +109 -0
- package/dist/helpers/resolve-canisters.d.ts +3 -1
- package/dist/helpers/resolve-canisters.js +34 -5
- package/dist/package.json +1 -1
- package/dist/tests/check-stable.test.js +18 -0
- package/dist/tests/check.test.js +23 -5
- package/dist/tests/migrate.test.d.ts +1 -0
- package/dist/tests/migrate.test.js +160 -0
- package/dist/types.d.ts +7 -0
- package/helpers/migrations.ts +166 -0
- package/helpers/resolve-canisters.ts +53 -5
- package/package.json +1 -1
- package/tests/__snapshots__/check.test.ts.snap +2 -2
- package/tests/__snapshots__/migrate.test.ts.snap +119 -0
- package/tests/check/canisters-canister-args/Warning.mo +5 -0
- package/tests/check/canisters-canister-args/mops.toml +9 -0
- package/tests/check-stable/canister-args/migrations/20250101_000000_Init.mo +8 -0
- package/tests/check-stable/canister-args/migrations/20250201_000000_AddField.mo +9 -0
- package/tests/check-stable/canister-args/mops.toml +9 -0
- package/tests/check-stable/canister-args/old.most +8 -0
- package/tests/check-stable/canister-args/src/main.mo +11 -0
- package/tests/check-stable.test.ts +21 -0
- package/tests/check.test.ts +26 -5
- package/tests/migrate/basic/deployed.most +12 -0
- package/tests/migrate/basic/migrations/20250101_000000_Init.mo +5 -0
- package/tests/migrate/basic/migrations/20250201_000000_AddName.mo +5 -0
- package/tests/migrate/basic/migrations/20250301_000000_AddEmail.mo +9 -0
- package/tests/migrate/basic/mops.toml +15 -0
- package/tests/migrate/basic/next-migration/.gitkeep +0 -0
- package/tests/migrate/basic/src/main.mo +11 -0
- package/tests/migrate/with-next/deployed.most +12 -0
- package/tests/migrate/with-next/migrations/20250101_000000_Init.mo +5 -0
- package/tests/migrate/with-next/migrations/20250201_000000_AddName.mo +5 -0
- package/tests/migrate/with-next/migrations/20250301_000000_AddEmail.mo +9 -0
- package/tests/migrate/with-next/mops.toml +15 -0
- package/tests/migrate/with-next/next-migration/20250401_000000_RenameId.mo +9 -0
- package/tests/migrate/with-next/src/main.mo +11 -0
- package/tests/migrate.test.ts +228 -0
- 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
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
@@ -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 });
|
package/dist/tests/check.test.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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("
|
|
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("
|
|
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 {};
|