ic-mops 2.2.1 → 2.3.1

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 (151) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/RELEASE.md +9 -1
  3. package/cli.ts +25 -2
  4. package/commands/build.ts +2 -10
  5. package/commands/check-stable.ts +177 -0
  6. package/commands/check.ts +53 -6
  7. package/dist/bin/mops.js +1 -1
  8. package/dist/cli.js +22 -2
  9. package/dist/commands/build.js +2 -5
  10. package/dist/commands/check-stable.d.ts +14 -0
  11. package/dist/commands/check-stable.js +95 -0
  12. package/dist/commands/check.js +41 -6
  13. package/dist/fix-dist.js +5 -0
  14. package/dist/helpers/resolve-canisters.d.ts +7 -0
  15. package/dist/helpers/resolve-canisters.js +31 -0
  16. package/dist/package.json +2 -2
  17. package/dist/tests/build.test.js +18 -0
  18. package/dist/tests/check-fix.test.js +17 -0
  19. package/dist/tests/check-stable.test.d.ts +1 -0
  20. package/dist/tests/check-stable.test.js +51 -0
  21. package/dist/tests/check.test.js +54 -1
  22. package/dist/tests/moc-args.test.js +3 -3
  23. package/dist/types.d.ts +5 -1
  24. package/dist/wasm/pkg/nodejs/wasm_bg.wasm +0 -0
  25. package/dist/wasm/pkg/web/wasm_bg.wasm +0 -0
  26. package/fix-dist.ts +9 -0
  27. package/helpers/resolve-canisters.ts +52 -0
  28. package/package.json +2 -2
  29. package/tests/README.md +16 -0
  30. package/tests/__snapshots__/check-fix.test.ts.snap +16 -10
  31. package/tests/__snapshots__/check-stable.test.ts.snap +29 -0
  32. package/tests/__snapshots__/check.test.ts.snap +26 -16
  33. package/tests/build/no-dfx/mops.toml +3 -0
  34. package/tests/build/no-dfx/src/Main.mo +1 -1
  35. package/tests/build.test.ts +24 -0
  36. package/tests/check/canisters/Ok.mo +5 -0
  37. package/tests/check/canisters/mops.toml +8 -0
  38. package/tests/check/canisters-error/Error.mo +7 -0
  39. package/tests/check/canisters-error/mops.toml +8 -0
  40. package/tests/check/canisters-moc-args/Warning.mo +5 -0
  41. package/tests/check/canisters-moc-args/mops.toml +8 -0
  42. package/tests/check/deployed-compatible/main.mo +4 -0
  43. package/tests/check/deployed-compatible/mops.toml +11 -0
  44. package/tests/check/deployed-compatible/old.mo +3 -0
  45. package/tests/check/deployed-compile-error/Error.mo +7 -0
  46. package/tests/check/deployed-compile-error/mops.toml +11 -0
  47. package/tests/check/deployed-compile-error/old.mo +3 -0
  48. package/tests/check/deployed-missing-error/Ok.mo +5 -0
  49. package/tests/check/deployed-missing-error/mops.toml +11 -0
  50. package/tests/check/deployed-missing-skip/Ok.mo +5 -0
  51. package/tests/check/deployed-missing-skip/mops.toml +12 -0
  52. package/tests/check/error/Error.mo +1 -1
  53. package/tests/check/error/mops.toml +5 -2
  54. package/tests/check/fix/M0223.mo +1 -1
  55. package/tests/check/fix/M0236.mo +1 -1
  56. package/tests/check/fix/M0237.mo +1 -1
  57. package/tests/check/fix/Ok.mo +1 -1
  58. package/tests/check/fix/fix-with-error.mo +9 -0
  59. package/tests/check/fix/fix-with-warning.mo +8 -0
  60. package/tests/check/fix/mops.toml +3 -0
  61. package/tests/check/fix/transitive-main.mo +1 -1
  62. package/tests/check/moc-args/Warning.mo +1 -1
  63. package/tests/check/moc-args/mops.toml +4 -1
  64. package/tests/check/success/Ok.mo +1 -1
  65. package/tests/check/success/Warning.mo +1 -1
  66. package/tests/check/success/mops.toml +5 -2
  67. package/tests/check-fix.test.ts +25 -0
  68. package/tests/check-stable/compatible/mops.toml +8 -0
  69. package/tests/check-stable/compatible/new.mo +4 -0
  70. package/tests/check-stable/compatible/old.mo +3 -0
  71. package/tests/check-stable/incompatible/mops.toml +8 -0
  72. package/tests/check-stable/incompatible/new.mo +3 -0
  73. package/tests/check-stable/incompatible/old.mo +4 -0
  74. package/tests/check-stable/subdirectory/.old/src/main.mo +3 -0
  75. package/tests/check-stable/subdirectory/mops.toml +8 -0
  76. package/tests/check-stable/subdirectory/src/main.mo +4 -0
  77. package/tests/check-stable.test.ts +56 -0
  78. package/tests/check.test.ts +63 -1
  79. package/tests/moc-args.test.ts +3 -3
  80. package/types.ts +5 -1
  81. package/wasm/pkg/nodejs/wasm_bg.wasm +0 -0
  82. package/wasm/pkg/web/wasm_bg.wasm +0 -0
  83. package/.DS_Store +0 -0
  84. package/bundle/bench/bench-canister.mo +0 -130
  85. package/bundle/bench/user-bench.mo +0 -10
  86. package/bundle/bin/moc-wrapper.sh +0 -40
  87. package/bundle/bin/mops.js +0 -3
  88. package/bundle/cli.js +0 -1569
  89. package/bundle/cli.tgz +0 -0
  90. package/bundle/declarations/bench/bench.did +0 -30
  91. package/bundle/declarations/bench/bench.did.d.ts +0 -33
  92. package/bundle/declarations/bench/bench.did.js +0 -30
  93. package/bundle/declarations/bench/index.d.ts +0 -50
  94. package/bundle/declarations/bench/index.js +0 -40
  95. package/bundle/declarations/main/index.d.ts +0 -50
  96. package/bundle/declarations/main/index.js +0 -40
  97. package/bundle/declarations/main/main.did +0 -428
  98. package/bundle/declarations/main/main.did.d.ts +0 -348
  99. package/bundle/declarations/main/main.did.js +0 -406
  100. package/bundle/declarations/storage/index.d.ts +0 -50
  101. package/bundle/declarations/storage/index.js +0 -30
  102. package/bundle/declarations/storage/storage.did +0 -46
  103. package/bundle/declarations/storage/storage.did.d.ts +0 -40
  104. package/bundle/declarations/storage/storage.did.js +0 -38
  105. package/bundle/default-stylesheet.css +0 -415
  106. package/bundle/package.json +0 -36
  107. package/bundle/templates/README.md +0 -13
  108. package/bundle/templates/licenses/Apache-2.0 +0 -202
  109. package/bundle/templates/licenses/Apache-2.0-NOTICE +0 -13
  110. package/bundle/templates/licenses/MIT +0 -21
  111. package/bundle/templates/mops-publish.yml +0 -17
  112. package/bundle/templates/mops-test.yml +0 -24
  113. package/bundle/templates/src/lib.mo +0 -15
  114. package/bundle/templates/test/lib.test.mo +0 -4
  115. package/bundle/wasm_bg.wasm +0 -0
  116. package/bundle/xhr-sync-worker.js +0 -51
  117. package/dist/wasm/pkg/bundler/package.json +0 -20
  118. package/dist/wasm/pkg/bundler/wasm.d.ts +0 -3
  119. package/dist/wasm/pkg/bundler/wasm.js +0 -5
  120. package/dist/wasm/pkg/bundler/wasm_bg.js +0 -93
  121. package/dist/wasm/pkg/bundler/wasm_bg.wasm +0 -0
  122. package/dist/wasm/pkg/bundler/wasm_bg.wasm.d.ts +0 -8
  123. package/tests/build/success/.dfx/local/canister_ids.json +0 -17
  124. package/tests/build/success/.dfx/local/canisters/bar/bar.did +0 -3
  125. package/tests/build/success/.dfx/local/canisters/bar/bar.most +0 -4
  126. package/tests/build/success/.dfx/local/canisters/bar/bar.wasm +0 -0
  127. package/tests/build/success/.dfx/local/canisters/bar/constructor.did +0 -3
  128. package/tests/build/success/.dfx/local/canisters/bar/index.js +0 -42
  129. package/tests/build/success/.dfx/local/canisters/bar/init_args.txt +0 -1
  130. package/tests/build/success/.dfx/local/canisters/bar/service.did +0 -3
  131. package/tests/build/success/.dfx/local/canisters/bar/service.did.d.ts +0 -7
  132. package/tests/build/success/.dfx/local/canisters/bar/service.did.js +0 -4
  133. package/tests/build/success/.dfx/local/canisters/foo/constructor.did +0 -3
  134. package/tests/build/success/.dfx/local/canisters/foo/foo.did +0 -3
  135. package/tests/build/success/.dfx/local/canisters/foo/foo.most +0 -4
  136. package/tests/build/success/.dfx/local/canisters/foo/foo.wasm +0 -0
  137. package/tests/build/success/.dfx/local/canisters/foo/index.js +0 -42
  138. package/tests/build/success/.dfx/local/canisters/foo/init_args.txt +0 -1
  139. package/tests/build/success/.dfx/local/canisters/foo/service.did +0 -3
  140. package/tests/build/success/.dfx/local/canisters/foo/service.did.d.ts +0 -7
  141. package/tests/build/success/.dfx/local/canisters/foo/service.did.js +0 -4
  142. package/tests/build/success/.dfx/local/lsp/ucwa4-rx777-77774-qaada-cai.did +0 -3
  143. package/tests/build/success/.dfx/local/lsp/ulvla-h7777-77774-qaacq-cai.did +0 -3
  144. package/tests/build/success/.dfx/local/network-id +0 -4
  145. package/wasm/Cargo.lock +0 -1475
  146. package/wasm/pkg/bundler/package.json +0 -20
  147. package/wasm/pkg/bundler/wasm.d.ts +0 -3
  148. package/wasm/pkg/bundler/wasm.js +0 -5
  149. package/wasm/pkg/bundler/wasm_bg.js +0 -93
  150. package/wasm/pkg/bundler/wasm_bg.wasm +0 -0
  151. package/wasm/pkg/bundler/wasm_bg.wasm.d.ts +0 -8
