ic-mops 2.15.0 → 2.15.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Next
4
4
 
5
+ ## 2.15.1
6
+
7
+ - `mops check --fix` no longer aborts when a fixable file is read-only (e.g. a frozen migration chain file deliberately `chmod`'d to remove write access). The autofixer now skips such files with a warning and continues fixing the rest, instead of crashing the whole run on `EACCES`/`EPERM`.
8
+
9
+ - Load the PocketIC client lazily, only when a command actually starts a replica. Commands like `mops check`, `mops build`, and `mops install` no longer pay to load it (and its `@icp-sdk/core` dependency) at startup. This also unblocks running the CLI via `tsx` in local dev, where `pic-js-mops` (shipped as ESM without `"type": "module"`) fails to resolve as a static import.
10
+
11
+ - `mops bench --verbose` is now actually verbose. It prints the benchmark pipeline up front — compiler version, replica + version, GC, profile, and whether the wasm is optimized (`dfx` post-optimizes with `optimize: "cycles"` via ic-wasm on deploy; `pocket-ic` runs the raw `moc` output) — logs the full `moc` build command, and streams the compiler and `dfx` output instead of capturing and discarding it. Notably this surfaces dfx's `WARNING: Failed to optimize the Wasm module`, which dfx prints (and then silently deploys the unoptimized module) when `optimize: "cycles"` fails — e.g. on multi-value modules that the bundled ic-wasm can't process. Previously all of this was hidden even with `--verbose`.
12
+
5
13
  ## 2.15.0
6
14
 
7
15
  - Fix `mops check --fix` corrupting source on lines containing multi-byte UTF-8 characters (e.g. `Char.toNat32('京')` dropping its trailing `)`). The autofixer was feeding moc's UTF-8 byte columns into LSP's UTF-16 position API, mis-applying every edit past the first non-ASCII byte on the line. When moc emits `byte_start`/`byte_end` (1.10.0 and newer) the fixer now applies edits byte-accurately; older moc still falls back to the line+column path (unchanged behavior, still ASCII-only).
package/bundle/cli.tgz CHANGED
Binary file
package/cli.ts CHANGED
@@ -543,7 +543,12 @@ program
543
543
  ),
544
544
  )
545
545
  // .addOption(new Option('--force-gc', 'Force GC'))
