ic-mops 2.8.0 → 2.9.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 (60) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bundle/cli.tgz +0 -0
  3. package/cli.ts +11 -1
  4. package/commands/build.ts +127 -92
  5. package/commands/check-stable.ts +9 -10
  6. package/commands/info.ts +103 -0
  7. package/commands/lint.ts +132 -42
  8. package/commands/toolchain/moc.ts +5 -5
  9. package/dist/cli.js +9 -0
  10. package/dist/commands/build.js +102 -69
  11. package/dist/commands/check-stable.js +9 -10
  12. package/dist/commands/info.d.ts +4 -0
  13. package/dist/commands/info.js +75 -0
  14. package/dist/commands/lint.js +84 -37
  15. package/dist/commands/toolchain/moc.js +5 -5
  16. package/dist/package.json +3 -1
  17. package/dist/templates/mops-publish.yml +1 -1
  18. package/dist/templates/mops-test.yml +1 -1
  19. package/dist/tests/build.test.js +17 -0
  20. package/dist/tests/check-stable.test.js +11 -0
  21. package/dist/tests/lint.test.js +33 -0
  22. package/dist/types.d.ts +1 -0
  23. package/dist/wasm/pkg/nodejs/wasm_bg.wasm +0 -0
  24. package/dist/wasm/pkg/web/wasm_bg.wasm +0 -0
  25. package/package.json +3 -1
  26. package/templates/mops-publish.yml +1 -1
  27. package/templates/mops-test.yml +1 -1
  28. package/tests/__snapshots__/lint.test.ts.snap +163 -5
  29. package/tests/build.test.ts +17 -0
  30. package/tests/check-stable/actor-idl/mops.toml +8 -0
  31. package/tests/check-stable/actor-idl/new.mo +6 -0
  32. package/tests/check-stable/actor-idl/old.mo +5 -0
  33. package/tests/check-stable/actor-idl/system-idl/aaaaa-aa.did +3 -0
  34. package/tests/check-stable.test.ts +12 -0
  35. package/tests/lint-extra/mops.toml +5 -0
  36. package/tests/lint-extra/src/Ok.mo +5 -0
  37. package/tests/lint-extra/src/restricted/B.mo +8 -0
  38. package/tests/lint-extra/src/restricted/Restricted.mo +8 -0
  39. package/tests/lint-extra-edge-cases/mops.toml +8 -0
  40. package/tests/lint-extra-edge-cases/src/Clean.mo +5 -0
  41. package/tests/lint-extra-example-rules/lint/migration-only/migration-only.toml +9 -0
  42. package/tests/lint-extra-example-rules/lint/no-types/no-types.toml +5 -0
  43. package/tests/lint-extra-example-rules/lint/types-only/types-only.toml +6 -0
  44. package/tests/lint-extra-example-rules/mops.toml +7 -0
  45. package/tests/lint-extra-example-rules/src/Main.mo +10 -0
  46. package/tests/lint-extra-example-rules/src/Migration.mo +9 -0
  47. package/tests/lint-extra-example-rules/src/Types.mo +10 -0
  48. package/tests/lint-extra-with-base/mops.toml +8 -0
  49. package/tests/lint-extra-with-base/src/BadBase.mo +8 -0
  50. package/tests/lint-extra-with-base/src/Ok.mo +5 -0
  51. package/tests/lint-extra-with-base/src/Restricted.mo +5 -0
  52. package/tests/lint-extra-with-cli-rules/empty-rules/.gitkeep +0 -0
  53. package/tests/lint-extra-with-cli-rules/mops.toml +5 -0
  54. package/tests/lint-extra-with-cli-rules/rules-b/no-bool-switch-2.toml +9 -0
  55. package/tests/lint-extra-with-cli-rules/src/Ok.mo +5 -0
  56. package/tests/lint-extra-with-cli-rules/src/Restricted.mo +8 -0
  57. package/tests/lint.test.ts +42 -0
  58. package/types.ts +1 -0
  59. package/wasm/pkg/nodejs/wasm_bg.wasm +0 -0
  60. package/wasm/pkg/web/wasm_bg.wasm +0 -0