package/CHANGELOG.md CHANGED
@@ -2,8 +2,21 @@
2
2
 
3
3
  ## Next
4
4
 
5
+ ## 2.3.1
6
+ - Fix `mops build` and `mops check-candid` failing with "Wasm bindings have not been set" when installed via `npm i -g ic-mops`
7
+
8
+ ## 2.3.0
9
+ - Add `mops check-stable` command for stable variable compatibility checking
10
+ - `mops check` now falls back to canister entrypoints from `mops.toml` when no files are specified
11
+ - `mops check` automatically runs stable compatibility when `[canisters.<name>.check-stable]` is configured
12
+ - `mops check --fix` now behaves like fix + `mops check` — reports changed files, then type-checks and runs stable compatibility if configured
13
+ - `skipIfMissing` in `[canisters.<name>.check-stable]` silently skips when the file doesn't exist
14
+ - Add docs for `mops lint`, `mops moc-args`, `[canisters]`, `[build]`, and `[lint]` config sections
15
+ - Add docs canister deployment step to release process
16
+
5
17
  ## 2.2.1
6
18
  - Fix `mops toolchain` when toolchain version is a local file path with subdirectories.
19
+ - Update Motoko formatter (`prettier-plugin-motoko`).
7
20
 
8
21
  ## 2.2.0
