ic-mops 2.5.0 → 2.6.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 (80) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/api/network.ts +1 -1
  3. package/bun.lock +1082 -78
  4. package/bundle/cli.tgz +0 -0
  5. package/cli.ts +1 -1
  6. package/commands/add.ts +4 -1
  7. package/commands/build.ts +7 -12
  8. package/commands/check.ts +20 -2
  9. package/commands/docs-coverage.ts +26 -21
  10. package/commands/install/install-dep.ts +5 -3
  11. package/commands/lint.ts +107 -10
  12. package/commands/publish.ts +24 -11
  13. package/commands/remove.ts +5 -2
  14. package/commands/self.ts +1 -1
  15. package/commands/sources.ts +3 -2
  16. package/commands/sync.ts +13 -16
  17. package/commands/test/test.ts +3 -3
  18. package/commands/update.ts +13 -7
  19. package/commands/watch/error-checker.ts +3 -8
  20. package/commands/watch/warning-checker.ts +3 -8
  21. package/dist/api/network.js +1 -1
  22. package/dist/cli.js +1 -1
  23. package/dist/commands/add.js +4 -1
  24. package/dist/commands/build.js +5 -10
  25. package/dist/commands/check.js +14 -2
  26. package/dist/commands/docs-coverage.js +22 -22
  27. package/dist/commands/install/install-dep.js +3 -3
  28. package/dist/commands/lint.d.ts +3 -0
  29. package/dist/commands/lint.js +75 -10
  30. package/dist/commands/publish.js +19 -11
  31. package/dist/commands/remove.js +5 -2
  32. package/dist/commands/self.js +1 -1
  33. package/dist/commands/sources.js +3 -2
  34. package/dist/commands/sync.js +9 -14
  35. package/dist/commands/test/test.js +3 -3
  36. package/dist/commands/update.js +9 -4
  37. package/dist/commands/watch/error-checker.js +3 -8
  38. package/dist/commands/watch/warning-checker.js +3 -8
  39. package/dist/helpers/find-changelog-entry.js +1 -1
  40. package/dist/integrity.js +9 -3
  41. package/dist/mops.js +3 -0
  42. package/dist/package.json +3 -5
  43. package/dist/release-cli.js +2 -2
  44. package/dist/resolve-packages.js +4 -4
  45. package/dist/tests/build.test.js +1 -1
  46. package/dist/tests/check.test.js +24 -0
  47. package/dist/tests/helpers.js +8 -1
  48. package/dist/tests/lint.test.js +28 -2
  49. package/dist/types.d.ts +2 -0
  50. package/dist/vessel.d.ts +1 -1
  51. package/dist/vessel.js +3 -2
  52. package/helpers/find-changelog-entry.ts +3 -1
  53. package/integrity.ts +12 -3
  54. package/mops.ts +7 -0
  55. package/package.json +3 -5
  56. package/release-cli.ts +2 -2
  57. package/resolve-packages.ts +6 -4
  58. package/tests/build.test.ts +1 -1
  59. package/tests/check/with-lint-fail/NoBoolSwitch.mo +8 -0
  60. package/tests/check/with-lint-fail/lints/no-bool-switch.toml +9 -0
  61. package/tests/check/with-lint-fail/mops.toml +9 -0
  62. package/tests/check/with-lint-pass/Ok.mo +5 -0
  63. package/tests/check/with-lint-pass/lints/no-bool-switch.toml +9 -0
  64. package/tests/check/with-lint-pass/mops.toml +9 -0
  65. package/tests/check.test.ts +28 -0
  66. package/tests/helpers.ts +9 -1
  67. package/tests/lint-config-rules/extra-rules/no-bool-switch.toml +9 -0
  68. package/tests/lint-config-rules/mops.toml +5 -0
  69. package/tests/lint-config-rules/src/NoBoolSwitch.mo +8 -0
  70. package/tests/lint-extends/mops.toml +8 -0
  71. package/tests/lint-extends/my-pkg/mops.toml +3 -0
  72. package/tests/lint-extends/my-pkg/rules/no-bool-switch.toml +9 -0
  73. package/tests/lint-extends/src/NoBoolSwitch.mo +8 -0
  74. package/tests/lint-extends-all/mops.toml +8 -0
  75. package/tests/lint-extends-all/src/NoBoolSwitch.mo +8 -0
  76. package/tests/lint-extends-ignored/mops.toml +8 -0
  77. package/tests/lint-extends-ignored/src/NoBoolSwitch.mo +8 -0
  78. package/tests/lint.test.ts +32 -2
  79. package/types.ts +2 -0
  80. package/vessel.ts +5 -3