@@ -8,7 +8,7 @@ import * as toolchainUtils from "./toolchain-utils.js";
8
8
 
9
9
  let cacheDir = path.join(globalCacheDir, "moc");
10
10
 
11
- export let repo = "dfinity/motoko";
11
+ export let repo = "caffeinelabs/motoko";
12
12
 
13
13
  export let getLatestReleaseTag = async () => {
14
14
  return toolchainUtils.getLatestReleaseTag(repo);
@@ -44,7 +44,7 @@ export let download = async (
44
44
  }
45
45
  } else {
46
46
  // Download the .js artifact
47
- const jsUrl = `https://github.com/dfinity/motoko/releases/download/${version}/moc-${version}.js`;
47
+ const jsUrl = `https://github.com/caffeinelabs/motoko/releases/download/${version}/moc-${version}.js`;
48
48
  const jsDestPath = path.join(destDir, "moc.js");
49
49
 
50
50
  if (verbose && !silent) {
@@ -75,14 +75,14 @@ export let download = async (
75
75
  ? "arm64"
76
76
  : "aarch64"
77
77
  : "x86_64";
78
- url = `https://github.com/dfinity/motoko/releases/download/${version}/motoko-${platfrom}-${arch}-${version}.tar.gz`;
78
+ url = `https://github.com/caffeinelabs/motoko/releases/download/${version}/motoko-${platfrom}-${arch}-${version}.tar.gz`;
79
79
  } else if (new SemVer(version).compare(new SemVer("0.9.5")) >= 0) {
80
80
  let platfrom = process.platform == "darwin" ? "Darwin" : "Linux";
81
81
  let arch = "x86_64";
82
- url = `https://github.com/dfinity/motoko/releases/download/${version}/motoko-${platfrom}-${arch}-${version}.tar.gz`;
82
+ url = `https://github.com/caffeinelabs/motoko/releases/download/${version}/motoko-${platfrom}-${arch}-${version}.tar.gz`;
83
83
  } else {
84
84
  let platfrom = process.platform == "darwin" ? "macos" : "linux64";
85
- url = `https://github.com/dfinity/motoko/releases/download/${version}/motoko-${platfrom}-${version}.tar.gz`;
85
+ url = `https://github.com/caffeinelabs/motoko/releases/download/${version}/motoko-${platfrom}-${version}.tar.gz`;
86
86
  }
87
87
 
88
88
  if (verbose && !silent) {
package/dist/cli.js CHANGED
@@ -15,6 +15,7 @@ import { checkStable } from "./commands/check-stable.js";
15
15
  import { docsCoverage } from "./commands/docs-coverage.js";
16
16
  import { docs } from "./commands/docs.js";
17
17
  import { format } from "./commands/format.js";
18
+ import { info } from "./commands/info.js";
18
19
  import { init } from "./commands/init.js";
19
20
  import { lint } from "./commands/lint.js";
20
21
  import { installAll } from "./commands/install/install-all.js";
@@ -213,6 +214,14 @@ program
213
214
  .action(async (text) => {
214
215
  await search(text);
215
216
  });
217
+ // info
218
+ program
219
+ .command("info <pkg>")
220
+ .description("Show detailed information about a package from the registry")
221
+ .option("--versions", "List all published versions, one per line")
222
+ .action(async (pkg, options) => {
223
+ await info(pkg, options);
224
+ });
216
225
  // cache
217
226
  program
218
227
  .command("cache")
@@ -3,6 +3,7 @@ import { execa } from "execa";
3
3
  import { exists } from "fs-extra";
4
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import { join } from "node:path";
6
+ import { lock, unlockSync } from "proper-lockfile";
6
7
  import { cliError } from "../error.js";
7
8
  import { isCandidCompatible } from "../helpers/is-candid-compatible.js";
8
9
  import { resolveCanisterConfigs } from "../helpers/resolve-canisters.js";
@@ -46,88 +47,120 @@ export async function build(canisterNames, options) {
46
47
  motokoPath = resolveConfigPath(motokoPath);
47
48
  const wasmPath = join(outputDir, `${canisterName}.wasm`);
48
49
  const mostPath = join(outputDir, `${canisterName}.most`);
49
- let args = [
50
- "-c",
51
- "--idl",
52
- "--stable-types",
53
- "-o",
54
- wasmPath,
55
- motokoPath,
56
- ...(await sourcesArgs()).flat(),
57
- ...getGlobalMocArgs(config),
58
- ];
59
- args.push(...collectExtraArgs(config, canister, canisterName, options.extraArgs));
60
- const isPublicCandid = true; // always true for now to reduce corner cases
61
- const candidVisibility = isPublicCandid ? "icp:public" : "icp:private";
62
- if (isPublicCandid) {
63
- args.push("--public-metadata", "candid:service");
64
- args.push("--public-metadata", "candid:args");
50
+ // per-canister lock to prevent parallel builds of the same canister from clobbering output files
51
+ const lockTarget = join(outputDir, `.${canisterName}.buildlock`);
52
+ await writeFile(lockTarget, "", { flag: "a" });
53
+ let release;
54
+ try {
55
+ release = await lock(lockTarget, {
56
+ stale: 300_000,
57
+ retries: { retries: 60, minTimeout: 500, maxTimeout: 5_000 },
58
+ });
65
59
  }
60
+ catch {
61
+ cliError(`Failed to acquire build lock for canister ${canisterName} — another build may be stuck`);
62
+ }
63
+ // proper-lockfile registers its own signal-exit handler, but it doesn't reliably
64
+ // fire on process.exit(). This manual handler covers that gap. Double-unlock is
65
+ // harmless (the second call throws and is caught).
66
+ const exitCleanup = () => {
67
+ try {
68
+ unlockSync(lockTarget);
69
+ }
70
+ catch { }
71
+ };
72
+ process.on("exit", exitCleanup);
66
73
  try {
67
- if (options.verbose) {
68
- console.log(chalk.gray(mocPath, JSON.stringify(args)));
74
+ let args = [
75
+ "-c",
76
+ "--idl",
77
+ "--stable-types",
78
+ "-o",
79
+ wasmPath,
80
+ motokoPath,
81
+ ...(await sourcesArgs()).flat(),
82
+ ...getGlobalMocArgs(config),
83
+ ];
84
+ args.push(...collectExtraArgs(config, canister, canisterName, options.extraArgs));
85
+ const isPublicCandid = true; // always true for now to reduce corner cases
86
+ const candidVisibility = isPublicCandid ? "icp:public" : "icp:private";
87
+ if (isPublicCandid) {
88
+ args.push("--public-metadata", "candid:service");
89
+ args.push("--public-metadata", "candid:args");
69
90
  }
70
- const result = await execa(mocPath, args, {
71
- stdio: options.verbose ? "inherit" : "pipe",
72
- reject: false,
73
- });
74
- if (result.exitCode !== 0) {
75
- if (!options.verbose) {
76
- if (result.stderr) {
77
- console.error(chalk.red(result.stderr));
78
- }
79
- if (result.stdout?.trim()) {
80
- console.error(chalk.yellow("Build output:"));
81
- console.error(result.stdout);
91
+ try {
92
+ if (options.verbose) {
93
+ console.log(chalk.gray(mocPath, JSON.stringify(args)));
94
+ }
95
+ const result = await execa(mocPath, args, {
96
+ stdio: options.verbose ? "inherit" : "pipe",
97
+ reject: false,
98
+ });
99
+ if (result.exitCode !== 0) {
100
+ if (!options.verbose) {
101
+ if (result.stderr) {
102
+ console.error(chalk.red(result.stderr));
103
+ }
104
+ if (result.stdout?.trim()) {
105
+ console.error(chalk.yellow("Build output:"));
106
+ console.error(result.stdout);
107
+ }
82
108
  }
109
+ cliError(`Build failed for canister ${canisterName} (exit code: ${result.exitCode})`);
83
110
  }
84
- cliError(`Build failed for canister ${canisterName} (exit code: ${result.exitCode})`);
85
- }
86
- if (options.verbose && result.stdout && result.stdout.trim()) {
87
- console.log(result.stdout);
88
- }
89
- options.verbose &&
90
- console.log(chalk.gray(`Stable types written to ${mostPath}`));
91
- const generatedDidPath = join(outputDir, `${canisterName}.did`);
92
- const resolvedCandidPath = canister.candid
93
- ? resolveConfigPath(canister.candid)
94
- : null;
95
- if (resolvedCandidPath) {
96
- try {
97
- const compatible = await isCandidCompatible(generatedDidPath, resolvedCandidPath);
98
- if (!compatible) {
99
- cliError(`Candid compatibility check failed for canister ${canisterName}`);
111
+ if (options.verbose && result.stdout && result.stdout.trim()) {
112
+ console.log(result.stdout);
113
+ }
114
+ options.verbose &&
115
+ console.log(chalk.gray(`Stable types written to ${mostPath}`));
116
+ const generatedDidPath = join(outputDir, `${canisterName}.did`);
117
+ const resolvedCandidPath = canister.candid
118
+ ? resolveConfigPath(canister.candid)
119
+ : null;
120
+ if (resolvedCandidPath) {
121
+ try {
122
+ const compatible = await isCandidCompatible(generatedDidPath, resolvedCandidPath);
123
+ if (!compatible) {
124
+ cliError(`Candid compatibility check failed for canister ${canisterName}`);
125
+ }
126
+ if (options.verbose) {
127
+ console.log(chalk.gray(`Candid compatibility check passed for canister ${canisterName}`));
128
+ }
100
129
  }
101
- if (options.verbose) {
102
- console.log(chalk.gray(`Candid compatibility check passed for canister ${canisterName}`));
130
+ catch (err) {
131
+ cliError(`Error during Candid compatibility check for canister ${canisterName}${err?.message ? `\n${err.message}` : ""}`);
103
132
  }
104
133
  }
105
- catch (err) {
106
- cliError(`Error during Candid compatibility check for canister ${canisterName}${err?.message ? `\n${err.message}` : ""}`);
134
+ options.verbose &&
135
+ console.log(chalk.gray(`Adding metadata to ${wasmPath}`));
136
+ const candidPath = resolvedCandidPath ?? generatedDidPath;
137
+ const candidText = await readFile(candidPath, "utf-8");
138
+ const customSections = [
139
+ { name: `${candidVisibility} candid:service`, data: candidText },
140
+ ];
141
+ if (canister.initArg) {
142
+ customSections.push({
143
+ name: `${candidVisibility} candid:args`,
144
+ data: canister.initArg,
145
+ });
107
146
  }
147
+ const wasmBytes = await readFile(wasmPath);
148
+ const newWasm = getWasmBindings().add_custom_sections(wasmBytes, customSections);
149
+ await writeFile(wasmPath, newWasm);
108
150
  }
109
- options.verbose &&
110
- console.log(chalk.gray(`Adding metadata to ${wasmPath}`));
111
- const candidPath = resolvedCandidPath ?? generatedDidPath;
112
- const candidText = await readFile(candidPath, "utf-8");
113
- const customSections = [
114
- { name: `${candidVisibility} candid:service`, data: candidText },
115
- ];
116
- if (canister.initArg) {
117
- customSections.push({
118
- name: `${candidVisibility} candid:args`,
119
- data: canister.initArg,
120
- });
151
+ catch (err) {
152
+ if (err.message?.includes("Build failed for canister")) {
153
+ throw err;
154
+ }
155
+ cliError(`Error while compiling canister ${canisterName}${err?.message ? `\n${err.message}` : ""}`);
121
156
  }
122
- const wasmBytes = await readFile(wasmPath);
123
- const newWasm = getWasmBindings().add_custom_sections(wasmBytes, customSections);
124
- await writeFile(wasmPath, newWasm);
125
157
  }
126
- catch (err) {
127
- if (err.message?.includes("Build failed for canister")) {
128
- throw err;
158
+ finally {
159
+ process.removeListener("exit", exitCleanup);
160
+ try {
161
+ await release?.();
129
162
  }
130
- cliError(`Error while compiling canister ${canisterName}${err?.message ? `\n${err.message}` : ""}`);
163
+ catch { }
131
164
  }
132
165
  }
133
166
  console.log(chalk.green(`\n✓ Built ${Object.keys(filteredCanisters).length} canister${Object.keys(filteredCanisters).length === 1 ? "" : "s"} successfully`));
@@ -1,6 +1,6 @@
1
- import { basename, join, relative, resolve } from "node:path";
1
+ import { basename, join } from "node:path";
2
2
  import { existsSync, mkdirSync } from "node:fs";
3
- import { rename, rm } from "node:fs/promises";
3
+ 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";
@@ -28,8 +28,7 @@ export async function checkStable(oldFile, canisterName, options = {}) {
28
28
  }
29
29
  export async function runStableCheck(params) {
30
30
  const { oldFile, canisterMain, canisterName, mocPath, globalMocArgs, options = {}, } = params;
31
- const checkStableDir = resolve(CHECK_STABLE_DIR);
32
- const sources = (await sourcesArgs({ cwd: checkStableDir })).flat();
31
+ const sources = (await sourcesArgs()).flat();
33
32
  const isOldMostFile = oldFile.endsWith(".most");
34
33
  if (!existsSync(oldFile)) {
35
34
  cliError(`File not found: ${oldFile}`);
@@ -65,10 +64,13 @@ export async function runStableCheck(params) {
65
64
  }
66
65
  }
67
66
  async function generateStableTypes(mocPath, moFile, outputPath, sources, globalMocArgs, options) {
68
- const relFile = relative(resolve(CHECK_STABLE_DIR), resolve(moFile));
67
+ const base = basename(outputPath, ".most");
68
+ const wasmPath = join(CHECK_STABLE_DIR, base + ".wasm");
69
69
  const args = [
70
70
  "--stable-types",
71
- relFile,
71
+ "-o",
72
+ wasmPath,
73
+ moFile,
72
74
  ...sources,
73
75
  ...globalMocArgs,
74
76
  ...(options.extraArgs ?? []),
@@ -78,7 +80,6 @@ async function generateStableTypes(mocPath, moFile, outputPath, sources, globalM
78
80
  console.log(chalk.gray(mocPath, JSON.stringify(args)));
79
81
  }
80
82
  const result = await execa(mocPath, args, {
81
- cwd: CHECK_STABLE_DIR,
82
83
  stdio: "pipe",
83
84
  reject: false,
84
85
  });
@@ -88,8 +89,6 @@ async function generateStableTypes(mocPath, moFile, outputPath, sources, globalM
88
89
  }
89
90
  cliError(`Failed to generate stable types for ${moFile} (exit code: ${result.exitCode})`);
90
91
  }
91
- const base = basename(moFile, ".mo");
92
- await rename(join(CHECK_STABLE_DIR, base + ".most"), outputPath);
93
- await rm(join(CHECK_STABLE_DIR, base + ".wasm"), { force: true });
92
+ await rm(wasmPath, { force: true });
94
93
  return outputPath;
95
94
  }
@@ -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}`);
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.8.0",
3
+ "version": "2.9.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;