9
22
  - Add `[moc]` config section for global `moc` compiler flags (applied to `check`, `build`, `test`, `bench`, `watch`)
package/RELEASE.md CHANGED
@@ -130,7 +130,15 @@ dfx deploy --network ic --no-wallet cli --identity mops
130
130
 
131
131
  This deploys the `cli-releases` canister (serving `cli.mops.one`) to the Internet Computer mainnet.
132
132
 
133
- ### 10. Commit and push release artifacts
133
+ ### 10. Deploy the docs canister
134
+
135
+ ```bash
136
+ dfx deploy --network ic --no-wallet docs --identity mops
137
+ ```
138
+
139
+ This builds the Docusaurus site (`docs/`) and deploys the `docs` assets canister (serving `docs.mops.one`). Docs are not auto-deployed, so this step ensures any documentation changes from the release are published.
140
+
141
+ ### 11. Commit and push release artifacts
134
142
 
135
143
  Step 8 generates files in `cli-releases/` that must be committed and pushed:
136
144
 
package/cli.ts CHANGED
@@ -12,6 +12,7 @@ import { build, DEFAULT_BUILD_OUTPUT_DIR } from "./commands/build.js";
12
12
  import { bump } from "./commands/bump.js";
13
13
  import { check } from "./commands/check.js";
14
14
  import { checkCandid } from "./commands/check-candid.js";
15
+ import { checkStable } from "./commands/check-stable.js";
15
16
  import { docsCoverage } from "./commands/docs-coverage.js";
16
17
  import { docs } from "./commands/docs.js";
17
18
  import { format } from "./commands/format.js";
@@ -322,9 +323,9 @@ program
322
323
 
323
324
  // check
324
325
  program
325
- .command("check <files...>")
326
+ .command("check [files...]")
326
327
  .description(
327
- "Check Motoko entrypoint files for syntax errors and type issues (including transitively imported files)",
328
+ "Check Motoko files for syntax errors and type issues. If no files are specified, checks all canister entrypoints from mops.toml. Also runs stable compatibility checks for canisters with [check-stable] configured",
328
329
  )
329
330
  .option("--verbose", "Verbose console output")
330
331
  .addOption(
@@ -362,6 +363,28 @@ program
362
363
  await checkCandid(newCandid, originalCandid);
363
364
  });
364
365
 
366
+ // check-stable
367
+ program
368
+ .command("check-stable <old-file> [canister]")
369
+ .description(
370
+ "Check stable variable compatibility between an old version (.mo or .most file) and the current canister entrypoint",
371
+ )
372
+ .option("--verbose", "Verbose console output")
373
+ .allowUnknownOption(true)
374
+ .action(async (oldFile, canister, options) => {
375
+ checkConfigFile(true);
376
+ const { extraArgs } = parseExtraArgs();
377
+ await installAll({
378
+ silent: true,
379
+ lock: "ignore",
380
+ installFromLockFile: true,
381
+ });
382
+ await checkStable(oldFile, canister, {
383
+ ...options,
384
+ extraArgs,
385
+ });
386
+ });
387
+
365
388
  // test
366
389
  program
367
390
  .command("test [filter]")
