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
@@ -34,30 +34,48 @@ describe("check", () => {
34
34
  const cwd = path.join(import.meta.dirname, "check/moc-args");
35
35
  await cliSnapshot(["check", "Warning.mo"], { cwd }, 1);
36
36
  });
37
- test("no args falls back to [canisters] entrypoints", async () => {
37
+ test("no args checks all canisters", async () => {
38
38
  const cwd = path.join(import.meta.dirname, "check/canisters");
39
39
  await cliSnapshot(["check"], { cwd }, 0);
40
40
  });
41
- test("canister entrypoint resolved relative to config root when run from subdirectory", async () => {
41
+ test("canister name filters to specific canister", async () => {
42
+ const cwd = path.join(import.meta.dirname, "check/canisters");
43
+ const result = await cli(["check", "backend"], { cwd });
44
+ expect(result.exitCode).toBe(0);
45
+ expect(result.stdout).toMatch(/✓ backend/);
46
+ });
47
+ test("canister resolved relative to config root when run from subdirectory", async () => {
42
48
  const fixtureRoot = path.join(import.meta.dirname, "check/canisters-subdir");
43
49
  const subdir = path.join(fixtureRoot, "src/backend");
44
50
  const result = await cli(["check"], { cwd: subdir });
45
51
  expect(result.exitCode).toBe(0);
46
52
  expect(result.stdout).toMatch(/✓/);
47
53
  });
48
- test("[moc] args applied when using canister fallback", async () => {
54
+ test("[moc] args applied to canister check", async () => {
49
55
  const cwd = path.join(import.meta.dirname, "check/canisters-moc-args");
50
56
  const result = await cli(["check"], { cwd });
51
57
  expect(result.exitCode).toBe(1);
52
58
  expect(result.stderr).toMatch(/warning \[M0194\]/);
53
59
  });
54
- test("canister entrypoint with errors", async () => {
60
+ test("[canisters.X].args applied to canister check", async () => {
61
+ const cwd = path.join(import.meta.dirname, "check/canisters-canister-args");
62
+ const result = await cli(["check"], { cwd });
63
+ expect(result.exitCode).toBe(1);
64
+ expect(result.stderr).toMatch(/warning \[M0194\]/);
65
+ });
66
+ test("canister with errors", async () => {
55
67
  const cwd = path.join(import.meta.dirname, "check/canisters-error");
56
68
  const result = await cli(["check"], { cwd });
57
69
  expect(result.exitCode).toBe(1);
58
70
  expect(result.stderr).toMatch(/error/i);
59
71
  });
60
- test("--fix with canister fallback", async () => {
72
+ test("invalid canister name errors", async () => {
73
+ const cwd = path.join(import.meta.dirname, "check/canisters");
74
+ const result = await cli(["check", "nonexistent"], { cwd });
75
+ expect(result.exitCode).toBe(1);
76
+ expect(result.stderr).toMatch(/not found in mops\.toml/);
77
+ });
78
+ test("--fix with canister", async () => {
61
79
  const cwd = path.join(import.meta.dirname, "check/canisters");
62
80
  const result = await cli(["check", "--fix"], { cwd });
63
81
  expect(result.exitCode).toBe(0);
@@ -38,4 +38,37 @@ describe("lint", () => {
38
38
  expect(result.exitCode).toBe(0);
39
39
  expect(result.stderr).toMatch(/not found in dependencies/);
40
40
  });
41
+ describe("[lint.extra]", () => {
42
+ test("extra rules on glob-matched files", async () => {
43
+ // src/restricted/*.mo has violations, Ok.mo does not.
44
+ // Extra rules apply only to the glob match → fails on restricted/ files.
45
+ // Filter "Ok" narrows scope so extra is skipped → passes.
46
+ const cwd = path.join(import.meta.dirname, "lint-extra");
47
+ await cliSnapshot(["lint"], { cwd }, 1);
48
+ await cliSnapshot(["lint", "Ok"], { cwd }, 0);
49
+ });
50
+ test("edge cases: pass, empty value, no-match, missing dir", async () => {
51
+ // Single fixture with 4 entries processed in order:
52
+ // 1. Clean.mo + valid rules → passes
53
+ // 2. empty array → warns and skips
54
+ // 3. non-matching glob → warns and skips
55
+ // 4. missing rule dir → errors
56
+ const cwd = path.join(import.meta.dirname, "lint-extra-edge-cases");
57
+ await cliSnapshot(["lint"], { cwd }, 1);
58
+ });
59
+ test("base rules still run alongside extra rules", async () => {
60
+ const cwd = path.join(import.meta.dirname, "lint-extra-with-base");
61
+ await cliSnapshot(["lint"], { cwd }, 1);
62
+ });
63
+ test("--rules CLI flag does not affect extra runs, multi-rules", async () => {
64
+ // --rules overrides base with an empty dir (no base violations).
65
+ // Extra runs independently with two rule dirs → Restricted.mo fails.
66
+ const cwd = path.join(import.meta.dirname, "lint-extra-with-cli-rules");
67
+ await cliSnapshot(["lint", "--rules", "empty-rules", "--verbose"], { cwd }, 1);
68
+ });
69
+ test("example rules: no-types, types-only, migration-only", async () => {
70
+ const cwd = path.join(import.meta.dirname, "lint-extra-example-rules");
71
+ await cliSnapshot(["lint"], { cwd }, 1);
72
+ });
73
+ });
41
74
  });
package/dist/types.d.ts CHANGED
@@ -31,6 +31,7 @@ export type Config = {
31
31
  args?: string[];
32
32
  rules?: string[];
33
33
  extends?: string[] | true;
34
+ extra?: Record<string, string[]>;
34
35
  };
35
36
  };
36
37
  export type CanisterConfig = {
Binary file
Binary file
@@ -14,11 +14,22 @@ export function resolveCanisterConfigs(
14
14
  );
15
15
  }
16
16
 
17
- export function resolveCanisterEntrypoints(config: Config): string[] {
18
- const canisters = resolveCanisterConfigs(config);
19
- return Object.values(canisters)
20
- .map((c) => c.main)
21
- .filter((main): main is string => Boolean(main));
17
+ export function filterCanisters(
18
+ canisters: Record<string, CanisterConfig>,
19
+ names?: string[],
20
+ ): Record<string, CanisterConfig> {
21
+ if (!names) {
22
+ return canisters;
23
+ }
24
+ const invalidNames = names.filter((name) => !(name in canisters));
25
+ if (invalidNames.length) {
26
+ cliError(
27
+ `Canister(s) not found in mops.toml: ${invalidNames.join(", ")}. Available: ${Object.keys(canisters).join(", ")}`,
28
+ );
29
+ }
30
+ return Object.fromEntries(
31
+ Object.entries(canisters).filter(([name]) => names.includes(name)),
32
+ );
22
33
  }
23
34
 
24
35
  export function resolveSingleCanister(
@@ -50,3 +61,23 @@ export function resolveSingleCanister(
50
61
 
51
62
  return { name: names[0]!, canister: canisters[names[0]!]! };
52
63
  }
64
+
65
+ export function looksLikeFile(arg: string): boolean {
66
+ return (
67
+ arg.endsWith(".mo") ||
68
+ arg.endsWith(".most") ||
69
+ arg.includes("/") ||
70
+ arg.includes("\\")
71
+ );
72
+ }
73
+
74
+ export function validateCanisterArgs(
75
+ canister: CanisterConfig,
76
+ canisterName: string,
77
+ ): void {
78
+ if (canister.args && typeof canister.args === "string") {
79
+ cliError(
80
+ `Canister config 'args' should be an array of strings for canister ${canisterName}`,
81
+ );
82
+ }
83
+ }
package/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": "dist/bin/mops.js",
@@ -86,6 +86,7 @@
86
86
  "prettier-plugin-motoko": "0.13.0",
87
87
  "promisify-child-process": "4.1.2",
88
88
  "prompts": "2.4.2",
89
+ "proper-lockfile": "4.1.2",
89
90
  "semver": "7.7.1",
90
91
  "stream-to-promise": "3.0.0",
91
92
  "string-width": "7.2.0",
@@ -102,6 +103,7 @@
102
103
  "@types/ncp": "2.0.8",
103
104
  "@types/node": "24.0.3",
104
105
  "@types/prompts": "2.4.9",
106
+ "@types/proper-lockfile": "4.1.4",
105
107
  "@types/semver": "7.5.8",
106
108
  "@types/stream-to-promise": "2.2.4",
107
109
  "@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
 
@@ -32,11 +32,11 @@ exports[`check error 2`] = `
32
32
  }
33
33
  `;
34
34
 
35
- exports[`check no args falls back to [canisters] entrypoints 1`] = `
35
+ exports[`check no args checks all canisters 1`] = `
36
36
  {
37
37
  "exitCode": 0,
38
38
  "stderr": "",
39
- "stdout": "✓ Ok.mo",
39
+ "stdout": "✓ backend",
40
40
  }
41
41
  `;
42
42
 
@@ -1,5 +1,163 @@
1
1
  // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2
2
 
3
+ exports[`lint [lint.extra] --rules CLI flag does not affect extra runs, multi-rules 1`] = `
4
+ {
5
+ "exitCode": 1,
6
+ "stderr": " × [ERROR]: no-bool-switch
7
+ ╭─[<TEST_DIR>/lint-extra-with-cli-rules/src/Restricted.mo:3:5]
8
+ 2 │ public func boolSwitch(b : Bool) : Bool {
9
+ 3 │ ╭─▶ switch (b) {
10
+ 4 │ │ case false { false };
11
+ 5 │ │ case true { true };
12
+ 6 │ ├─▶ };
13
+ · ╰──── Don't switch on boolean values, use if instead
14
+ 7 │ };
15
+ ╰────
16
+
17
+ × [ERROR]: no-bool-switch-2
18
+ ╭─[<TEST_DIR>/lint-extra-with-cli-rules/src/Restricted.mo:3:5]
19
+ 2 │ public func boolSwitch(b : Bool) : Bool {
20
+ 3 │ ╭─▶ switch (b) {
21
+ 4 │ │ case false { false };
22
+ 5 │ │ case true { true };
23
+ 6 │ ├─▶ };
24
+ · ╰──── Duplicate detection: don't switch on boolean values
25
+ 7 │ };
26
+ ╰────
27
+
28
+ Error: Found 2 errors
29
+ Lint failed",
30
+ "stdout": "lint Running lintoko (base):
31
+ <CACHE>/lintoko/0.7.0/lintoko
32
+ ["--verbose","--rules","empty-rules","<TEST_DIR>/lint-extra-with-cli-rules/src/Restricted.mo","<TEST_DIR>/lint-extra-with-cli-rules/src/Ok.mo"]
33
+ DEBUG file input: <TEST_DIR>/lint-extra-with-cli-rules/src/Restricted.mo
34
+ DEBUG file input: <TEST_DIR>/lint-extra-with-cli-rules/src/Ok.mo
35
+ DEBUG Loading rules from: empty-rules
36
+ DEBUG Linting file: <TEST_DIR>/lint-extra-with-cli-rules/src/Ok.mo
37
+ DEBUG Linting file: <TEST_DIR>/lint-extra-with-cli-rules/src/Restricted.mo
38
+ lint Running lintoko (extra: src/Restricted.mo):
39
+ <CACHE>/lintoko/0.7.0/lintoko
40
+ ["--verbose","--rules","../lint/lints","--rules","rules-b","<TEST_DIR>/lint-extra-with-cli-rules/src/Restricted.mo"]
41
+ DEBUG file input: <TEST_DIR>/lint-extra-with-cli-rules/src/Restricted.mo
42
+ DEBUG Loading rules from: ../lint/lints
43
+ DEBUG Parsing extra rule at: ../lint/lints/no-bool-switch.toml
44
+ DEBUG Loading rules from: rules-b
45
+ DEBUG Parsing extra rule at: rules-b/no-bool-switch-2.toml
46
+ DEBUG Linting file: <TEST_DIR>/lint-extra-with-cli-rules/src/Restricted.mo",
47
+ }
48
+ `;
49
+
50
+ exports[`lint [lint.extra] base rules still run alongside extra rules 1`] = `
51
+ {
52
+ "exitCode": 1,
53
+ "stderr": " × [ERROR]: no-bool-switch
54
+ ╭─[<TEST_DIR>/lint-extra-with-base/src/BadBase.mo:3:5]
55
+ 2 │ public func boolSwitch(b : Bool) : Bool {
56
+ 3 │ ╭─▶ switch (b) {
57
+ 4 │ │ case false { false };
58
+ 5 │ │ case true { true };
59
+ 6 │ ├─▶ };
60
+ · ╰──── Don't switch on boolean values, use if instead
61
+ 7 │ };
62
+ ╰────
63
+
64
+ Error: Found 1 errors
65
+ Lint failed",
66
+ "stdout": "",
67
+ }
68
+ `;
69
+
70
+ exports[`lint [lint.extra] edge cases: pass, empty value, no-match, missing dir 1`] = `
71
+ {
72
+ "exitCode": 1,
73
+ "stderr": "[lint.extra] skipping 'src/also-missing/*.mo': value must be a non-empty array of rule directories
74
+ [lint.extra] no files matched glob 'src/nonexistent/*.mo', skipping
75
+ [lint.extra] rule directory 'nonexistent-rules' not found (referenced by glob 'src/*.mo')",
76
+ "stdout": "",
77
+ }
78
+ `;
79
+
80
+ exports[`lint [lint.extra] example rules: no-types, types-only, migration-only 1`] = `
81
+ {
82
+ "exitCode": 1,
83
+ "stderr": " × [ERROR]: no-types
84
+ ╭─[<TEST_DIR>/lint-extra-example-rules/src/Main.mo:6:10]
85
+ 5 │
86
+ 6 │ ╭─▶ public type User = {
87
+ 7 │ │ name : Text;
88
+ 8 │ │ age : Nat;
89
+ 9 │ ├─▶ };
90
+ · ╰──── File must not contain type declarations. Move types to a separate Types module.
91
+ 10 │ };
92
+ ╰────
93
+
94
+ Error: Found 1 errors
95
+ × [ERROR]: types-only
96
+ ╭─[<TEST_DIR>/lint-extra-example-rules/src/Types.mo:7:10]
97
+ 6 │
98
+ 7 │ ╭─▶ public func helper() : Text {
99
+ 8 │ │ "oops";
100
+ 9 │ ├─▶ };
101
+ · ╰──── File must contain only type declarations. No functions, classes, or variable bindings.
102
+ 10 │ };
103
+ ╰────
104
+
105
+ Error: Found 1 errors
106
+ × [ERROR]: migration-only
107
+ ╭─[<TEST_DIR>/lint-extra-example-rules/src/Migration.mo:6:10]
108
+ 5 │
109
+ 6 │ ╭─▶ public func notAllowed(old : {}) : {} {
110
+ 7 │ │ {};
111
+ 8 │ ├─▶ };
112
+ · ╰──── Only migration() may be a public function.
113
+ 9 │ };
114
+ ╰────
115
+
116
+ Error: Found 1 errors
117
+ Lint failed",
118
+ "stdout": "",
119
+ }
120
+ `;
121
+
122
+ exports[`lint [lint.extra] extra rules on glob-matched files 1`] = `
123
+ {
124
+ "exitCode": 1,
125
+ "stderr": " × [ERROR]: no-bool-switch
126
+ ╭─[<TEST_DIR>/lint-extra/src/restricted/B.mo:3:5]
127
+ 2 │ public func anotherBoolSwitch(b : Bool) : Bool {
128
+ 3 │ ╭─▶ switch (b) {
129
+ 4 │ │ case true { false };
130
+ 5 │ │ case false { true };
131
+ 6 │ ├─▶ };
132
+ · ╰──── Don't switch on boolean values, use if instead
133
+ 7 │ };
134
+ ╰────
135
+
136
+ × [ERROR]: no-bool-switch
137
+ ╭─[<TEST_DIR>/lint-extra/src/restricted/Restricted.mo:3:5]
138
+ 2 │ public func boolSwitch(b : Bool) : Bool {
139
+ 3 │ ╭─▶ switch (b) {
140
+ 4 │ │ case false { false };
141
+ 5 │ │ case true { true };
142
+ 6 │ ├─▶ };
143
+ · ╰──── Don't switch on boolean values, use if instead
144
+ 7 │ };
145
+ ╰────
146
+
147
+ Error: Found 2 errors
148
+ Lint failed",
149
+ "stdout": "",
150
+ }
151
+ `;
152
+
153
+ exports[`lint [lint.extra] extra rules on glob-matched files 2`] = `
154
+ {
155
+ "exitCode": 0,
156
+ "stderr": "[lint.extra] no files matched glob 'src/restricted/*.mo', skipping",
157
+ "stdout": "✓ Lint succeeded",
158
+ }
159
+ `;
160
+
3
161
  exports[`lint error 1`] = `
4
162
  {
5
163
  "exitCode": 1,
@@ -15,8 +173,8 @@ exports[`lint error 1`] = `
15
173
  ╰────
16
174
 
17
175
  Error: Found 1 errors
18
- Lint failed with exit code 1",
19
- "stdout": "lint Running lintoko:
176
+ Lint failed",
177
+ "stdout": "lint Running lintoko (base):
20
178
  <CACHE>/lintoko/0.7.0/lintoko
21
179
  ["--verbose","--rules","lints","<TEST_DIR>/lint/src/Ok.mo","<TEST_DIR>/lint/src/NoBoolSwitch.mo"]
22
180
  DEBUG file input: <TEST_DIR>/lint/src/Ok.mo
@@ -43,8 +201,8 @@ exports[`lint error 2`] = `
43
201
  ╰────
44
202
 
45
203
  Error: Found 1 errors
46
- Lint failed with exit code 1",
47
- "stdout": "lint Running lintoko:
204
+ Lint failed",
205
+ "stdout": "lint Running lintoko (base):
48
206
  <CACHE>/lintoko/0.7.0/lintoko
49
207
  ["--verbose","--rules","lints","<TEST_DIR>/lint/src/NoBoolSwitch.mo"]
50
208
  DEBUG file input: <TEST_DIR>/lint/src/NoBoolSwitch.mo
@@ -66,7 +224,7 @@ exports[`lint ok 1`] = `
66
224
  {
67
225
  "exitCode": 0,
68
226
  "stderr": "",
69
- "stdout": "lint Running lintoko:
227
+ "stdout": "lint Running lintoko (base):
70
228
  <CACHE>/lintoko/0.7.0/lintoko
71
229
  ["--verbose","--rules","lints","<TEST_DIR>/lint/src/Ok.mo"]
72
230
  DEBUG file input: <TEST_DIR>/lint/src/Ok.mo
@@ -100,6 +100,23 @@ describe("build", () => {
100
100
  }
101
101
  });
102
102
 
103
+ test("parallel builds of the same canister both succeed", async () => {
104
+ const cwd = path.join(import.meta.dirname, "build/success");
105
+ try {
106
+ const [a, b] = await Promise.all([
107
+ cli(["build", "foo"], { cwd }),
108
+ cli(["build", "foo"], { cwd }),
109
+ ]);
110
+ expect(a.exitCode).toBe(0);
111
+ expect(b.exitCode).toBe(0);
112
+ expect(existsSync(path.join(cwd, ".mops/.build/foo.wasm"))).toBe(true);
113
+ expect(existsSync(path.join(cwd, ".mops/.build/foo.did"))).toBe(true);
114
+ expect(existsSync(path.join(cwd, ".mops/.build/foo.most"))).toBe(true);
115
+ } finally {
116
+ cleanFixture(cwd);
117
+ }
118
+ });
119
+
103
120
  // Regression: bin/mops.js must route through environments/nodejs/cli.js
104
121
  // so that setWasmBindings() is called before any command runs.
105
122
  // The dev entry point (npm run mops) uses tsx and always worked;
@@ -0,0 +1,5 @@
1
+ actor {
2
+ public func example() : async () {
3
+ let unused = 123;
4
+ };
5
+ };
@@ -0,0 +1,9 @@
1
+ [toolchain]
2
+ moc = "1.3.0"
3
+
4
+ [moc]
5
+ args = ["--default-persistent-actors"]
6
+
7
+ [canisters.backend]
8
+ main = "Warning.mo"
9
+ args = ["-Werror"]
@@ -0,0 +1,8 @@
1
+ module {
2
+ public func migration(_ : {}) : { a : Nat; b : Text } {
3
+ {
4
+ a = 42;
5
+ b = "hello";
6
+ };
7
+ };
8
+ };
@@ -0,0 +1,9 @@
1
+ module {
2
+ public func migration(old : { a : Nat; b : Text }) : {
3
+ a : Nat;
4
+ b : Text;
5
+ c : Bool;
6
+ } {
7
+ { old with c = true };
8
+ };
9
+ };
@@ -0,0 +1,9 @@
1
+ [toolchain]
2
+ moc = "1.5.0"
3
+
4
+ [moc]
5
+ args = ["--default-persistent-actors"]
6
+
7
+ [canisters.backend]
8
+ main = "src/main.mo"
9
+ args = ["--enhanced-migration=migrations"]
@@ -0,0 +1,8 @@
1
+ // Version: 4.0.0
2
+ {
3
+ "20250101_000000_Init" : {} -> {a : Nat; b : Text}
4
+ }
5
+ actor {
6
+ stable a : Nat;
7
+ stable b : Text
8
+ };
@@ -0,0 +1,11 @@
1
+ import Prim "mo:prim";
2
+
3
+ actor {
4
+ let a : Nat;
5
+ let b : Text;
6
+ let c : Bool;
7
+
8
+ public func check() : async () {
9
+ Prim.debugPrint(debug_show { a; b; c });
10
+ };
11
+ };
@@ -59,6 +59,27 @@ describe("check-stable", () => {
59
59
  expect(existsSync(path.join(cwd, "new.wasm"))).toBe(false);
60
60
  });
61
61
 
62
+ test("[canisters.X].args are passed to moc (enhanced migration)", async () => {
63
+ const cwd = path.join(import.meta.dirname, "check-stable/canister-args");
64
+ const result = await cli(["check-stable", "old.most"], { cwd });
65
+ expect(result.exitCode).toBe(0);
66
+ expect(result.stdout).toMatch(/Stable compatibility check passed/);
67
+ });
68
+
69
+ test("no args checks all canisters with [check-stable] config", async () => {
70
+ const cwd = path.join(import.meta.dirname, "check/deployed-compatible");
71
+ const result = await cli(["check-stable"], { cwd });
72
+ expect(result.exitCode).toBe(0);
73
+ expect(result.stdout).toMatch(/Stable compatibility check passed/);
74
+ });
75
+
76
+ test("canister name filters to specific canister", async () => {
77
+ const cwd = path.join(import.meta.dirname, "check/deployed-compatible");
78
+ const result = await cli(["check-stable", "backend"], { cwd });
79
+ expect(result.exitCode).toBe(0);
80
+ expect(result.stdout).toMatch(/Stable compatibility check passed/);
81
+ });
82
+
62
83
  test("errors when old file does not exist", async () => {
63
84
  const cwd = path.join(import.meta.dirname, "check-stable/compatible");
64
85
  const result = await cli(["check-stable", "nonexistent.mo"], { cwd });
@@ -49,12 +49,19 @@ describe("check", () => {
49
49
  await cliSnapshot(["check", "Warning.mo"], { cwd }, 1);
50
50
  });
51
51
 
52
- test("no args falls back to [canisters] entrypoints", async () => {
52
+ test("no args checks all canisters", async () => {
53
53
  const cwd = path.join(import.meta.dirname, "check/canisters");
54
54
  await cliSnapshot(["check"], { cwd }, 0);
55
55
  });
56
56
 
57
- test("canister entrypoint resolved relative to config root when run from subdirectory", async () => {
57
+ test("canister name filters to specific canister", async () => {
58
+ const cwd = path.join(import.meta.dirname, "check/canisters");
59
+ const result = await cli(["check", "backend"], { cwd });
60
+ expect(result.exitCode).toBe(0);
61
+ expect(result.stdout).toMatch(/✓ backend/);
62
+ });
63
+
64
+ test("canister resolved relative to config root when run from subdirectory", async () => {
58
65
  const fixtureRoot = path.join(
59
66
  import.meta.dirname,
60
67
  "check/canisters-subdir",
@@ -65,21 +72,35 @@ describe("check", () => {
65
72
  expect(result.stdout).toMatch(/✓/);
66
73
  });
67
74
 
68
- test("[moc] args applied when using canister fallback", async () => {
75
+ test("[moc] args applied to canister check", async () => {
69
76
  const cwd = path.join(import.meta.dirname, "check/canisters-moc-args");
70
77
  const result = await cli(["check"], { cwd });
71
78
  expect(result.exitCode).toBe(1);
72
79
  expect(result.stderr).toMatch(/warning \[M0194\]/);
73
80
  });
74
81
 
75
- test("canister entrypoint with errors", async () => {
82
+ test("[canisters.X].args applied to canister check", async () => {
83
+ const cwd = path.join(import.meta.dirname, "check/canisters-canister-args");
84
+ const result = await cli(["check"], { cwd });
85
+ expect(result.exitCode).toBe(1);
86
+ expect(result.stderr).toMatch(/warning \[M0194\]/);
87
+ });
88
+
89
+ test("canister with errors", async () => {
76
90
  const cwd = path.join(import.meta.dirname, "check/canisters-error");
77
91
  const result = await cli(["check"], { cwd });
78
92
  expect(result.exitCode).toBe(1);
79
93
  expect(result.stderr).toMatch(/error/i);
80
94
  });
81
95
 
82
- test("--fix with canister fallback", async () => {
96
+ test("invalid canister name errors", async () => {
97
+ const cwd = path.join(import.meta.dirname, "check/canisters");
98
+ const result = await cli(["check", "nonexistent"], { cwd });
99
+ expect(result.exitCode).toBe(1);
100
+ expect(result.stderr).toMatch(/not found in mops\.toml/);
101
+ });
102
+
103
+ test("--fix with canister", async () => {
83
104
  const cwd = path.join(import.meta.dirname, "check/canisters");
84
105
  const result = await cli(["check", "--fix"], { cwd });
85
106
  expect(result.exitCode).toBe(0);
@@ -0,0 +1,5 @@
1
+ [toolchain]
2
+ lintoko = "0.7.0"
3
+
4
+ [lint.extra]
5
+ "src/restricted/*.mo" = ["../lint/lints"]
@@ -0,0 +1,5 @@
1
+ module {
2
+ public func greet(name : Text) : Text {
3
+ "Hello, " # name # "!";
4
+ };
5
+ };
@@ -0,0 +1,8 @@
1
+ module {
2
+ public func anotherBoolSwitch(b : Bool) : Bool {
3
+ switch (b) {
4
+ case true { false };
5
+ case false { true };
6
+ };
7
+ };
8
+ };
@@ -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
+ [toolchain]
2
+ lintoko = "0.7.0"
3
+
4
+ [lint.extra]
5
+ "src/Clean.mo" = ["../lint/lints"]
6
+ "src/also-missing/*.mo" = []
7
+ "src/nonexistent/*.mo" = ["../lint/lints"]
8
+ "src/*.mo" = ["nonexistent-rules"]
@@ -0,0 +1,5 @@
1
+ module {
2
+ public func greet(name : Text) : Text {
3
+ "Hello, " # name # "!";
4
+ };
5
+ };
@@ -0,0 +1,9 @@
1
+ name = "migration-only"
2
+ description = "Only migration() may be a public function."
3
+ query = """
4
+ (dec_field
5
+ "public"
6
+ (func_dec
7
+ (identifier) @name
8
+ (#not-eq? @name "migration")) @error)
9
+ """