@@ -4,7 +4,7 @@ import os from "node:os";
4
4
  import chalk from "chalk";
5
5
  import { getMocPath } from "../../helpers/get-moc-path.js";
6
6
  import { getGlobalMocArgs, getRootDir, readConfig } from "../../mops.js";
7
- import { sources } from "../sources.js";
7
+ import { sourcesArgs } from "../sources.js";
8
8
  import { parallel } from "../../parallel.js";
9
9
  import { globMoFiles } from "./globMoFiles.js";
10
10
  export class WarningChecker {
@@ -50,7 +50,7 @@ export class WarningChecker {
50
50
  onProgress();
51
51
  let rootDir = getRootDir();
52
52
  let mocPath = getMocPath();
53
- let deps = await sources({ cwd: rootDir });
53
+ let deps = (await sourcesArgs({ cwd: rootDir })).flat();
54
54
  let globalMocArgs = getGlobalMocArgs(readConfig());
55
55
  let paths = globMoFiles(rootDir);
56
56
  this.totalFiles = paths.length;
@@ -59,12 +59,7 @@ export class WarningChecker {
59
59
  let controller = new AbortController();
60
60
  let { signal } = controller;
61
61
  this.controllers.set(file, controller);
62
- let { stderr } = await promisify(execFile)(mocPath, [
63
- "--check",
64
- ...deps.flatMap((x) => x.split(" ")),
65
- ...globalMocArgs,
66
- file,
67
- ], { cwd: rootDir, signal }).catch((error) => {
62
+ let { stderr } = await promisify(execFile)(mocPath, ["--check", ...deps, ...globalMocArgs, file], { cwd: rootDir, signal }).catch((error) => {
68
63
  if (error.code === "ABORT_ERR") {
69
64
  return { stderr: "" };
70
65
  }
@@ -15,7 +15,7 @@ export function findChangelogEntry(changelog, version) {
15
15
  }
16
16
  }
17
17
  else if (node.type === "heading" &&
18
- toMarkdown(node).match(new RegExp(`\\b${version}\\b`))) {
18
+ toMarkdown(node).match(new RegExp(`\\b${version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`))) {
19
19
  depth = node.depth;
20
20
  found = true;
21
21
  }
package/dist/integrity.js CHANGED
@@ -50,7 +50,7 @@ export function getLocalFileHash(fileId) {
50
50
  return bytesToHex(sha256(fileData));
51
51
  }
52
52
  function getMopsTomlHash() {
53
- return bytesToHex(sha256(fs.readFileSync(getRootDir() + "/mops.toml")));
53
+ return bytesToHex(sha256(fs.readFileSync(path.join(getRootDir(), "mops.toml"))));
54
54
  }
55
55
  function getMopsTomlDepsHash() {
56
56
  let config = readConfig();
@@ -90,7 +90,13 @@ export function readLockFile() {
90
90
  let rootDir = getRootDir();
91
91
  let lockFile = path.join(rootDir, "mops.lock");
92
92
  if (fs.existsSync(lockFile)) {
93
- return JSON.parse(fs.readFileSync(lockFile).toString());
93
+ try {
94
+ return JSON.parse(fs.readFileSync(lockFile).toString());
95
+ }
96
+ catch {
97
+ console.error("mops.lock is corrupted. Delete it and run `mops install` to regenerate.");
98
+ process.exit(1);
99
+ }
94
100
  }
95
101
  return null;
96
102
  }
@@ -208,7 +214,7 @@ export async function checkLockFile(force = false) {
208
214
  }
209
215
  for (let [fileId, lockedHash] of Object.entries(hashes)) {
210
216
  // check if file belongs to package
211
- if (!fileId.startsWith(packageId)) {
217
+ if (!fileId.startsWith(packageId + "/")) {
212
218
  console.error("Integrity check failed");
213
219
  console.error(`File ${fileId} in lock file does not belong to package ${packageId}`);
214
220
  process.exit(1);
package/dist/mops.js CHANGED
@@ -131,6 +131,9 @@ export async function getGithubCommit(repo, ref) {
131
131
  res = await fetch(`https://api.github.com/repos/${repo}/commits/main`);
132
132
  json = await res.json();
133
133
  }
134
+ if (!res.ok || !json.sha) {
135
+ throw new Error(`Failed to fetch commit for ${repo}#${ref}: ${json.message || `HTTP ${res.status}`}`);
136
+ }
134
137
  return json;
135
138
  }
136
139
  export function getDependencyType(version) {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "bin/mops.js",
@@ -34,7 +34,6 @@
34
34
  "@noble/hashes": "1.8.0",
35
35
  "as-table": "1.0.55",
36
36
  "buffer": "6.0.3",
37
- "cacheable-request": "12.0.1",
38
37
  "chalk": "5.4.1",
39
38
  "change-case": "5.4.4",
40
39
  "chokidar": "3.6.0",
@@ -56,7 +55,7 @@
56
55
  "markdown-table": "3.0.4",
57
56
  "mdast-util-from-markdown": "2.0.2",
58
57
  "mdast-util-to-markdown": "2.1.2",
59
- "minimatch": "10.0.1",
58
+ "minimatch": "10.2.4",
60
59
  "ncp": "2.0.0",
61
60
  "octokit": "3.1.2",
62
61
  "pem-file": "1.0.1",
@@ -69,7 +68,7 @@
69
68
  "semver": "7.7.1",
70
69
  "stream-to-promise": "3.0.0",
71
70
  "string-width": "7.2.0",
72
- "tar": "7.5.9",
71
+ "tar": "7.5.11",
73
72
  "terminal-size": "4.0.0",
74
73
  "vscode-languageserver-textdocument": "1.0.12"
75
74
  },
@@ -78,7 +77,6 @@
78
77
  "@types/debounce": "1.2.4",
79
78
  "@types/decompress": "4.2.7",
80
79
  "@types/fs-extra": "11.0.4",
81
- "@types/glob": "8.1.0",
82
80
  "@types/jsdom": "28.0.0",
83
81
  "@types/ncp": "2.0.8",
84
82
  "@types/node": "24.0.3",
@@ -29,7 +29,7 @@ fs.cpSync(path.resolve(__dirname, `../cli-releases/versions/${version}.tgz`), pa
29
29
  fs.writeFileSync(path.resolve(__dirname, `../cli-releases/tags/${tag}`), version);
30
30
  console.log(`Release '${version}' created with tag '${tag}'`);
31
31
  if (!fs.existsSync(path.resolve(__dirname, "../cli-releases/releases.json"))) {
32
- fs.writeFileSync(path.resolve(__dirname, "../cli-releases/releases.json"), JSON.stringify({ tags: {}, versions: {} }, null, 2));
32
+ fs.writeFileSync(path.resolve(__dirname, "../cli-releases/releases.json"), JSON.stringify({ tags: {}, versions: {} }, null, 2) + "\n");
33
33
  }
34
34
  let releases = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../cli-releases/releases.json"), "utf8"));
35
35
  releases.tags[tag] = version;
@@ -41,4 +41,4 @@ releases.versions[version] = {
41
41
  url: `https://cli.mops.one/versions/${version}.tgz`,
42
42
  hash,
43
43
  };
44
- fs.writeFileSync(path.resolve(__dirname, "../cli-releases/releases.json"), JSON.stringify(releases, null, 2));
44
+ fs.writeFileSync(path.resolve(__dirname, "../cli-releases/releases.json"), JSON.stringify(releases, null, 2) + "\n");
@@ -21,8 +21,8 @@ export async function resolvePackages({ conflicts = "ignore", } = {}) {
21
21
  let packages = {};
22
22
  let versions = {};
23
23
  let compareVersions = (a = "0.0.0", b = "0.0.0") => {
24
- let ap = a.split(".").map((x) => parseInt(x));
25
- let bp = b.split(".").map((x) => parseInt(x));
24
+ let ap = a.split(".").map((x) => parseInt(x) || 0);
25
+ let bp = b.split(".").map((x) => parseInt(x) || 0);
26
26
  if (ap[0] - bp[0]) {
27
27
  return Math.sign(ap[0] - bp[0]);
28
28
  }
@@ -97,7 +97,7 @@ export async function resolvePackages({ conflicts = "ignore", } = {}) {
97
97
  }
98
98
  else if (version) {
99
99
  let cacheDir = getDepCacheName(name, version);
100
- nestedConfig = readConfig(getDepCacheDir(cacheDir) + "/mops.toml");
100
+ nestedConfig = readConfig(path.join(getDepCacheDir(cacheDir), "mops.toml"));
101
101
  }
102
102
  // collect nested deps
103
103
  if (nestedConfig) {
@@ -137,7 +137,7 @@ export async function resolvePackages({ conflicts = "ignore", } = {}) {
137
137
  if (majors.size > 1) {
138
138
  console.error(chalk.reset("") +
139
139
  chalk.redBright(conflicts === "error" ? "Error!" : "Warning!"), `Conflicting versions of dependency "${dep}"`);
140
- for (let { version, dependencyOf } of vers.reverse()) {
140
+ for (let { version, dependencyOf } of [...vers].reverse()) {
141
141
  console.error(chalk.reset(" ") +
142
142
  `${dep} ${chalk.bold.red(version.split(".")[0])}.${version.split(".").slice(1).join(".")} is dependency of ${chalk.bold(dependencyOf)}`);
143
143
  }
@@ -11,7 +11,7 @@ function cleanFixture(cwd, ...extras) {
11
11
  }
12
12
  }
13
13
  describe("build", () => {
14
- // Several dfx/pocket-ic builds per test; slow CI (e.g. node 20 matrix) can exceed 60s default.
14
+ // Several dfx/pocket-ic builds per test; slow CI can exceed 60s default.
15
15
  jest.setTimeout(120_000);
16
16
  test("ok", async () => {
17
17
  const cwd = path.join(import.meta.dirname, "build/success");
@@ -94,4 +94,28 @@ describe("check", () => {
94
94
  expect(result.stderr).toMatch(/error/i);
95
95
  expect(result.stdout).not.toMatch(/Stable compatibility/);
96
96
  });
97
+ test("lint runs after moc check and passes", async () => {
98
+ const cwd = path.join(import.meta.dirname, "check/with-lint-pass");
99
+ const result = await cli(["check"], { cwd });
100
+ expect(result.exitCode).toBe(0);
101
+ expect(result.stdout).toMatch(/✓ Lint succeeded/);
102
+ });
103
+ test("check fails when lint finds errors", async () => {
104
+ const cwd = path.join(import.meta.dirname, "check/with-lint-fail");
105
+ const result = await cli(["check"], { cwd });
106
+ expect(result.exitCode).toBe(1);
107
+ expect(result.stderr).toMatch(/no-bool-switch/);
108
+ });
109
+ test("lint is skipped when lintoko not configured and no rules exist", async () => {
110
+ const cwd = path.join(import.meta.dirname, "check/canisters");
111
+ const result = await cli(["check"], { cwd });
112
+ expect(result.exitCode).toBe(0);
113
+ expect(result.stdout).not.toMatch(/Lint/);
114
+ });
115
+ test("--fix flag reaches lint step", async () => {
116
+ const cwd = path.join(import.meta.dirname, "check/with-lint-pass");
117
+ const result = await cli(["check", "--fix"], { cwd });
118
+ expect(result.exitCode).toBe(0);
119
+ expect(result.stdout).toMatch(/✓ Lint fixes applied/);
120
+ });
97
121
  });
@@ -2,8 +2,15 @@ import { expect } from "@jest/globals";
2
2
  import { execa } from "execa";
3
3
  import { dirname } from "path";
4
4
  import { fileURLToPath } from "url";
5
+ // When MOPS_TEST_GLOBAL is set, invoke the globally-installed `mops` binary
6
+ // directly rather than the npm script. This exercises the real global-install
7
+ // code path where the binary lives outside the project tree.
8
+ const useGlobalBinary = Boolean(process.env.MOPS_TEST_GLOBAL);
5
9
  export const cli = async (args, { cwd } = {}) => {
6
- return await execa("npm", ["run", "--silent", "mops", "--", ...args], {
10
+ const [cmd, cmdArgs] = useGlobalBinary
11
+ ? ["mops", args]
12
+ : ["npm", ["run", "--silent", "mops", "--", ...args]];
13
+ return await execa(cmd, cmdArgs, {
7
14
  env: { ...process.env, ...(cwd != null && { MOPS_CWD: cwd }) },
8
15
  ...(cwd != null && { cwd }),
9
16
  stdio: "pipe",
@@ -1,6 +1,6 @@
1
- import { describe, test } from "@jest/globals";
1
+ import { describe, expect, test } from "@jest/globals";
2
2
  import path from "path";
3
- import { cliSnapshot } from "./helpers";
3
+ import { cli, cliSnapshot } from "./helpers";
4
4
  describe("lint", () => {
5
5
  test("ok", async () => {
6
6
  const cwd = path.join(import.meta.dirname, "lint");
@@ -12,4 +12,30 @@ describe("lint", () => {
12
12
  await cliSnapshot(["lint", "NoBoolSwitch", "--verbose"], { cwd }, 1);
13
13
  await cliSnapshot(["lint", "DoesNotExist"], { cwd }, 1);
14
14
  });
15
+ test("[lint] rules - additional config rules directory is used", async () => {
16
+ const cwd = path.join(import.meta.dirname, "lint-config-rules");
17
+ const result = await cli(["lint"], { cwd });
18
+ expect(result.exitCode).toBe(1);
19
+ expect(result.stderr).toMatch(/no-bool-switch/);
20
+ });
21
+ test("[lint] extends - picks up rules/ from named dependency", async () => {
22
+ const cwd = path.join(import.meta.dirname, "lint-extends");
23
+ const result = await cli(["lint"], { cwd });
24
+ expect(result.exitCode).toBe(1);
25
+ expect(result.stderr).toMatch(/no-bool-switch/);
26
+ });
27
+ test("[lint] extends true - picks up rules/ from all dependencies", async () => {
28
+ const cwd = path.join(import.meta.dirname, "lint-extends-all");
29
+ const result = await cli(["lint"], { cwd });
30
+ expect(result.exitCode).toBe(1);
31
+ expect(result.stderr).toMatch(/no-bool-switch/);
32
+ });
33
+ test("[lint] extends - dep not in extends list is ignored", async () => {
34
+ // my-pkg has rules/ but extends only lists "other-pkg" (which doesn't exist),
35
+ // so no dep rules are loaded and NoBoolSwitch.mo passes with exit 0.
36
+ const cwd = path.join(import.meta.dirname, "lint-extends-ignored");
37
+ const result = await cli(["lint"], { cwd });
38
+ expect(result.exitCode).toBe(0);
39
+ expect(result.stderr).toMatch(/not found in dependencies/);
40
+ });
15
41
  });
package/dist/types.d.ts CHANGED
@@ -29,6 +29,8 @@ export type Config = {
29
29
  };
30
30
  lint?: {
31
31
  args?: string[];
32
+ rules?: string[];
33
+ extends?: string[] | true;
32
34
  };
33
35
  };
34
36
  export type CanisterConfig = {
package/dist/vessel.d.ts CHANGED
@@ -18,4 +18,4 @@ export declare const installFromGithub: (name: string, repo: string, { verbose,
18
18
  dep?: boolean | undefined;
19
19
  silent?: boolean | undefined;
20
20
  ignoreTransitive?: boolean | undefined;
21
- }) => Promise<void>;
21
+ }) => Promise<boolean>;
package/dist/vessel.js CHANGED
@@ -141,7 +141,7 @@ export const installFromGithub = async (name, repo, { verbose = false, dep = fal
141
141
  }
142
142
  catch (err) {
143
143
  deleteSync([cacheDir], { force: true });
144
- process.exit(1);
144
+ return false;
145
145
  }
146
146
  }
147
147
  if (verbose) {
@@ -151,7 +151,7 @@ export const installFromGithub = async (name, repo, { verbose = false, dep = fal
151
151
  logUpdate.clear();
152
152
  }
153
153
  if (ignoreTransitive) {
154
- return;
154
+ return true;
155
155
  }
156
156
  const config = await readVesselConfig(cacheDir, { silent });
157
157
  if (config) {
@@ -161,4 +161,5 @@ export const installFromGithub = async (name, repo, { verbose = false, dep = fal
161
161
  }
162
162
  }
163
163
  }
164
+ return true;
164
165
  };
@@ -16,7 +16,9 @@ export function findChangelogEntry(changelog: string, version: string): string {
16
16
  }
17
17
  } else if (
18
18
  node.type === "heading" &&
19
- toMarkdown(node).match(new RegExp(`\\b${version}\\b`))
19
+ toMarkdown(node).match(
20
+ new RegExp(`\\b${version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`),
21
+ )
20
22
  ) {
21
23
  depth = node.depth;
22
24
  found = true;
package/integrity.ts CHANGED
@@ -87,7 +87,9 @@ export function getLocalFileHash(fileId: string): string {
87
87
  }
88
88
 
89
89
  function getMopsTomlHash(): string {
90
- return bytesToHex(sha256(fs.readFileSync(getRootDir() + "/mops.toml")));
90
+ return bytesToHex(
91
+ sha256(fs.readFileSync(path.join(getRootDir(), "mops.toml"))),
92
+ );
91
93
  }
92
94
 
93
95
  function getMopsTomlDepsHash(): string {
@@ -137,7 +139,14 @@ export function readLockFile(): LockFile | null {
137
139
  let rootDir = getRootDir();
138
140
  let lockFile = path.join(rootDir, "mops.lock");
139
141
  if (fs.existsSync(lockFile)) {
140
- return JSON.parse(fs.readFileSync(lockFile).toString()) as LockFile;
142
+ try {
143
+ return JSON.parse(fs.readFileSync(lockFile).toString()) as LockFile;
144
+ } catch {
145
+ console.error(
146
+ "mops.lock is corrupted. Delete it and run `mops install` to regenerate.",
147
+ );
148
+ process.exit(1);
149
+ }
141
150
  }
142
151
  return null;
143
152
  }
@@ -290,7 +299,7 @@ export async function checkLockFile(force = false) {
290
299
 
291
300
  for (let [fileId, lockedHash] of Object.entries(hashes)) {
292
301
  // check if file belongs to package
293
- if (!fileId.startsWith(packageId)) {
302
+ if (!fileId.startsWith(packageId + "/")) {
294
303
  console.error("Integrity check failed");
295
304
  console.error(
296
305
  `File ${fileId} in lock file does not belong to package ${packageId}`,
package/mops.ts CHANGED
@@ -152,6 +152,13 @@ export async function getGithubCommit(repo: string, ref: string): Promise<any> {
152
152
  res = await fetch(`https://api.github.com/repos/${repo}/commits/main`);
153
153
  json = await res.json();
154
154
  }
155
+
156
+ if (!res.ok || !json.sha) {
157
+ throw new Error(
158
+ `Failed to fetch commit for ${repo}#${ref}: ${json.message || `HTTP ${res.status}`}`,
159
+ );
160
+ }
161
+
155
162
  return json;
156
163
  }
157
164
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "dist/bin/mops.js",
@@ -55,7 +55,6 @@
55
55
  "@noble/hashes": "1.8.0",
56
56
  "as-table": "1.0.55",
57
57
  "buffer": "6.0.3",
58
- "cacheable-request": "12.0.1",
59
58
  "chalk": "5.4.1",
60
59
  "change-case": "5.4.4",
61
60
  "chokidar": "3.6.0",
@@ -77,7 +76,7 @@
77
76
  "markdown-table": "3.0.4",
78
77
  "mdast-util-from-markdown": "2.0.2",
79
78
  "mdast-util-to-markdown": "2.1.2",
80
- "minimatch": "10.0.1",
79
+ "minimatch": "10.2.4",
81
80
  "ncp": "2.0.0",
82
81
  "octokit": "3.1.2",
83
82
  "pem-file": "1.0.1",
@@ -90,7 +89,7 @@
90
89
  "semver": "7.7.1",
91
90
  "stream-to-promise": "3.0.0",
92
91
  "string-width": "7.2.0",
93
- "tar": "7.5.9",
92
+ "tar": "7.5.11",
94
93
  "terminal-size": "4.0.0",
95
94
  "vscode-languageserver-textdocument": "1.0.12"
96
95
  },
@@ -99,7 +98,6 @@
99
98
  "@types/debounce": "1.2.4",
100
99
  "@types/decompress": "4.2.7",
101
100
  "@types/fs-extra": "11.0.4",
102
- "@types/glob": "8.1.0",
103
101
  "@types/jsdom": "28.0.0",
104
102
  "@types/ncp": "2.0.8",
105
103
  "@types/node": "24.0.3",
package/release-cli.ts CHANGED
@@ -79,7 +79,7 @@ type Releases = {
79
79
  if (!fs.existsSync(path.resolve(__dirname, "../cli-releases/releases.json"))) {
80
80
  fs.writeFileSync(
81
81
  path.resolve(__dirname, "../cli-releases/releases.json"),
82
- JSON.stringify({ tags: {}, versions: {} }, null, 2),
82
+ JSON.stringify({ tags: {}, versions: {} }, null, 2) + "\n",
83
83
  );
84
84
  }
85
85
 
@@ -102,5 +102,5 @@ releases.versions[version] = {
102
102
 
103
103
  fs.writeFileSync(
104
104
  path.resolve(__dirname, "../cli-releases/releases.json"),
105
- JSON.stringify(releases, null, 2),
105
+ JSON.stringify(releases, null, 2) + "\n",
106
106
  );
@@ -41,12 +41,12 @@ export async function resolvePackages({
41
41
  > = {};
42
42
 
43
43
  let compareVersions = (a: string = "0.0.0", b: string = "0.0.0") => {
44
- let ap = a.split(".").map((x: string) => parseInt(x)) as [
44
+ let ap = a.split(".").map((x: string) => parseInt(x) || 0) as [
45
45
  number,
46
46
  number,
47
47
  number,
48
48
  ];
49
- let bp = b.split(".").map((x: string) => parseInt(x)) as [
49
+ let bp = b.split(".").map((x: string) => parseInt(x) || 0) as [
50
50
  number,
51
51
  number,
52
52
  number,
@@ -138,7 +138,9 @@ export async function resolvePackages({
138
138
  }
139
139
  } else if (version) {
140
140
  let cacheDir = getDepCacheName(name, version);
141
- nestedConfig = readConfig(getDepCacheDir(cacheDir) + "/mops.toml");
141
+ nestedConfig = readConfig(
142
+ path.join(getDepCacheDir(cacheDir), "mops.toml"),
143
+ );
142
144
  }
143
145
 
144
146
  // collect nested deps
@@ -193,7 +195,7 @@ export async function resolvePackages({
193
195
  `Conflicting versions of dependency "${dep}"`,
194
196
  );
195
197
 
196
- for (let { version, dependencyOf } of vers.reverse()) {
198
+ for (let { version, dependencyOf } of [...vers].reverse()) {
197
199
  console.error(
198
200
  chalk.reset(" ") +
199
201
  `${dep} ${chalk.bold.red(version.split(".")[0])}.${version.split(".").slice(1).join(".")} is dependency of ${chalk.bold(dependencyOf)}`,
@@ -14,7 +14,7 @@ function cleanFixture(cwd: string, ...extras: string[]) {
14
14
  }
15
15
 
16
16
  describe("build", () => {
17
- // Several dfx/pocket-ic builds per test; slow CI (e.g. node 20 matrix) can exceed 60s default.
17
+ // Several dfx/pocket-ic builds per test; slow CI can exceed 60s default.
18
18
  jest.setTimeout(120_000);
19
19
 
20
20
  test("ok", async () => {
@@ -0,0 +1,8 @@
1
+ module {
2
+ public func boolSwitch(b : Bool) : Bool {
3
+ switch (b) {
4
+ case false { false };
5
+ case true { true };
6
+ };
7
+ };
8
+ };
@@ -0,0 +1,9 @@
1
+ name = "no-bool-switch"
2
+ description = "Don't switch on boolean values, use if instead"
3
+ query = """
4
+ (switch_exp
5
+ (case [
6
+ (lit_pat (bool_literal))
7
+ (tup_pat . (lit_pat (bool_literal)) @trailing)
8
+ ])) @error
9
+ """
@@ -0,0 +1,9 @@
1
+ [toolchain]
2
+ moc = "1.3.0"
3
+ lintoko = "0.7.0"
4
+
5
+ [moc]
6
+ args = ["--default-persistent-actors"]
7
+
8
+ [canisters.backend]
9
+ main = "NoBoolSwitch.mo"
@@ -0,0 +1,5 @@
1
+ module {
2
+ public func greet(name : Text) : Text {
3
+ "Hello, " # name # "!";
4
+ };
5
+ };
@@ -0,0 +1,9 @@
1
+ name = "no-bool-switch"
2
+ description = "Don't switch on boolean values, use if instead"
3
+ query = """
4
+ (switch_exp
5
+ (case [
6
+ (lit_pat (bool_literal))
7
+ (tup_pat . (lit_pat (bool_literal)) @trailing)
8
+ ])) @error
9
+ """
@@ -0,0 +1,9 @@
1
+ [toolchain]
2
+ moc = "1.3.0"
3
+ lintoko = "0.7.0"
4
+
5
+ [moc]
6
+ args = ["--default-persistent-actors"]
7
+
8
+ [canisters.backend]
9
+ main = "Ok.mo"
@@ -121,4 +121,32 @@ describe("check", () => {
121
121
  expect(result.stderr).toMatch(/error/i);
122
122
  expect(result.stdout).not.toMatch(/Stable compatibility/);
123
123
  });
124
+
125
+ test("lint runs after moc check and passes", async () => {
126
+ const cwd = path.join(import.meta.dirname, "check/with-lint-pass");
127
+ const result = await cli(["check"], { cwd });
128
+ expect(result.exitCode).toBe(0);
129
+ expect(result.stdout).toMatch(/✓ Lint succeeded/);
130
+ });
131
+
132
+ test("check fails when lint finds errors", async () => {
133
+ const cwd = path.join(import.meta.dirname, "check/with-lint-fail");
134
+ const result = await cli(["check"], { cwd });
135
+ expect(result.exitCode).toBe(1);
136
+ expect(result.stderr).toMatch(/no-bool-switch/);
137
+ });
138
+
139
+ test("lint is skipped when lintoko not configured and no rules exist", async () => {
140
+ const cwd = path.join(import.meta.dirname, "check/canisters");
141
+ const result = await cli(["check"], { cwd });
142
+ expect(result.exitCode).toBe(0);
143
+ expect(result.stdout).not.toMatch(/Lint/);
144
+ });
145
+
146
+ test("--fix flag reaches lint step", async () => {
147
+ const cwd = path.join(import.meta.dirname, "check/with-lint-pass");
148
+ const result = await cli(["check", "--fix"], { cwd });
149
+ expect(result.exitCode).toBe(0);
150
+ expect(result.stdout).toMatch(/✓ Lint fixes applied/);
151
+ });
124
152
  });
package/tests/helpers.ts CHANGED
@@ -7,8 +7,16 @@ export interface CliOptions {
7
7
  cwd?: string;
8
8
  }
9
9
 
10
+ // When MOPS_TEST_GLOBAL is set, invoke the globally-installed `mops` binary
11
+ // directly rather than the npm script. This exercises the real global-install
12
+ // code path where the binary lives outside the project tree.
13
+ const useGlobalBinary = Boolean(process.env.MOPS_TEST_GLOBAL);
14
+
10
15
  export const cli = async (args: string[], { cwd }: CliOptions = {}) => {
11
- return await execa("npm", ["run", "--silent", "mops", "--", ...args], {
16
+ const [cmd, cmdArgs] = useGlobalBinary
17
+ ? ["mops", args]
18
+ : ["npm", ["run", "--silent", "mops", "--", ...args]];
19
+ return await execa(cmd, cmdArgs, {
12
20
  env: { ...process.env, ...(cwd != null && { MOPS_CWD: cwd }) },
13
21
  ...(cwd != null && { cwd }),
14
22
  stdio: "pipe",
@@ -0,0 +1,9 @@
1
+ name = "no-bool-switch"
2
+ description = "Don't switch on boolean values, use if instead"
3
+ query = """
4
+ (switch_exp
5
+ (case [
6
+ (lit_pat (bool_literal))
7
+ (tup_pat . (lit_pat (bool_literal)) @trailing)
8
+ ])) @error
9
+ """
@@ -0,0 +1,5 @@
1
+ [toolchain]
2
+ lintoko = "0.7.0"
3
+
4
+ [lint]
5
+ rules = ["extra-rules"]
@@ -0,0 +1,8 @@
1
+ module {
2
+ public func boolSwitch(b : Bool) : Bool {
3
+ switch (b) {
4
+ case false { false };
5
+ case true { true };
6
+ };
7
+ };
8
+ };
@@ -0,0 +1,8 @@
1
+ [dependencies]
2
+ my-pkg = "./my-pkg"
3
+
4
+ [toolchain]
5
+ lintoko = "0.7.0"
6
+
7
+ [lint]
8
+ extends = ["my-pkg"]
@@ -0,0 +1,3 @@
1
+ [package]
2
+ name = "my-pkg"
3
+ version = "0.1.0"
@@ -0,0 +1,9 @@
1
+ name = "no-bool-switch"
2
+ description = "Don't switch on boolean values, use if instead"
3
+ query = """
4
+ (switch_exp
5
+ (case [
6
+ (lit_pat (bool_literal))
7
+ (tup_pat . (lit_pat (bool_literal)) @trailing)
8
+ ])) @error
9
+ """