package/commands/build.ts CHANGED
@@ -5,9 +5,9 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import { join } from "node:path";
6
6
  import { cliError } from "../error.js";
7
7
  import { isCandidCompatible } from "../helpers/is-candid-compatible.js";
8
+ import { resolveCanisterConfigs } from "../helpers/resolve-canisters.js";
8
9
  import { CustomSection, getWasmBindings } from "../wasm.js";
9
10
  import { getGlobalMocArgs, readConfig } from "../mops.js";
10
- import { CanisterConfig } from "../types.js";
11
11
  import { sourcesArgs } from "./sources.js";
12
12
  import { toolchain } from "./toolchain/index.js";
13
13
 
@@ -29,16 +29,8 @@ export async function build(
29
29
 
30
30
  let outputDir = options.outputDir ?? DEFAULT_BUILD_OUTPUT_DIR;
31
31
  let mocPath = await toolchain.bin("moc", { fallback: true });
32
- let canisters: Record<string, CanisterConfig> = {};
33
32
  let config = readConfig();
34
- if (config.canisters) {
35
- canisters =
36
- Object.fromEntries(
37
- Object.entries(config.canisters).map(([name, c]) =>
38
- typeof c === "string" ? [name, { main: c }] : [name, c],
39
- ),
40
- ) ?? {};
41
- }
33
+ let canisters = resolveCanisterConfigs(config);
42
34
  if (!Object.keys(canisters).length) {
43
35
  cliError(`No Motoko canisters found in mops.toml configuration`);
44
36
  }
@@ -0,0 +1,177 @@
1
+ import { basename, join, relative, resolve } from "node:path";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
+ import { rename, rm } from "node:fs/promises";
4
+ import chalk from "chalk";
5
+ import { execa } from "execa";
6
+ import { cliError } from "../error.js";
7
+ import { getGlobalMocArgs, readConfig } from "../mops.js";
8
+ import { resolveSingleCanister } from "../helpers/resolve-canisters.js";
9
+ import { sourcesArgs } from "./sources.js";
10
+ import { toolchain } from "./toolchain/index.js";
11
+
12
+ const CHECK_STABLE_DIR = ".mops/.check-stable";
13
+
14
+ export interface CheckStableOptions {
15
+ verbose: boolean;
16
+ extraArgs: string[];
17
+ }
18
+
19
+ export async function checkStable(
20
+ oldFile: string,
21
+ canisterName: string | undefined,
22
+ options: Partial<CheckStableOptions> = {},
23
+ ): Promise<void> {
24
+ const config = readConfig();
25
+ const { name, canister } = resolveSingleCanister(config, canisterName);
26
+
27
+ if (!canister.main) {
28
+ cliError(`No main file specified for canister '${name}' in mops.toml`);
29
+ }
30
+
31
+ const mocPath = await toolchain.bin("moc", { fallback: true });
32
+ const globalMocArgs = getGlobalMocArgs(config);
33
+
34
+ await runStableCheck({
35
+ oldFile,
36
+ canisterMain: canister.main,
37
+ canisterName: name,
38
+ mocPath,
39
+ globalMocArgs,
40
+ options,
41
+ });
42
+ }
43
+
44
+ export interface RunStableCheckParams {
45
+ oldFile: string;
46
+ canisterMain: string;
47
+ canisterName: string;
48
+ mocPath: string;
49
+ globalMocArgs: string[];
50
+ options?: Partial<CheckStableOptions>;
51
+ }
52
+
53
+ export async function runStableCheck(
54
+ params: RunStableCheckParams,
55
+ ): Promise<void> {
56
+ const {
57
+ oldFile,
58
+ canisterMain,
59
+ canisterName,
60
+ mocPath,
61
+ globalMocArgs,
62
+ options = {},
63
+ } = params;
64
+
65
+ const checkStableDir = resolve(CHECK_STABLE_DIR);
66
+ const sources = (await sourcesArgs({ cwd: checkStableDir })).flat();
67
+ const isOldMostFile = oldFile.endsWith(".most");
68
+
69
+ if (!existsSync(oldFile)) {
70
+ cliError(`File not found: ${oldFile}`);
71
+ }
72
+
73
+ await rm(CHECK_STABLE_DIR, { recursive: true, force: true });
74
+ mkdirSync(CHECK_STABLE_DIR, { recursive: true });
75
+ try {
76
+ const oldMostPath = isOldMostFile
77
+ ? oldFile
78
+ : await generateStableTypes(
79
+ mocPath,
80
+ oldFile,
81
+ join(CHECK_STABLE_DIR, "old.most"),
82
+ sources,
83
+ globalMocArgs,
84
+ options,
85
+ );
86
+
87
+ const newMostPath = await generateStableTypes(
88
+ mocPath,
89
+ canisterMain,
90
+ join(CHECK_STABLE_DIR, "new.most"),
91
+ sources,
92
+ globalMocArgs,
93
+ options,
94
+ );
95
+
96
+ if (options.verbose) {
97
+ console.log(
98
+ chalk.blue("check-stable"),
99
+ chalk.gray(`Comparing ${oldMostPath} ↔ ${newMostPath}`),
100
+ );
101
+ }
102
+
103
+ const args = ["--stable-compatible", oldMostPath, newMostPath];
104
+ if (options.verbose) {
105
+ console.log(chalk.gray(mocPath, JSON.stringify(args)));
106
+ }
107
+
108
+ const result = await execa(mocPath, args, {
109
+ stdio: "pipe",
110
+ reject: false,
111
+ });
112
+
113
+ if (result.exitCode !== 0) {
114
+ if (result.stderr) {
115
+ console.error(result.stderr);
116
+ }
117
+ cliError(
118
+ `✗ Stable compatibility check failed for canister '${canisterName}'`,
119
+ );
120
+ }
121
+
122
+ console.log(
123
+ chalk.green(
124
+ `✓ Stable compatibility check passed for canister '${canisterName}'`,
125
+ ),
126
+ );
127
+ } finally {
128
+ await rm(CHECK_STABLE_DIR, { recursive: true, force: true });
129
+ }
130
+ }
131
+
132
+ async function generateStableTypes(
133
+ mocPath: string,
134
+ moFile: string,
135
+ outputPath: string,
136
+ sources: string[],
137
+ globalMocArgs: string[],
138
+ options: Partial<CheckStableOptions>,
139
+ ): Promise<string> {
140
+ const relFile = relative(resolve(CHECK_STABLE_DIR), resolve(moFile));
141
+ const args = [
142
+ "--stable-types",
143
+ relFile,
144
+ ...sources,
145
+ ...globalMocArgs,
146
+ ...(options.extraArgs ?? []),
147
+ ];
148
+
149
+ if (options.verbose) {
150
+ console.log(
151
+ chalk.blue("check-stable"),
152
+ chalk.gray(`Generating stable types for ${moFile}`),
153
+ );
154
+ console.log(chalk.gray(mocPath, JSON.stringify(args)));
155
+ }
156
+
157
+ const result = await execa(mocPath, args, {
158
+ cwd: CHECK_STABLE_DIR,
159
+ stdio: "pipe",
160
+ reject: false,
161
+ });
162
+
163
+ if (result.exitCode !== 0) {
164
+ if (result.stderr) {
165
+ console.error(result.stderr);
166
+ }
167
+ cliError(
168
+ `Failed to generate stable types for ${moFile} (exit code: ${result.exitCode})`,
169
+ );
170
+ }
171
+
172
+ const base = basename(moFile, ".mo");
173
+ await rename(join(CHECK_STABLE_DIR, base + ".most"), outputPath);
174
+ await rm(join(CHECK_STABLE_DIR, base + ".wasm"), { force: true });
175
+
176
+ return outputPath;
177
+ }
package/commands/check.ts CHANGED
@@ -1,10 +1,16 @@
1
1
  import { relative } from "node:path";
2
+ import { existsSync } from "node:fs";
2
3
  import chalk from "chalk";
3
4
  import { execa } from "execa";
4
5
  import { cliError } from "../error.js";
5
6
  import { getGlobalMocArgs, readConfig } from "../mops.js";
6
7
  import { autofixMotoko } from "../helpers/autofix-motoko.js";
7
8
  import { getMocSemVer } from "../helpers/get-moc-version.js";
9
+ import {
10
+ resolveCanisterConfigs,
11
+ resolveCanisterEntrypoints,
12
+ } from "../helpers/resolve-canisters.js";
13
+ import { runStableCheck } from "./check-stable.js";
8
14
  import { sourcesArgs } from "./sources.js";
9
15
  import { toolchain } from "./toolchain/index.js";
10
16
 
@@ -25,13 +31,23 @@ export async function check(
25
31
  files: string | string[],
26
32
  options: Partial<CheckOptions> = {},
27
33
  ): Promise<void> {
28
- const fileList = Array.isArray(files) ? files : [files];
34
+ let fileList = Array.isArray(files) ? files : files ? [files] : [];
35
+
36
+ const config = readConfig();
29
37
 
30
38
  if (fileList.length === 0) {
31
- cliError("No Motoko files specified for checking");
39
+ fileList = resolveCanisterEntrypoints(config);
32
40
  }
33
41
 
34
- const config = readConfig();
42
+ if (fileList.length === 0) {
43
+ cliError(
44
+ "No Motoko files specified and no canisters defined in mops.toml.\n" +
45
+ "Either pass files: mops check <files...>\n" +
46
+ "Or define canisters in mops.toml:\n\n" +
47
+ " [canisters.backend]\n" +
48
+ ' main = "src/main.mo"',
49
+ );
50
+ }
35
51
  const mocPath = await toolchain.bin("moc", { fallback: true });
36
52
  const sources = await sourcesArgs();
37
53
  const globalMocArgs = getGlobalMocArgs(config);
@@ -109,13 +125,44 @@ export async function check(
109
125
  );
110
126
  }
111
127
 
112
- if (!options.fix) {
113
- console.log(chalk.green(`✓ ${file}`));
114
- }
128
+ console.log(chalk.green(`✓ ${file}`));
115
129
  } catch (err: any) {
116
130
  cliError(
117
131
  `Error while checking ${file}${err?.message ? `\n${err.message}` : ""}`,
118
132
  );
119
133
  }