546
- .addOption(new Option("--verbose", "Show more information"))
546
+ .addOption(
547
+ new Option(
548
+ "--verbose",
549
+ "Print the benchmark pipeline (compiler, replica, GC, optimization) and stream compiler/replica output, including dfx optimization warnings",
550
+ ),
551
+ )
547
552
  .action(async (filter, options) => {
548
553
  checkConfigFile(true);
549
554
  await installAll({
@@ -74,7 +74,8 @@ export class BenchReplica {
74
74
  if (this.type === "dfx" || this.type === "dfx-pocket-ic") {
75
75
  await execaCommand(
76
76
  `dfx deploy ${name} --mode reinstall --yes --identity anonymous`,
77
- { cwd, stdio: this.verbose ? "pipe" : ["pipe", "ignore", "pipe"] },
77
+ // `inherit` so dfx output is streamed under --verbose (incl. its `Failed to optimize` warning)
78
+ { cwd, stdio: this.verbose ? "inherit" : ["pipe", "ignore", "pipe"] },
78
79
  );
79
80
  let canisterId = execSync(`dfx canister id ${name}`, { cwd })
80
81
  .toString()
package/commands/bench.ts CHANGED
@@ -101,7 +101,28 @@ export async function bench(
101
101
 
102
102
  warnIfDfxReplica(replicaType, optionsArg.replica === "dfx");
103
103
 
104
- options.verbose && console.log(options);
104
+ if (options.verbose) {
105
+ // `dfx` post-optimizes the wasm on deploy (`optimize: "cycles"`, via ic-wasm);
106
+ // `pocket-ic` runs the raw moc output. This changes instruction counts, so surface it.
107
+ let optimize =
108
+ replicaType === "dfx" || replicaType === "dfx-pocket-ic"
109
+ ? 'dfx `optimize: "cycles"` (ic-wasm) on deploy'
110
+ : "none (raw moc output)";
111
+ console.log(chalk.gray("Benchmark pipeline:"));
112
+ console.log(chalk.gray(` compiler: moc ${options.compilerVersion}`));
113
+ console.log(
114
+ chalk.gray(
115
+ ` replica: ${replicaType}${options.replicaVersion ? ` ${options.replicaVersion}` : ""}`,
116
+ ),
117
+ );
118
+ console.log(
119
+ chalk.gray(
120
+ ` gc: ${options.gc}${options.forceGc ? " (forced)" : ""}`,
121
+ ),
122
+ );
123
+ console.log(chalk.gray(` profile: ${options.profile}`));
124
+ console.log(chalk.gray(` optimize: ${optimize}`));
125
+ }
105
126
 
106
127
  let replica = new BenchReplica(replicaType, options.verbose);
107
128
 
@@ -303,14 +324,16 @@ async function deployBenchFile(
303
324
  // build canister
304
325
  let mocPath = getMocPath();
305
326
  let mocArgs = getMocArgs(options);
306
- options.verbose && console.time(`build ${canisterName}`);
307
- await execaCommand(
308
- `${mocPath} -c --idl canister.mo ${globalMocArgs.join(" ")} ${mocArgs} ${(await sources({ cwd: tempDir })).join(" ")}`,
309
- {
310
- cwd: tempDir,
311
- stdio: options.verbose ? "pipe" : ["pipe", "ignore", "pipe"],
312
- },
313
- );
327
+ let buildCmd = `${mocPath} -c --idl canister.mo ${globalMocArgs.join(" ")} ${mocArgs} ${(await sources({ cwd: tempDir })).join(" ")}`;
328
+ if (options.verbose) {
329
+ console.log(chalk.gray(`[${canisterName}] ${buildCmd}`));
330
+ console.time(`build ${canisterName}`);
331
+ }
332
+ await execaCommand(buildCmd, {
333
+ cwd: tempDir,
334
+ // `inherit` so the compiler output (warnings/errors) is streamed under --verbose
335
+ stdio: options.verbose ? "inherit" : ["pipe", "ignore", "pipe"],
336
+ });
314
337
  options.verbose && console.timeEnd(`build ${canisterName}`);
315
338
 
316
339
  // deploy canister
package/dist/cli.js CHANGED
@@ -393,7 +393,7 @@ program
393
393
  .addOption(new Option("--save", "Save benchmark results to .bench/<filename>.json"))
394
394
  .addOption(new Option("--compare", "Run benchmark and compare results with .bench/<filename>.json"))
395
395
  // .addOption(new Option('--force-gc', 'Force GC'))
396
- .addOption(new Option("--verbose", "Show more information"))
396
+ .addOption(new Option("--verbose", "Print the benchmark pipeline (compiler, replica, GC, optimization) and stream compiler/replica output, including dfx optimization warnings"))
397
397
  .action(async (filter, options) => {
398
398
  checkConfigFile(true);
399
399
  await installAll({
@@ -55,7 +55,9 @@ export class BenchReplica {
55
55
  }
56
56
  async deploy(name, wasm, cwd = process.cwd()) {
57
57
  if (this.type === "dfx" || this.type === "dfx-pocket-ic") {
58
- await execaCommand(`dfx deploy ${name} --mode reinstall --yes --identity anonymous`, { cwd, stdio: this.verbose ? "pipe" : ["pipe", "ignore", "pipe"] });
58
+ await execaCommand(`dfx deploy ${name} --mode reinstall --yes --identity anonymous`,
59
+ // `inherit` so dfx output is streamed under --verbose (incl. its `Failed to optimize` warning)
60
+ { cwd, stdio: this.verbose ? "inherit" : ["pipe", "ignore", "pipe"] });
59
61
  let canisterId = execSync(`dfx canister id ${name}`, { cwd })
60
62
  .toString()
61
63
  .trim();
@@ -61,7 +61,19 @@ export async function bench(filter = "", optionsArg = {}) {
61
61
  options.replicaVersion = config.toolchain?.["pocket-ic"] || "";
62
62
  }
63
63
  warnIfDfxReplica(replicaType, optionsArg.replica === "dfx");
64
- options.verbose && console.log(options);
64
+ if (options.verbose) {
65
+ // `dfx` post-optimizes the wasm on deploy (`optimize: "cycles"`, via ic-wasm);
66
+ // `pocket-ic` runs the raw moc output. This changes instruction counts, so surface it.
67
+ let optimize = replicaType === "dfx" || replicaType === "dfx-pocket-ic"
68
+ ? 'dfx `optimize: "cycles"` (ic-wasm) on deploy'
69
+ : "none (raw moc output)";
70
+ console.log(chalk.gray("Benchmark pipeline:"));
71
+ console.log(chalk.gray(` compiler: moc ${options.compilerVersion}`));
72
+ console.log(chalk.gray(` replica: ${replicaType}${options.replicaVersion ? ` ${options.replicaVersion}` : ""}`));
73
+ console.log(chalk.gray(` gc: ${options.gc}${options.forceGc ? " (forced)" : ""}`));
74
+ console.log(chalk.gray(` profile: ${options.profile}`));
75
+ console.log(chalk.gray(` optimize: ${optimize}`));
76
+ }
65
77
  let replica = new BenchReplica(replicaType, options.verbose);
66
78
  let rootDir = getRootDir();
67
79
  let globStr = "**/bench?(mark)/**/*.bench.mo";
@@ -207,10 +219,15 @@ async function deployBenchFile(file, options, replica, globalMocArgs) {
207
219
  // build canister
208
220
  let mocPath = getMocPath();
209
221
  let mocArgs = getMocArgs(options);
210
- options.verbose && console.time(`build ${canisterName}`);
211
- await execaCommand(`${mocPath} -c --idl canister.mo ${globalMocArgs.join(" ")} ${mocArgs} ${(await sources({ cwd: tempDir })).join(" ")}`, {
222
+ let buildCmd = `${mocPath} -c --idl canister.mo ${globalMocArgs.join(" ")} ${mocArgs} ${(await sources({ cwd: tempDir })).join(" ")}`;
223
+ if (options.verbose) {
224
+ console.log(chalk.gray(`[${canisterName}] ${buildCmd}`));
225
+ console.time(`build ${canisterName}`);
226
+ }
227
+ await execaCommand(buildCmd, {
212
228
  cwd: tempDir,
213
- stdio: options.verbose ? "pipe" : ["pipe", "ignore", "pipe"],
229
+ // `inherit` so the compiler output (warnings/errors) is streamed under --verbose
230
+ stdio: options.verbose ? "inherit" : ["pipe", "ignore", "pipe"],
214
231
  });
215
232
  options.verbose && console.timeEnd(`build ${canisterName}`);
216
233
  // deploy canister
@@ -1,5 +1,6 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
- import { resolve } from "node:path";
2
+ import { relative, resolve } from "node:path";
3
+ import chalk from "chalk";
3
4
  import { execa } from "execa";
4
5
  import { TextDocument } from "vscode-languageserver-textdocument";
5
6
  export function parseDiagnostics(stdout) {
@@ -110,6 +111,9 @@ function applyDiagnosticFixes(content, fixes) {
110
111
  const MAX_FIX_ITERATIONS = 10;
111
112
  export async function autofixMotoko(mocPath, files, mocArgs) {
112
113
  const fixedFilesCodes = new Map();
114
+ // Frozen migration files are often chmod'd read-only; warn once and skip
115
+ // them rather than aborting the whole run on EACCES/EPERM.
116
+ const readOnlySkipped = new Set();
113
117
  for (let iteration = 0; iteration < MAX_FIX_ITERATIONS; iteration++) {
114
118
  const fixesByFile = new Map();
115
119
  for (const file of files) {
@@ -126,12 +130,25 @@ export async function autofixMotoko(mocPath, files, mocArgs) {
126
130
  }
127
131
  let progress = false;
128
132
  for (const [file, fixes] of fixesByFile) {
133
+ if (readOnlySkipped.has(file)) {
134
+ continue;
135
+ }
129
136
  const original = await readFile(file, "utf-8");
130
137
  const { text: result, appliedCodes } = applyDiagnosticFixes(original, fixes);
131
138
  if (result === original) {
132
139
  continue;
133
140
  }
134
- await writeFile(file, result, "utf-8");
141
+ try {
142
+ await writeFile(file, result, "utf-8");
143
+ }
144
+ catch (err) {
145
+ if (err?.code === "EACCES" || err?.code === "EPERM") {
146
+ readOnlySkipped.add(file);
147
+ console.warn(chalk.yellow(`Skipped read-only file ${relative(process.cwd(), file)} (${appliedCodes.length} fix(es) not applied)`));
148
+ continue;
149
+ }
150
+ throw err;
151
+ }
135
152
  progress = true;
136
153
  const existing = fixedFilesCodes.get(file) ?? [];
137
154
  existing.push(...appliedCodes);
@@ -1,5 +1,5 @@
1
- import { PocketIc, PocketIcServer } from "pic-ic";
2
- import { PocketIc as PocketIcModern, PocketIcServer as PocketIcServerModern, type StartServerOptions } from "pic-js-mops";
1
+ import type { PocketIc, PocketIcServer } from "pic-ic";
2
+ import type { PocketIc as PocketIcModern, PocketIcServer as PocketIcServerModern, StartServerOptions } from "pic-js-mops";
3
3
  export type AnyPocketIcServer = PocketIcServer | PocketIcServerModern;
4
4
  export type AnyPocketIc = PocketIc | PocketIcModern;
5
5
  export type AnySetupCanister = PocketIc["setupCanister"] & PocketIcModern["setupCanister"];
@@ -1,18 +1,22 @@
1
1
  import semver from "semver";
2
- import { PocketIc, PocketIcServer } from "pic-ic";
3
- import { PocketIc as PocketIcModern, PocketIcServer as PocketIcServerModern, } from "pic-js-mops";
4
2
  import { readConfig } from "../mops.js";
5
3
  function isLegacy() {
6
4
  let version = readConfig().toolchain?.["pocket-ic"];
7
5
  return !!version && !!semver.valid(version) && semver.lt(version, "9.0.0");
8
6
  }
9
7
  export async function startPocketIc(options) {
8
+ // Imported lazily so commands that never start a replica don't load the
9
+ // PocketIC client. `pic-js-mops` ships ESM without `type: module`, which a
10
+ // static import fails to resolve under tsx (local dev); a dynamic import
11
+ // resolves it on every platform.
10
12
  if (isLegacy()) {
13
+ const { PocketIc, PocketIcServer } = await import("pic-ic");
11
14
  let server = await PocketIcServer.start(options);
12
15
  let client = await PocketIc.create(server.getUrl());
13
16
  return { server, client };
14
17
  }
15
- let server = await PocketIcServerModern.start(options);
16
- let client = await PocketIcModern.create(server.getUrl());
18
+ const { PocketIc, PocketIcServer } = await import("pic-js-mops");
19
+ let server = await PocketIcServer.start(options);
20
+ let client = await PocketIc.create(server.getUrl());
17
21
  return { server, client };
18
22
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.15.0",
3
+ "version": "2.15.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "bin/mops.js",
@@ -1,5 +1,5 @@
1
1
  import { beforeAll, describe, expect, test } from "@jest/globals";
2
- import { cpSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
2
+ import { chmodSync, cpSync, readdirSync, readFileSync, unlinkSync, } from "node:fs";
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
4
  import path from "path";
5
5
  import { lock } from "proper-lockfile";
@@ -19,7 +19,9 @@ describe("check --fix", () => {
19
19
  const diagnosticFlags = [warningFlags, "--error-format=json"];
20
20
  beforeAll(() => {
21
21
  for (const file of readdirSync(runDir).filter((f) => f.endsWith(".mo"))) {
22
- unlinkSync(path.join(runDir, file));
22
+ const p = path.join(runDir, file);
23
+ chmodSync(p, 0o644);
24
+ unlinkSync(p);
23
25
  }
24
26
  });
25
27
  function copyFixture(file) {
@@ -86,6 +88,16 @@ describe("check --fix", () => {
86
88
  expect(fixResult.stdout).toContain("1 fix applied");
87
89
  expect(readFileSync(runFilePath, "utf-8")).not.toContain("<Nat>");
88
90
  });
91
+ test("read-only file is skipped, not crashed", async () => {
92
+ const runFilePath = copyFixture("M0223.mo");
93
+ const before = readFileSync(runFilePath, "utf-8");
94
+ chmodSync(runFilePath, 0o444);
95
+ const result = await cli(["check", runFilePath, "--fix", "--", warningFlags], { cwd: fixDir });
96
+ expect(result.exitCode).toBe(0);
97
+ expect(result.stderr).toMatch(/Skipped read-only file/);
98
+ // File left untouched since the fix couldn't be written.
99
+ expect(readFileSync(runFilePath, "utf-8")).toBe(before);
100
+ });
89
101
  test("verbose", async () => {
90
102
  const result = await cli(["check", "Ok.mo", "--fix", "--verbose"], {
91
103
  cwd: fixDir,
@@ -1,5 +1,6 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
- import { resolve } from "node:path";
2
+ import { relative, resolve } from "node:path";
3
+ import chalk from "chalk";
3
4
  import { execa } from "execa";
4
5
  import { TextDocument } from "vscode-languageserver-textdocument";
5
6
 
@@ -186,6 +187,9 @@ export async function autofixMotoko(
186
187
  mocArgs: string[],
187
188
  ): Promise<AutofixResult | null> {
188
189
  const fixedFilesCodes = new Map<string, string[]>();
190
+ // Frozen migration files are often chmod'd read-only; warn once and skip
191
+ // them rather than aborting the whole run on EACCES/EPERM.
192
+ const readOnlySkipped = new Set<string>();
189
193
 
190
194
  for (let iteration = 0; iteration < MAX_FIX_ITERATIONS; iteration++) {
191
195
  const fixesByFile = new Map<string, DiagnosticFix[]>();
@@ -212,6 +216,10 @@ export async function autofixMotoko(
212
216
  let progress = false;
213
217
 
214
218
  for (const [file, fixes] of fixesByFile) {
219
+ if (readOnlySkipped.has(file)) {
220
+ continue;
221
+ }
222
+
215
223
  const original = await readFile(file, "utf-8");
216
224
  const { text: result, appliedCodes } = applyDiagnosticFixes(
217
225
  original,
@@ -222,7 +230,20 @@ export async function autofixMotoko(
222
230
  continue;
223
231
  }
224
232
 
225
- await writeFile(file, result, "utf-8");
233
+ try {
234
+ await writeFile(file, result, "utf-8");
235
+ } catch (err: any) {
236
+ if (err?.code === "EACCES" || err?.code === "EPERM") {
237
+ readOnlySkipped.add(file);
238
+ console.warn(
239
+ chalk.yellow(
240
+ `Skipped read-only file ${relative(process.cwd(), file)} (${appliedCodes.length} fix(es) not applied)`,
241
+ ),
242
+ );
243
+ continue;
244
+ }
245
+ throw err;
246
+ }
226
247
  progress = true;
227
248
 
228
249
  const existing = fixedFilesCodes.get(file) ?? [];
@@ -1,9 +1,9 @@
1
1
  import semver from "semver";
2
- import { PocketIc, PocketIcServer } from "pic-ic";
3
- import {
2
+ import type { PocketIc, PocketIcServer } from "pic-ic";
3
+ import type {
4
4
  PocketIc as PocketIcModern,
5
5
  PocketIcServer as PocketIcServerModern,
6
- type StartServerOptions,
6
+ StartServerOptions,
7
7
  } from "pic-js-mops";
8
8
  import { readConfig } from "../mops.js";
9
9
 
@@ -20,13 +20,19 @@ function isLegacy(): boolean {
20
20
  export async function startPocketIc(
21
21
  options: StartServerOptions,
22
22
  ): Promise<{ server: AnyPocketIcServer; client: AnyPocketIc }> {
23
+ // Imported lazily so commands that never start a replica don't load the
24
+ // PocketIC client. `pic-js-mops` ships ESM without `type: module`, which a
25
+ // static import fails to resolve under tsx (local dev); a dynamic import
26
+ // resolves it on every platform.
23
27
  if (isLegacy()) {
28
+ const { PocketIc, PocketIcServer } = await import("pic-ic");
24
29
  let server = await PocketIcServer.start(options);
25
30
  let client = await PocketIc.create(server.getUrl());
26
31
  return { server, client };
27
32
  }
28
33
 
29
- let server = await PocketIcServerModern.start(options);
30
- let client = await PocketIcModern.create(server.getUrl());
34
+ const { PocketIc, PocketIcServer } = await import("pic-js-mops");
35
+ let server = await PocketIcServer.start(options);
36
+ let client = await PocketIc.create(server.getUrl());
31
37
  return { server, client };
32
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.15.0",
3
+ "version": "2.15.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "dist/bin/mops.js",
@@ -1,5 +1,11 @@
1
1
  import { beforeAll, describe, expect, test } from "@jest/globals";
2
- import { cpSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
2
+ import {
3
+ chmodSync,
4
+ cpSync,
5
+ readdirSync,
6
+ readFileSync,
7
+ unlinkSync,
8
+ } from "node:fs";
3
9
  import { mkdir, writeFile } from "node:fs/promises";
4
10
  import path from "path";
5
11
  import { lock } from "proper-lockfile";
@@ -22,7 +28,9 @@ describe("check --fix", () => {
22
28
 
23
29
  beforeAll(() => {
24
30
  for (const file of readdirSync(runDir).filter((f) => f.endsWith(".mo"))) {
25
- unlinkSync(path.join(runDir, file));
31
+ const p = path.join(runDir, file);
32
+ chmodSync(p, 0o644);
33
+ unlinkSync(p);
26
34
  }
27
35
  });
28
36
 
@@ -132,6 +140,22 @@ describe("check --fix", () => {
132
140
  expect(readFileSync(runFilePath, "utf-8")).not.toContain("<Nat>");
133
141
  });
134
142
 
143
+ test("read-only file is skipped, not crashed", async () => {
144
+ const runFilePath = copyFixture("M0223.mo");
145
+ const before = readFileSync(runFilePath, "utf-8");
146
+ chmodSync(runFilePath, 0o444);
147
+
148
+ const result = await cli(
149
+ ["check", runFilePath, "--fix", "--", warningFlags],
150
+ { cwd: fixDir },
151
+ );
152
+
153
+ expect(result.exitCode).toBe(0);
154
+ expect(result.stderr).toMatch(/Skipped read-only file/);
155
+ // File left untouched since the fix couldn't be written.
156
+ expect(readFileSync(runFilePath, "utf-8")).toBe(before);
157
+ });
158
+
135
159
  test("verbose", async () => {
136
160
  const result = await cli(["check", "Ok.mo", "--fix", "--verbose"], {
137
161
  cwd: fixDir,