ic-mops 2.8.1 → 2.10.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 (73) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/bundle/cli.tgz +0 -0
  3. package/cli.ts +21 -11
  4. package/commands/build.ts +134 -114
  5. package/commands/check-stable.ts +110 -17
  6. package/commands/check.ts +183 -102
  7. package/commands/info.ts +103 -0
  8. package/commands/lint.ts +132 -42
  9. package/commands/toolchain/moc.ts +5 -5
  10. package/dist/cli.js +19 -10
  11. package/dist/commands/build.js +105 -82
  12. package/dist/commands/check-stable.d.ts +7 -1
  13. package/dist/commands/check-stable.js +83 -19
  14. package/dist/commands/check.d.ts +1 -1
  15. package/dist/commands/check.js +127 -78
  16. package/dist/commands/info.d.ts +4 -0
  17. package/dist/commands/info.js +75 -0
  18. package/dist/commands/lint.js +84 -37
  19. package/dist/commands/toolchain/moc.js +5 -5
  20. package/dist/helpers/resolve-canisters.d.ts +3 -1
  21. package/dist/helpers/resolve-canisters.js +20 -5
  22. package/dist/package.json +3 -1
  23. package/dist/templates/mops-publish.yml +1 -1
  24. package/dist/templates/mops-test.yml +1 -1
  25. package/dist/tests/build.test.js +17 -0
  26. package/dist/tests/check-stable.test.js +18 -0
  27. package/dist/tests/check.test.js +23 -5
  28. package/dist/tests/lint.test.js +33 -0
  29. package/dist/types.d.ts +1 -0
  30. package/dist/wasm/pkg/nodejs/wasm_bg.wasm +0 -0
  31. package/dist/wasm/pkg/web/wasm_bg.wasm +0 -0
  32. package/helpers/resolve-canisters.ts +36 -5
  33. package/package.json +3 -1
  34. package/templates/mops-publish.yml +1 -1
  35. package/templates/mops-test.yml +1 -1
  36. package/tests/__snapshots__/check.test.ts.snap +2 -2
  37. package/tests/__snapshots__/lint.test.ts.snap +163 -5
  38. package/tests/build.test.ts +17 -0
  39. package/tests/check/canisters-canister-args/Warning.mo +5 -0
  40. package/tests/check/canisters-canister-args/mops.toml +9 -0
  41. package/tests/check-stable/canister-args/migrations/20250101_000000_Init.mo +8 -0
  42. package/tests/check-stable/canister-args/migrations/20250201_000000_AddField.mo +9 -0
  43. package/tests/check-stable/canister-args/mops.toml +9 -0
  44. package/tests/check-stable/canister-args/old.most +8 -0
  45. package/tests/check-stable/canister-args/src/main.mo +11 -0
  46. package/tests/check-stable.test.ts +21 -0
  47. package/tests/check.test.ts +26 -5
  48. package/tests/lint-extra/mops.toml +5 -0
  49. package/tests/lint-extra/src/Ok.mo +5 -0
  50. package/tests/lint-extra/src/restricted/B.mo +8 -0
  51. package/tests/lint-extra/src/restricted/Restricted.mo +8 -0
  52. package/tests/lint-extra-edge-cases/mops.toml +8 -0
  53. package/tests/lint-extra-edge-cases/src/Clean.mo +5 -0
  54. package/tests/lint-extra-example-rules/lint/migration-only/migration-only.toml +9 -0
  55. package/tests/lint-extra-example-rules/lint/no-types/no-types.toml +5 -0
  56. package/tests/lint-extra-example-rules/lint/types-only/types-only.toml +6 -0
  57. package/tests/lint-extra-example-rules/mops.toml +7 -0
  58. package/tests/lint-extra-example-rules/src/Main.mo +10 -0
  59. package/tests/lint-extra-example-rules/src/Migration.mo +9 -0
  60. package/tests/lint-extra-example-rules/src/Types.mo +10 -0
  61. package/tests/lint-extra-with-base/mops.toml +8 -0
  62. package/tests/lint-extra-with-base/src/BadBase.mo +8 -0
  63. package/tests/lint-extra-with-base/src/Ok.mo +5 -0
  64. package/tests/lint-extra-with-base/src/Restricted.mo +5 -0
  65. package/tests/lint-extra-with-cli-rules/empty-rules/.gitkeep +0 -0
  66. package/tests/lint-extra-with-cli-rules/mops.toml +5 -0
  67. package/tests/lint-extra-with-cli-rules/rules-b/no-bool-switch-2.toml +9 -0
  68. package/tests/lint-extra-with-cli-rules/src/Ok.mo +5 -0
  69. package/tests/lint-extra-with-cli-rules/src/Restricted.mo +8 -0
  70. package/tests/lint.test.ts +42 -0
  71. package/types.ts +1 -0
  72. package/wasm/pkg/nodejs/wasm_bg.wasm +0 -0
  73. package/wasm/pkg/web/wasm_bg.wasm +0 -0