120
134
  }
135
+
136
+ const canisters = resolveCanisterConfigs(config);
137
+ for (const [name, canister] of Object.entries(canisters)) {
138
+ const stableConfig = canister["check-stable"];
139
+ if (!stableConfig) {
140
+ continue;
141
+ }
142
+
143
+ if (!canister.main) {
144
+ cliError(`No main file specified for canister '${name}' in mops.toml`);
145
+ }
146
+
147
+ if (!existsSync(stableConfig.path)) {
148
+ if (stableConfig.skipIfMissing) {
149
+ continue;
150
+ }
151
+ cliError(
152
+ `Deployed file not found: ${stableConfig.path} (canister '${name}')\n` +
153
+ "Set skipIfMissing = true in [canisters." +
154
+ name +
155
+ ".check-stable] to skip this check when the file is missing.",
156
+ );
157
+ }
158
+
159
+ await runStableCheck({
160
+ oldFile: stableConfig.path,
161
+ canisterMain: canister.main,
162
+ canisterName: name,
163
+ mocPath,
164
+ globalMocArgs,
165
+ options: { verbose: options.verbose, extraArgs: options.extraArgs },
166
+ });
167
+ }
121
168
  }
package/dist/bin/mops.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import "../cli.js";
3
+ import "../environments/nodejs/cli.js";
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ import { build, DEFAULT_BUILD_OUTPUT_DIR } from "./commands/build.js";
11
11
  import { bump } from "./commands/bump.js";
12
12
  import { check } from "./commands/check.js";
13
13
  import { checkCandid } from "./commands/check-candid.js";
14
+ import { checkStable } from "./commands/check-stable.js";
14
15
  import { docsCoverage } from "./commands/docs-coverage.js";
15
16
  import { docs } from "./commands/docs.js";
16
17
  import { format } from "./commands/format.js";
@@ -252,8 +253,8 @@ program
252
253
  });
253
254
  // check
254
255
  program
255
- .command("check <files...>")
256
- .description("Check Motoko entrypoint files for syntax errors and type issues (including transitively imported files)")
256
+ .command("check [files...]")
257
+ .description("Check Motoko files for syntax errors and type issues. If no files are specified, checks all canister entrypoints from mops.toml. Also runs stable compatibility checks for canisters with [check-stable] configured")
257
258
  .option("--verbose", "Verbose console output")
258
259
  .addOption(new Option("--fix", "Apply autofixes to all files, including transitively imported ones"))
259
260
  .allowUnknownOption(true)
@@ -283,6 +284,25 @@ program
283
284
  });
284
285
  await checkCandid(newCandid, originalCandid);
285
286
  });
287
+ // check-stable
288
+ program
289
+ .command("check-stable <old-file> [canister]")
290
+ .description("Check stable variable compatibility between an old version (.mo or .most file) and the current canister entrypoint")
291
+ .option("--verbose", "Verbose console output")
292
+ .allowUnknownOption(true)
293
+ .action(async (oldFile, canister, options) => {
294
+ checkConfigFile(true);
295
+ const { extraArgs } = parseExtraArgs();
296
+ await installAll({
297
+ silent: true,
298
+ lock: "ignore",
299
+ installFromLockFile: true,
300
+ });
301
+ await checkStable(oldFile, canister, {
302
+ ...options,
303
+ extraArgs,
304
+ });
305
+ });
286
306
  // test
287
307
  program
288
308
  .command("test [filter]")
@@ -5,6 +5,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import { join } from "node:path";
6
6
  import { cliError } from "../error.js";
7
7
  import { isCandidCompatible } from "../helpers/is-candid-compatible.js";
8
+ import { resolveCanisterConfigs } from "../helpers/resolve-canisters.js";
8
9
  import { getWasmBindings } from "../wasm.js";
9
10
  import { getGlobalMocArgs, readConfig } from "../mops.js";
10
11
  import { sourcesArgs } from "./sources.js";
@@ -16,12 +17,8 @@ export async function build(canisterNames, options) {
16
17
  }
17
18
  let outputDir = options.outputDir ?? DEFAULT_BUILD_OUTPUT_DIR;
18
19
  let mocPath = await toolchain.bin("moc", { fallback: true });
19
- let canisters = {};
20
20
  let config = readConfig();
21
- if (config.canisters) {
22
- canisters =
23
- Object.fromEntries(Object.entries(config.canisters).map(([name, c]) => typeof c === "string" ? [name, { main: c }] : [name, c])) ?? {};
24
- }
21
+ let canisters = resolveCanisterConfigs(config);
25
22
  if (!Object.keys(canisters).length) {
26
23
  cliError(`No Motoko canisters found in mops.toml configuration`);
27
24
  }