@@ -1,13 +1,12 @@
1
1
  import path from "node:path";
2
- import { existsSync } from "node:fs";
3
2
  import chalk from "chalk";
4
3
  import { execa } from "execa";
5
4
  import { cliError } from "../error.js";
6
5
  import { getGlobalMocArgs, getRootDir, readConfig, resolveConfigPath, } from "../mops.js";
7
6
  import { autofixMotoko } from "../helpers/autofix-motoko.js";
8
7
  import { getMocSemVer } from "../helpers/get-moc-version.js";
9
- import { resolveCanisterConfigs, resolveCanisterEntrypoints, } from "../helpers/resolve-canisters.js";
10
- import { runStableCheck } from "./check-stable.js";
8
+ import { filterCanisters, looksLikeFile, resolveCanisterConfigs, validateCanisterArgs, } from "../helpers/resolve-canisters.js";
9
+ import { resolveStablePath, runStableCheck } from "./check-stable.js";
11
10
  import { sourcesArgs } from "./sources.js";
12
11
  import { toolchain } from "./toolchain/index.js";
13
12
  import { collectLintRules, lint } from "./lint.js";
@@ -16,35 +15,137 @@ function supportsAllLibsFlag() {
16
15
  const version = getMocSemVer();
17
16
  return version ? version.compare(MOC_ALL_LIBS_MIN_VERSION) >= 0 : false;
18
17
  }