@@ -0,0 +1,14 @@
1
+ export interface CheckStableOptions {
2
+ verbose: boolean;
3
+ extraArgs: string[];
4
+ }
5
+ export declare function checkStable(oldFile: string, canisterName: string | undefined, options?: Partial<CheckStableOptions>): Promise<void>;
6
+ export interface RunStableCheckParams {
7
+ oldFile: string;
8
+ canisterMain: string;
9
+ canisterName: string;
10
+ mocPath: string;
11
+ globalMocArgs: string[];
12
+ options?: Partial<CheckStableOptions>;
13
+ }
14
+ export declare function runStableCheck(params: RunStableCheckParams): Promise<void>;
@@ -0,0 +1,95 @@
1
+ import { basename, join, relative, resolve } from "node:path";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
+ import { rename, rm } from "node:fs/promises";
4
+ import chalk from "chalk";
5
+ import { execa } from "execa";
6
+ import { cliError } from "../error.js";
7
+ import { getGlobalMocArgs, readConfig } from "../mops.js";
8
+ import { resolveSingleCanister } from "../helpers/resolve-canisters.js";
9
+ import { sourcesArgs } from "./sources.js";
10
+ import { toolchain } from "./toolchain/index.js";
11
+ const CHECK_STABLE_DIR = ".mops/.check-stable";
12
+ export async function checkStable(oldFile, canisterName, options = {}) {
13
+ const config = readConfig();
14
+ const { name, canister } = resolveSingleCanister(config, canisterName);
15
+ if (!canister.main) {
16
+ cliError(`No main file specified for canister '${name}' in mops.toml`);
17
+ }
18
+ const mocPath = await toolchain.bin("moc", { fallback: true });
19
+ const globalMocArgs = getGlobalMocArgs(config);
20
+ await runStableCheck({
21
+ oldFile,
22
+ canisterMain: canister.main,
23
+ canisterName: name,
24
+ mocPath,
25
+ globalMocArgs,
26
+ options,
27
+ });
28
+ }
29
+ export async function runStableCheck(params) {
30
+ const { oldFile, canisterMain, canisterName, mocPath, globalMocArgs, options = {}, } = params;
31
+ const checkStableDir = resolve(CHECK_STABLE_DIR);
32
+ const sources = (await sourcesArgs({ cwd: checkStableDir })).flat();
33
+ const isOldMostFile = oldFile.endsWith(".most");
34
+ if (!existsSync(oldFile)) {
35
+ cliError(`File not found: ${oldFile}`);
36
+ }
37
+ await rm(CHECK_STABLE_DIR, { recursive: true, force: true });
38
+ mkdirSync(CHECK_STABLE_DIR, { recursive: true });
39
+ try {
40
+ const oldMostPath = isOldMostFile
41
+ ? oldFile
42
+ : await generateStableTypes(mocPath, oldFile, join(CHECK_STABLE_DIR, "old.most"), sources, globalMocArgs, options);
43
+ const newMostPath = await generateStableTypes(mocPath, canisterMain, join(CHECK_STABLE_DIR, "new.most"), sources, globalMocArgs, options);
44
+ if (options.verbose) {
45
+ console.log(chalk.blue("check-stable"), chalk.gray(`Comparing ${oldMostPath} ↔ ${newMostPath}`));
46
+ }
47
+ const args = ["--stable-compatible", oldMostPath, newMostPath];
48
+ if (options.verbose) {
49
+ console.log(chalk.gray(mocPath, JSON.stringify(args)));
50
+ }
51
+ const result = await execa(mocPath, args, {
52
+ stdio: "pipe",
53
+ reject: false,
54
+ });
55
+ if (result.exitCode !== 0) {
56
+ if (result.stderr) {
57
+ console.error(result.stderr);
58
+ }
59
+ cliError(`✗ Stable compatibility check failed for canister '${canisterName}'`);
60
+ }
61
+ console.log(chalk.green(`✓ Stable compatibility check passed for canister '${canisterName}'`));
62
+ }
63
+ finally {
64
+ await rm(CHECK_STABLE_DIR, { recursive: true, force: true });
65
+ }
66
+ }
67
+ async function generateStableTypes(mocPath, moFile, outputPath, sources, globalMocArgs, options) {
68
+ const relFile = relative(resolve(CHECK_STABLE_DIR), resolve(moFile));
69
+ const args = [
70
+ "--stable-types",
71
+ relFile,
72
+ ...sources,
73
+ ...globalMocArgs,
74
+ ...(options.extraArgs ?? []),
75
+ ];
76
+ if (options.verbose) {
77
+ console.log(chalk.blue("check-stable"), chalk.gray(`Generating stable types for ${moFile}`));
78
+ console.log(chalk.gray(mocPath, JSON.stringify(args)));
79
+ }
80
+ const result = await execa(mocPath, args, {
81
+ cwd: CHECK_STABLE_DIR,
82
+ stdio: "pipe",
83
+ reject: false,
84
+ });
85
+ if (result.exitCode !== 0) {
86
+ if (result.stderr) {
87
+ console.error(result.stderr);
88
+ }
89
+ cliError(`Failed to generate stable types for ${moFile} (exit code: ${result.exitCode})`);
90
+ }
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 });
94
+ return outputPath;
95
+ }
@@ -1,10 +1,13 @@
1
1
  import { relative } from "node:path";
2
+ import { existsSync } from "node:fs";
2
3
  import chalk from "chalk";
3
4
  import { execa } from "execa";
4
5
  import { cliError } from "../error.js";
5
6
  import { getGlobalMocArgs, readConfig } from "../mops.js";
6
7
  import { autofixMotoko } from "../helpers/autofix-motoko.js";
7
8
  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
11
  import { sourcesArgs } from "./sources.js";
9
12
  import { toolchain } from "./toolchain/index.js";
10
13
  const MOC_ALL_LIBS_MIN_VERSION = "1.3.0";
@@ -13,11 +16,18 @@ function supportsAllLibsFlag() {
13
16
  return version ? version.compare(MOC_ALL_LIBS_MIN_VERSION) >= 0 : false;
14
17
  }
15
18
  export async function check(files, options = {}) {
16
- const fileList = Array.isArray(files) ? files : [files];
19
+ let fileList = Array.isArray(files) ? files : files ? [files] : [];
20
+ const config = readConfig();
17
21
  if (fileList.length === 0) {
18
- cliError("No Motoko files specified for checking");
22
+ fileList = resolveCanisterEntrypoints(config);
23
+ }
24
+ if (fileList.length === 0) {
25
+ cliError("No Motoko files specified and no canisters defined in mops.toml.\n" +
26
+ "Either pass files: mops check <files...>\n" +
27
+ "Or define canisters in mops.toml:\n\n" +
28
+ " [canisters.backend]\n" +
29
+ ' main = "src/main.mo"');
19
30
  }
20
- const config = readConfig();
21
31
  const mocPath = await toolchain.bin("moc", { fallback: true });
22
32
  const sources = await sourcesArgs();
23
33
  const globalMocArgs = getGlobalMocArgs(config);
@@ -71,12 +81,37 @@ export async function check(files, options = {}) {
71
81
  if (result.exitCode !== 0) {
72
82
  cliError(`✗ Check failed for file ${file} (exit code: ${result.exitCode})`);
73
83
  }
74
- if (!options.fix) {
75
- console.log(chalk.green(`✓ ${file}`));
76
- }
84
+ console.log(chalk.green(`✓ ${file}`));
77
85
  }
78
86
  catch (err) {
79
87
  cliError(`Error while checking ${file}${err?.message ? `\n${err.message}` : ""}`);
80
88
  }
81
89
  }
90
+ const canisters = resolveCanisterConfigs(config);
91
+ for (const [name, canister] of Object.entries(canisters)) {
92
+ const stableConfig = canister["check-stable"];
93
+ if (!stableConfig) {
94
+ continue;
95
+ }
96
+ if (!canister.main) {
97
+ cliError(`No main file specified for canister '${name}' in mops.toml`);
98
+ }
99
+ if (!existsSync(stableConfig.path)) {
100
+ if (stableConfig.skipIfMissing) {
101
+ continue;
102
+ }
103
+ cliError(`Deployed file not found: ${stableConfig.path} (canister '${name}')\n` +
104
+ "Set skipIfMissing = true in [canisters." +
105
+ name +
106
+ ".check-stable] to skip this check when the file is missing.");
107
+ }
108
+ await runStableCheck({
109
+ oldFile: stableConfig.path,
110
+ canisterMain: canister.main,
111
+ canisterName: name,
112
+ mocPath,
113
+ globalMocArgs,
114
+ options: { verbose: options.verbose, extraArgs: options.extraArgs },
115
+ });
116
+ }
82
117
  }
package/dist/fix-dist.js CHANGED
@@ -7,3 +7,8 @@ delete json.scripts;
7
7
  json.bin.mops = "bin/mops.js";
8
8
  json.bin["ic-mops"] = "bin/mops.js";
9
9
  writeFileSync("dist/package.json", JSON.stringify(json, null, 2));
10
+ // Route the npm entry point through the Node.js environment wrapper
11
+ // so setWasmBindings() is called before the CLI runs.
12
+ // The source bin/mops.js imports ../cli.js (needed for the single-file bundle),
13
+ // but dist/ has the full directory structure with environments/nodejs/cli.js.
14
+ writeFileSync("dist/bin/mops.js", '#!/usr/bin/env node\n\nimport "../environments/nodejs/cli.js";\n');
@@ -0,0 +1,7 @@
1
+ import { CanisterConfig, Config } from "../types.js";
2
+ export declare function resolveCanisterConfigs(config: Config): Record<string, CanisterConfig>;
3
+ export declare function resolveCanisterEntrypoints(config: Config): string[];
4
+ export declare function resolveSingleCanister(config: Config, canisterName?: string): {
5
+ name: string;
6
+ canister: CanisterConfig;
7
+ };