19
- export async function check(files, options = {}) {
20
- const explicitFiles = Array.isArray(files) ? files : files ? [files] : [];
21
- let fileList = [...explicitFiles];
22
- const config = readConfig();
23
- if (fileList.length === 0) {
24
- fileList = resolveCanisterEntrypoints(config).map(resolveConfigPath);
25
- }
26
- if (fileList.length === 0) {
27
- cliError("No Motoko files specified and no canisters defined in mops.toml.\n" +
28
- "Either pass files: mops check <files...>\n" +
29
- "Or define canisters in mops.toml:\n\n" +
30
- " [canisters.backend]\n" +
31
- ' main = "src/main.mo"');
32
- }
33
- const mocPath = await toolchain.bin("moc", { fallback: true });
34
- const sources = await sourcesArgs();
35
- const globalMocArgs = getGlobalMocArgs(config);
36
- // --all-libs enables richer diagnostics with edit suggestions from moc (requires moc >= 1.3.0)
18
+ function checkAllLibsSupport(verbose) {
37
19
  const allLibs = supportsAllLibsFlag();
38
20
  if (!allLibs) {
39
21
  console.log(chalk.yellow(`moc < ${MOC_ALL_LIBS_MIN_VERSION}: some diagnostic hints may be missing`));
40
22
  }
41
- else if (options.verbose) {
23
+ else if (verbose) {
42
24
  console.log(chalk.blue("check"), chalk.gray("Using --all-libs for richer diagnostics"));
43
25
  }
26
+ return allLibs;
27
+ }
28
+ function logAutofixResult(fixResult, verbose) {
29
+ if (fixResult) {
30
+ for (const [file, codes] of fixResult.fixedFiles) {
31
+ const unique = [...new Set(codes)].sort();
32
+ const n = codes.length;
33
+ const rel = path.relative(process.cwd(), file);
34
+ console.log(chalk.green(`Fixed ${rel} (${n} ${n === 1 ? "fix" : "fixes"}: ${unique.join(", ")})`));
35
+ }
36
+ const fileCount = fixResult.fixedFiles.size;
37
+ console.log(chalk.green(`\n✓ ${fixResult.totalFixCount} ${fixResult.totalFixCount === 1 ? "fix" : "fixes"} applied to ${fileCount} ${fileCount === 1 ? "file" : "files"}`));
38
+ }
39
+ else if (verbose) {
40
+ console.log(chalk.yellow("No fixes were needed"));
41
+ }
42
+ }
43
+ export async function check(args, options = {}) {
44
+ const config = readConfig();
45
+ const canisters = resolveCanisterConfigs(config);
46
+ const hasCanisters = Object.keys(canisters).length > 0;
47
+ const fileArgs = args.filter(looksLikeFile);
48
+ const nonFileArgs = args.filter((a) => !looksLikeFile(a));
49
+ const isFileMode = fileArgs.length > 0;
50
+ if (isFileMode && nonFileArgs.length > 0) {
51
+ cliError(`Cannot mix file paths and canister names: ${args.join(", ")}\n` +
52
+ "Pass either file paths (e.g. mops check src/main.mo) or canister names (e.g. mops check backend)");
53
+ }
54
+ if (isFileMode) {
55
+ await checkFiles(config, fileArgs, options);
56
+ }
57
+ else {
58
+ if (!hasCanisters) {
59
+ cliError("No canisters defined in mops.toml.\n" +
60
+ "Either pass files: mops check <files...>\n" +
61
+ "Or define canisters in mops.toml:\n\n" +
62
+ " [canisters.backend]\n" +
63
+ ' main = "src/main.mo"');
64
+ }
65
+ const canisterNames = args.length > 0 ? args : undefined;
66
+ const filtered = filterCanisters(canisters, canisterNames);
67
+ await checkCanisters(config, filtered, options);
68
+ }
69
+ if (config.toolchain?.lintoko) {
70
+ const rootDir = getRootDir();
71
+ const lintRules = await collectLintRules(config, rootDir);
72
+ const lintFiles = isFileMode ? fileArgs : undefined;
73
+ await lint(undefined, {
74
+ verbose: options.verbose,
75
+ fix: options.fix,
76
+ rules: lintRules,
77
+ files: lintFiles,
78
+ });
79
+ }
80
+ }
81
+ async function checkCanisters(config, canisters, options) {
82
+ const mocPath = await toolchain.bin("moc", { fallback: true });
83
+ const sources = (await sourcesArgs()).flat();
84
+ const globalMocArgs = getGlobalMocArgs(config);
85
+ const allLibs = checkAllLibsSupport(options.verbose);
86
+ for (const [canisterName, canister] of Object.entries(canisters)) {
87
+ if (!canister.main) {
88
+ cliError(`No main file specified for canister '${canisterName}' in mops.toml`);
89
+ }
90
+ validateCanisterArgs(canister, canisterName);
91
+ const motokoPath = resolveConfigPath(canister.main);
92
+ const mocArgs = [
93
+ "--check",
94
+ ...(allLibs ? ["--all-libs"] : []),
95
+ ...sources,
96
+ ...globalMocArgs,
97
+ ...(canister.args ?? []),
98
+ ...(options.extraArgs ?? []),
99
+ ];
100
+ if (options.fix) {
101
+ if (options.verbose) {
102
+ console.log(chalk.blue("check"), chalk.gray(`Attempting to fix ${canisterName}`));
103
+ }
104
+ const fixResult = await autofixMotoko(mocPath, [motokoPath], mocArgs);
105
+ logAutofixResult(fixResult, options.verbose);
106
+ }
107
+ try {
108
+ const args = [motokoPath, ...mocArgs];
109
+ if (options.verbose) {
110
+ console.log(chalk.blue("check"), chalk.gray(`Checking canister ${canisterName}:`));
111
+ console.log(chalk.gray(mocPath, JSON.stringify(args)));
112
+ }
113
+ const result = await execa(mocPath, args, {
114
+ stdio: "inherit",
115
+ reject: false,
116
+ });
117
+ if (result.exitCode !== 0) {
118
+ cliError(`✗ Check failed for canister ${canisterName} (exit code: ${result.exitCode})`);
119
+ }
120
+ console.log(chalk.green(`✓ ${canisterName}`));
121
+ }
122
+ catch (err) {
123
+ cliError(`Error while checking canister ${canisterName}${err?.message ? `\n${err.message}` : ""}`);
124
+ }
125
+ const stablePath = resolveStablePath(canister, canisterName);
126
+ if (stablePath) {
127
+ await runStableCheck({
128
+ oldFile: stablePath,
129
+ canisterMain: motokoPath,
130
+ canisterName,
131
+ mocPath,
132
+ globalMocArgs,
133
+ canisterArgs: canister.args ?? [],
134
+ sources,
135
+ options: { verbose: options.verbose, extraArgs: options.extraArgs },
136
+ });
137
+ }
138
+ }
139
+ }
140
+ async function checkFiles(config, files, options) {
141
+ const mocPath = await toolchain.bin("moc", { fallback: true });
142
+ const sources = (await sourcesArgs()).flat();
143
+ const globalMocArgs = getGlobalMocArgs(config);
144
+ const allLibs = checkAllLibsSupport(options.verbose);
44
145
  const mocArgs = [
45
146
  "--check",
46
147
  ...(allLibs ? ["--all-libs"] : []),
47
- ...sources.flat(),
148
+ ...sources,
48
149
  ...globalMocArgs,
49
150
  ...(options.extraArgs ?? []),
50
151
  ];
@@ -52,24 +153,10 @@ export async function check(files, options = {}) {
52
153
  if (options.verbose) {
53
154
  console.log(chalk.blue("check"), chalk.gray("Attempting to fix files"));
54
155
  }
55
- const fixResult = await autofixMotoko(mocPath, fileList, mocArgs);
56
- if (fixResult) {
57
- for (const [file, codes] of fixResult.fixedFiles) {
58
- const unique = [...new Set(codes)].sort();
59
- const n = codes.length;
60
- const rel = path.relative(process.cwd(), file);
61
- console.log(chalk.green(`Fixed ${rel} (${n} ${n === 1 ? "fix" : "fixes"}: ${unique.join(", ")})`));
62
- }
63
- const fileCount = fixResult.fixedFiles.size;
64
- console.log(chalk.green(`\n✓ ${fixResult.totalFixCount} ${fixResult.totalFixCount === 1 ? "fix" : "fixes"} applied to ${fileCount} ${fileCount === 1 ? "file" : "files"}`));
65
- }
66
- else {
67
- if (options.verbose) {
68
- console.log(chalk.yellow("No fixes were needed"));
69
- }
70
- }
156
+ const fixResult = await autofixMotoko(mocPath, files, mocArgs);
157
+ logAutofixResult(fixResult, options.verbose);
71
158
  }
72
- for (const file of fileList) {
159
+ for (const file of files) {
73
160
  try {
74
161
  const args = [file, ...mocArgs];
75
162
  if (options.verbose) {
@@ -89,42 +176,4 @@ export async function check(files, options = {}) {
89
176
  cliError(`Error while checking ${file}${err?.message ? `\n${err.message}` : ""}`);
90
177
  }
91
178
  }
92
- const canisters = resolveCanisterConfigs(config);
93
- for (const [name, canister] of Object.entries(canisters)) {
94
- const stableConfig = canister["check-stable"];
95
- if (!stableConfig) {
96
- continue;
97
- }
98
- if (!canister.main) {
99
- cliError(`No main file specified for canister '${name}' in mops.toml`);
100
- }
101
- const stablePath = resolveConfigPath(stableConfig.path);
102
- if (!existsSync(stablePath)) {
103
- if (stableConfig.skipIfMissing) {
104
- continue;
105
- }
106
- cliError(`Deployed file not found: ${stablePath} (canister '${name}')\n` +
107
- "Set skipIfMissing = true in [canisters." +
108
- name +
109
- ".check-stable] to skip this check when the file is missing.");
110
- }
111
- await runStableCheck({
112
- oldFile: stablePath,
113
- canisterMain: resolveConfigPath(canister.main),
114
- canisterName: name,
115
- mocPath,
116
- globalMocArgs,
117
- options: { verbose: options.verbose, extraArgs: options.extraArgs },
118
- });
119
- }
120
- if (config.toolchain?.lintoko) {
121
- const rootDir = getRootDir();
122
- const lintRules = await collectLintRules(config, rootDir);
123
- await lint(undefined, {
124
- verbose: options.verbose,
125
- fix: options.fix,
126
- rules: lintRules,
127
- files: explicitFiles.length > 0 ? explicitFiles : undefined,
128
- });
129
- }
130
179
  }
@@ -0,0 +1,4 @@
1
+ export interface InfoOptions {
2
+ versions?: boolean;
3
+ }
4
+ export declare function info(pkgArg: string, options?: InfoOptions): Promise<void>;
@@ -0,0 +1,75 @@
1
+ import process from "node:process";
2
+ import chalk from "chalk";
3
+ import { mainActor } from "../api/actors.js";
4
+ import { resolveVersion } from "../api/resolveVersion.js";
5
+ function label(text) {
6
+ return chalk.bold(text.padEnd(16));
7
+ }
8
+ export async function info(pkgArg, options = {}) {
9
+ let [name, versionArg] = pkgArg.split("@");
10
+ let actor = await mainActor();
11
+ let version;
12
+ try {
13
+ version = await resolveVersion(name, versionArg ?? "");
14
+ }
15
+ catch (err) {
16
+ let message = err instanceof Error ? err.message : String(err);
17
+ console.error(chalk.red("Error: ") + message);
18
+ process.exit(1);
19
+ }
20
+ let res = await actor.getPackageDetails(name, version);
21
+ if ("err" in res) {
22
+ console.error(chalk.red("Error: ") + res.err);
23
+ process.exit(1);
24
+ }
25
+ let d = res.ok;
26
+ let c = d.config;
27
+ // d.versions is in ascending order (oldest first)
28
+ if (options.versions) {
29
+ for (let ver of d.versions) {
30
+ console.log(ver);
31
+ }
32
+ return;
33
+ }
34
+ console.log("");
35
+ console.log(`${chalk.green.bold(c.name)}${chalk.gray("@")}${chalk.yellow(c.version)}`);
36
+ if (c.description) {
37
+ console.log(chalk.dim(c.description));
38
+ }
39
+ if (c.version !== d.highestVersion) {
40
+ console.log(chalk.yellow(`latest: ${d.highestVersion}`));
41
+ }
42
+ console.log("");
43
+ if (c.license) {
44
+ console.log(`${label("license")}${c.license}`);
45
+ }
46
+ if (c.repository) {
47
+ console.log(`${label("repository")}${chalk.cyan(c.repository)}`);
48
+ }
49
+ if (c.homepage) {
50
+ console.log(`${label("homepage")}${chalk.cyan(c.homepage)}`);
51
+ }
52
+ if (c.documentation) {
53
+ console.log(`${label("documentation")}${chalk.cyan(c.documentation)}`);
54
+ }
55
+ if (c.dependencies.length > 0) {
56
+ console.log("");
57
+ console.log(`${label("dependencies")}${c.dependencies.map((dep) => `${dep.name}${chalk.gray("@")}${dep.version || dep.repo}`).join(", ")}`);
58
+ }
59
+ if (c.devDependencies.length > 0) {
60
+ console.log(`${label("dev-deps")}${c.devDependencies.map((dep) => `${dep.name}${chalk.gray("@")}${dep.version || dep.repo}`).join(", ")}`);
61
+ }
62
+ if (c.keywords.length > 0) {
63
+ console.log("");
64
+ console.log(`${label("keywords")}${c.keywords.map((k) => chalk.yellow(k)).join(", ")}`);
65
+ }
66
+ if (d.versions.length > 0) {
67
+ let versionsDisplay = d.versions.slice(-10).reverse().join(", ");
68
+ let extra = d.versions.length > 10
69
+ ? ` ${chalk.gray(`(+${d.versions.length - 10} more)`)}`
70
+ : "";
71
+ console.log("");
72
+ console.log(`${label("versions")}${versionsDisplay}${extra}`);
73
+ }
74
+ console.log("");
75
+ }
@@ -64,6 +64,43 @@ export async function collectLintRules(config, rootDir) {
64
64
  const depRules = await resolveDepRules(config, rootDir);
65
65
  return [...localRules, ...depRules];
66
66
  }
67
+ function buildCommonArgs(options, config) {
68
+ const args = [];
69
+ if (options.verbose) {
70
+ args.push("--verbose");
71
+ }
72
+ if (options.fix) {
73
+ args.push("--fix");
74
+ }
75
+ if (config.lint?.args) {
76
+ if (typeof config.lint.args === "string") {
77
+ cliError(`[lint] config 'args' should be an array of strings in mops.toml config file`);
78
+ }
79
+ args.push(...config.lint.args);
80
+ }
81
+ if (options.extraArgs && options.extraArgs.length > 0) {
82
+ args.push(...options.extraArgs);
83
+ }
84
+ return args;
85
+ }
86
+ async function runLintoko(lintokoBinPath, rootDir, args, options, label) {
87
+ try {
88
+ if (options.verbose) {
89
+ console.log(chalk.blue("lint"), chalk.gray(`Running lintoko (${label}):`));
90
+ console.log(chalk.gray(lintokoBinPath));
91
+ console.log(chalk.gray(JSON.stringify(args)));
92
+ }
93
+ const result = await execa(lintokoBinPath, args, {
94
+ cwd: rootDir,
95
+ stdio: "inherit",
96
+ reject: false,
97
+ });
98
+ return result.exitCode === 0;
99
+ }
100
+ catch (err) {
101
+ cliError(`Error while running lintoko${err?.message ? `\n${err.message}` : ""}`);
102
+ }
103
+ }
67
104
  export async function lint(filter, options) {
68
105
  let config = readConfig();
69
106
  let rootDir = getRootDir();
@@ -86,49 +123,59 @@ export async function lint(filter, options) {
86
123
  : "No .mo files found in the project");
87
124
  }
88
125
  }
89
- let args = [];
90
- if (options.verbose) {
91
- args.push("--verbose");
92
- }
93
- if (options.fix) {
94
- args.push("--fix");
95
- }
126
+ const commonArgs = buildCommonArgs(options, config);
127
+ // --- base run ---
128
+ const baseArgs = [...commonArgs];
96
129
  const rules = options.rules !== undefined
97
130
  ? options.rules
98
131
  : await collectLintRules(config, rootDir);
99
- rules.forEach((rule) => args.push("--rules", rule));
100
- if (config.lint?.args) {
101
- if (typeof config.lint.args === "string") {
102
- cliError(`[lint] config 'args' should be an array of strings in mops.toml config file`);
132
+ rules.forEach((rule) => baseArgs.push("--rules", rule));
133
+ baseArgs.push(...filesToLint);
134
+ let failed = !(await runLintoko(lintokoBinPath, rootDir, baseArgs, options, "base"));
135
+ // --- extra runs ---
136
+ const extraEntries = config.lint?.extra;
137
+ if (extraEntries) {
138
+ const isFiltered = filter || (options.files && options.files.length > 0);
139
+ const baseFileSet = isFiltered
140
+ ? new Set(filesToLint.map((f) => path.resolve(rootDir, f)))
141
+ : undefined;
142
+ for (const [globPattern, ruleDirs] of Object.entries(extraEntries)) {
143
+ if (!Array.isArray(ruleDirs) || ruleDirs.length === 0) {
144
+ console.warn(chalk.yellow(`[lint.extra] skipping '${globPattern}': value must be a non-empty array of rule directories`));
145
+ continue;
146
+ }
147
+ for (const dir of ruleDirs) {
148
+ if (!existsSync(path.join(rootDir, dir))) {
149
+ cliError(`[lint.extra] rule directory '${dir}' not found (referenced by glob '${globPattern}')`);
150
+ }
151
+ }
152
+ let matchedFiles = globSync(path.join(rootDir, globPattern), {
153
+ ...MOTOKO_GLOB_CONFIG,
154
+ cwd: rootDir,
155
+ });
156
+ if (baseFileSet) {
157
+ matchedFiles = matchedFiles.filter((f) => baseFileSet.has(path.resolve(rootDir, f)));
158
+ }
159
+ if (matchedFiles.length === 0) {
160
+ console.warn(chalk.yellow(`[lint.extra] no files matched glob '${globPattern}', skipping`));
161
+ continue;
162
+ }
163
+ const extraArgs = [...commonArgs];
164
+ for (const dir of ruleDirs) {
165
+ extraArgs.push("--rules", dir);
166
+ }
167
+ extraArgs.push(...matchedFiles);
168
+ const passed = await runLintoko(lintokoBinPath, rootDir, extraArgs, options, `extra: ${globPattern}`);
169
+ failed ||= !passed;
103
170
  }
104
- args.push(...config.lint.args);
105
171
  }
106
- if (options.extraArgs && options.extraArgs.length > 0) {
107
- args.push(...options.extraArgs);
172
+ if (failed) {
173
+ cliError("Lint failed");
108
174
  }
109
- args.push(...filesToLint);
110
- try {
111
- if (options.verbose) {
112
- console.log(chalk.blue("lint"), chalk.gray("Running lintoko:"));
113
- console.log(chalk.gray(lintokoBinPath));
114
- console.log(chalk.gray(JSON.stringify(args)));
115
- }
116
- const result = await execa(lintokoBinPath, args, {
117
- cwd: rootDir,
118
- stdio: "inherit",
119
- reject: false,
120
- });
121
- if (result.exitCode !== 0) {
122
- cliError(`Lint failed with exit code ${result.exitCode}`);
123
- }
124
- if (options.fix) {
125
- console.log(chalk.green("✓ Lint fixes applied"));
126
- }
127
- else {
128
- console.log(chalk.green("✓ Lint succeeded"));
129
- }
175
+ else if (options.fix) {
176
+ console.log(chalk.green("✓ Lint fixes applied"));
130
177
  }
131
- catch (err) {
132
- cliError(`Error while running lintoko${err?.message ? `\n${err.message}` : ""}`);
178
+ else {
179
+ console.log(chalk.green("✓ Lint succeeded"));
133
180
  }
134
181
  }
@@ -5,7 +5,7 @@ import { SemVer } from "semver";
5
5
  import { globalCacheDir } from "../../mops.js";
6
6
  import * as toolchainUtils from "./toolchain-utils.js";
7
7
  let cacheDir = path.join(globalCacheDir, "moc");
8
- export let repo = "dfinity/motoko";
8
+ export let repo = "caffeinelabs/motoko";
9
9
  export let getLatestReleaseTag = async () => {
10
10
  return toolchainUtils.getLatestReleaseTag(repo);
11
11
  };
@@ -33,7 +33,7 @@ export let download = async (version, { silent = false, verbose = false } = {})
33
33
  }
34
34
  else {
35
35
  // Download the .js artifact
36
- const jsUrl = `https://github.com/dfinity/motoko/releases/download/${version}/moc-${version}.js`;
36
+ const jsUrl = `https://github.com/caffeinelabs/motoko/releases/download/${version}/moc-${version}.js`;
37
37
  const jsDestPath = path.join(destDir, "moc.js");
38
38
  if (verbose && !silent) {
39
39
  console.log(`Downloading ${jsUrl}`);
@@ -61,16 +61,16 @@ export let download = async (version, { silent = false, verbose = false } = {})
61
61
  ? "arm64"
62
62
  : "aarch64"
63
63
  : "x86_64";
64
- url = `https://github.com/dfinity/motoko/releases/download/${version}/motoko-${platfrom}-${arch}-${version}.tar.gz`;
64
+ url = `https://github.com/caffeinelabs/motoko/releases/download/${version}/motoko-${platfrom}-${arch}-${version}.tar.gz`;
65
65
  }
66
66
  else if (new SemVer(version).compare(new SemVer("0.9.5")) >= 0) {
67
67
  let platfrom = process.platform == "darwin" ? "Darwin" : "Linux";
68
68
  let arch = "x86_64";
69
- url = `https://github.com/dfinity/motoko/releases/download/${version}/motoko-${platfrom}-${arch}-${version}.tar.gz`;
69
+ url = `https://github.com/caffeinelabs/motoko/releases/download/${version}/motoko-${platfrom}-${arch}-${version}.tar.gz`;
70
70
  }
71
71
  else {
72
72
  let platfrom = process.platform == "darwin" ? "macos" : "linux64";
73
- url = `https://github.com/dfinity/motoko/releases/download/${version}/motoko-${platfrom}-${version}.tar.gz`;
73
+ url = `https://github.com/caffeinelabs/motoko/releases/download/${version}/motoko-${platfrom}-${version}.tar.gz`;
74
74
  }
75
75
  if (verbose && !silent) {
76
76
  console.log(`Downloading ${url}`);
@@ -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): 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,14 @@ 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) {
43
+ if (canister.args && typeof canister.args === "string") {
44
+ cliError(`Canister config 'args' should be an array of strings for canister ${canisterName}`);
45
+ }
46
+ }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.8.1",
3
+ "version": "2.10.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "bin/mops.js",
@@ -65,6 +65,7 @@
65
65
  "prettier-plugin-motoko": "0.13.0",
66
66
  "promisify-child-process": "4.1.2",
67
67
  "prompts": "2.4.2",
68
+ "proper-lockfile": "4.1.2",
68
69
  "semver": "7.7.1",
69
70
  "stream-to-promise": "3.0.0",
70
71
  "string-width": "7.2.0",
@@ -81,6 +82,7 @@
81
82
  "@types/ncp": "2.0.8",
82
83
  "@types/node": "24.0.3",
83
84
  "@types/prompts": "2.4.9",
85
+ "@types/proper-lockfile": "4.1.4",
84
86
  "@types/semver": "7.5.8",
85
87
  "@types/stream-to-promise": "2.2.4",
86
88
  "@types/tar": "6.1.13",
@@ -10,7 +10,7 @@ jobs:
10
10
 
11
11
  steps:
12
12
  - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
13
- - uses: dfinity/setup-mops@v1
13
+ - uses: caffeinelabs/setup-mops@v1
14
14
  with:
15
15
  # Make sure you set the MOPS_IDENTITY_PEM secret in your repository settings https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository
16
16
  identity-pem: ${{ secrets.MOPS_IDENTITY_PEM }}
@@ -13,7 +13,7 @@ jobs:
13
13
 
14
14
  steps:
15
15
  - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
16
- - uses: dfinity/setup-mops@v1
16
+ - uses: caffeinelabs/setup-mops@v1
17
17
  with:
18
18
  mops-version: 1
19
19
 
@@ -85,6 +85,23 @@ describe("build", () => {
85
85
  cleanFixture(cwd, artifact, artifactDid);
86
86
  }
87
87
  });
88
+ test("parallel builds of the same canister both succeed", async () => {
89
+ const cwd = path.join(import.meta.dirname, "build/success");
90
+ try {
91
+ const [a, b] = await Promise.all([
92
+ cli(["build", "foo"], { cwd }),
93
+ cli(["build", "foo"], { cwd }),
94
+ ]);
95
+ expect(a.exitCode).toBe(0);
96
+ expect(b.exitCode).toBe(0);
97
+ expect(existsSync(path.join(cwd, ".mops/.build/foo.wasm"))).toBe(true);
98
+ expect(existsSync(path.join(cwd, ".mops/.build/foo.did"))).toBe(true);
99
+ expect(existsSync(path.join(cwd, ".mops/.build/foo.most"))).toBe(true);
100
+ }
101
+ finally {
102
+ cleanFixture(cwd);
103
+ }
104
+ });
88
105
  // Regression: bin/mops.js must route through environments/nodejs/cli.js
89
106
  // so that setWasmBindings() is called before any command runs.
90
107
  // The dev entry point (npm run mops) uses tsx and always worked;
@@ -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 });