ic-mops 2.13.2 → 2.14.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 (70) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bun.lock +24 -12
  3. package/bundle/cli.tgz +0 -0
  4. package/cache.ts +101 -20
  5. package/cli.ts +40 -14
  6. package/commands/available-updates.ts +4 -2
  7. package/commands/bench.ts +3 -0
  8. package/commands/check.ts +33 -22
  9. package/commands/init.ts +15 -2
  10. package/commands/install/install-mops-dep.ts +14 -9
  11. package/commands/install/sync-local-cache.ts +7 -1
  12. package/commands/lint.ts +11 -0
  13. package/commands/outdated.ts +5 -2
  14. package/commands/publish.ts +0 -1
  15. package/commands/test/test.ts +14 -0
  16. package/commands/update.ts +3 -2
  17. package/commands/watch/tester.ts +5 -1
  18. package/dist/cache.d.ts +3 -0
  19. package/dist/cache.js +98 -18
  20. package/dist/cli.js +16 -12
  21. package/dist/commands/available-updates.d.ts +1 -1
  22. package/dist/commands/available-updates.js +4 -1
  23. package/dist/commands/bench.js +2 -0
  24. package/dist/commands/check.js +27 -18
  25. package/dist/commands/init.js +9 -2
  26. package/dist/commands/install/install-mops-dep.js +12 -9
  27. package/dist/commands/install/sync-local-cache.js +2 -1
  28. package/dist/commands/lint.js +7 -0
  29. package/dist/commands/outdated.d.ts +2 -1
  30. package/dist/commands/outdated.js +2 -2
  31. package/dist/commands/publish.js +0 -1
  32. package/dist/commands/test/test.d.ts +1 -1
  33. package/dist/commands/test/test.js +10 -5
  34. package/dist/commands/update.d.ts +2 -1
  35. package/dist/commands/update.js +2 -2
  36. package/dist/commands/watch/tester.js +4 -1
  37. package/dist/helpers/autofix-motoko.d.ts +1 -1
  38. package/dist/helpers/autofix-motoko.js +10 -8
  39. package/dist/helpers/deprecate-dfx-replica.d.ts +2 -0
  40. package/dist/helpers/deprecate-dfx-replica.js +20 -0
  41. package/dist/helpers/fix-lock.d.ts +1 -0
  42. package/dist/helpers/fix-lock.js +93 -0
  43. package/dist/integrity.d.ts +2 -2
  44. package/dist/integrity.js +22 -6
  45. package/dist/mops.js +1 -1
  46. package/dist/package.json +3 -3
  47. package/dist/tests/check-fix.test.js +40 -0
  48. package/dist/tests/check.test.js +6 -0
  49. package/dist/tests/cli.test.js +136 -1
  50. package/dist/vessel.js +21 -13
  51. package/dist/wasm/pkg/nodejs/package.json +1 -1
  52. package/dist/wasm/pkg/nodejs/wasm_bg.wasm +0 -0
  53. package/dist/wasm/pkg/web/package.json +1 -1
  54. package/dist/wasm/pkg/web/wasm_bg.wasm +0 -0
  55. package/helpers/autofix-motoko.ts +16 -14
  56. package/helpers/deprecate-dfx-replica.ts +32 -0
  57. package/helpers/fix-lock.ts +101 -0
  58. package/integrity.ts +30 -9
  59. package/mops.ts +1 -1
  60. package/package.json +3 -3
  61. package/tests/__snapshots__/check.test.ts.snap +17 -4
  62. package/tests/check-fix.test.ts +46 -0
  63. package/tests/check.test.ts +11 -0
  64. package/tests/cli.test.ts +180 -1
  65. package/tests/install/update-bound-patch/mops.toml +2 -0
  66. package/vessel.ts +31 -14
  67. package/wasm/pkg/nodejs/package.json +1 -1
  68. package/wasm/pkg/nodejs/wasm_bg.wasm +0 -0
  69. package/wasm/pkg/web/package.json +1 -1
  70. package/wasm/pkg/web/wasm_bg.wasm +0 -0
@@ -4,12 +4,11 @@ import path from "node:path";
4
4
  import { Buffer } from "node:buffer";
5
5
  import { createLogUpdate } from "log-update";
6
6
  import chalk from "chalk";
7
- import { deleteSync } from "del";
8
7
  import { checkConfigFile, progressBar, readConfig } from "../../mops.js";
9
8
  import { getHighestVersion } from "../../api/getHighestVersion.js";
10
9
  import { storageActor } from "../../api/actors.js";
11
10
  import { parallel } from "../../parallel.js";
12
- import { getDepCacheDir, getMopsDepCacheName, isDepCached, } from "../../cache.js";
11
+ import { commitStagingDir, createStagingDir, getDepCacheDir, getMopsDepCacheName, isDepCached, sweepStaleStagingDirs, } from "../../cache.js";
13
12
  import { downloadFile, getPackageFilesInfo, } from "../../api/downloadPackageFiles.js";
14
13
  import { installDeps } from "./install-deps.js";
15
14
  import { getDepName } from "../../helpers/get-dep-name.js";
@@ -37,6 +36,7 @@ export async function installMopsDep(pkg, version = "", { verbose, silent, dep,
37
36
  }
38
37
  version = versionRes.ok;
39
38
  }
39
+ sweepStaleStagingDirs();
40
40
  let cacheName = getMopsDepCacheName(depName, version);
41
41
  let cacheDir = getDepCacheDir(cacheName);
42
42
  // global cache hit
@@ -60,24 +60,27 @@ export async function installMopsDep(pkg, version = "", { verbose, silent, dep,
60
60
  filesData.set(path, data);
61
61
  progress();
62
62
  });
63
+ let stagingDir = createStagingDir(cacheDir);
63
64
  let onSigInt = () => {
64
- deleteSync([cacheDir], { force: true });
65
- process.exit();
65
+ fs.rmSync(stagingDir, { recursive: true, force: true });
66
+ process.exit(130);
66
67
  };
67
68
  process.on("SIGINT", onSigInt);
68
- // write files to global cache
69
69
  try {
70
70
  await Promise.all(Array.from(filesData.entries()).map(async ([filePath, data]) => {
71
- await fs.promises.mkdir(path.join(cacheDir, path.dirname(filePath)), { recursive: true });
72
- await fs.promises.writeFile(path.join(cacheDir, filePath), Buffer.from(data));
71
+ await fs.promises.mkdir(path.join(stagingDir, path.dirname(filePath)), { recursive: true });
72
+ await fs.promises.writeFile(path.join(stagingDir, filePath), Buffer.from(data));
73
73
  }));
74
+ commitStagingDir(stagingDir, cacheDir);
74
75
  }
75
76
  catch (err) {
76
77
  console.error(chalk.red("Error: ") + err);
77
- deleteSync([cacheDir], { force: true });
78
+ fs.rmSync(stagingDir, { recursive: true, force: true });
78
79
  return false;
79
80
  }
80
- process.off("SIGINT", onSigInt);
81
+ finally {
82
+ process.off("SIGINT", onSigInt);
83
+ }
81
84
  }
82
85
  catch (err) {
83
86
  console.error(chalk.red("Error: ") + err);
@@ -1,9 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { copyCache, getDepCacheName } from "../../cache.js";
3
+ import { copyCache, getDepCacheName, sweepStaleStagingDirs, } from "../../cache.js";
4
4
  import { getDependencyType, getRootDir } from "../../mops.js";
5
5
  import { resolvePackages } from "../../resolve-packages.js";
6
6
  export async function syncLocalCache({ verbose = false } = {}) {
7
+ sweepStaleStagingDirs();
7
8
  let resolvedPackages = await resolvePackages();
8
9
  let rootDir = getRootDir();
9
10
  verbose && console.log("Syncing local cache...");
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { cliError } from "../error.js";
6
6
  import { formatDir, formatGithubDir, getDependencyType, getRootDir, readConfig, } from "../mops.js";
7
7
  import { resolvePackages } from "../resolve-packages.js";
8
+ import { withFixLock } from "../helpers/fix-lock.js";
8
9
  import { toolchain } from "./toolchain/index.js";
9
10
  import { MOTOKO_GLOB_CONFIG } from "../constants.js";
10
11
  import { existsSync } from "node:fs";
@@ -109,6 +110,12 @@ async function runLintoko(lintokoBinPath, rootDir, args, options, label) {
109
110
  }
110
111
  }
111
112
  export async function lint(filter, options) {
113
+ if (options.fix) {
114
+ return withFixLock(() => lintImpl(filter, options));
115
+ }
116
+ return lintImpl(filter, options);
117
+ }
118
+ async function lintImpl(filter, options) {
112
119
  let config = readConfig();
113
120
  let rootDir = getRootDir();
114
121
  let lintokoBinPath = config.toolchain?.lintoko
@@ -1,3 +1,4 @@
1
- export declare function outdated({ major }?: {
1
+ export declare function outdated({ major, patch, }?: {
2
2
  major?: boolean;
3
+ patch?: boolean;
3
4
  }): Promise<void>;
@@ -2,12 +2,12 @@ import chalk from "chalk";
2
2
  import { checkConfigFile, readConfig } from "../mops.js";
3
3
  import { getAvailableUpdates } from "./available-updates.js";
4
4
  import { getDepName, getDepPinnedVersion } from "../helpers/get-dep-name.js";
5
- export async function outdated({ major } = {}) {
5
+ export async function outdated({ major, patch, } = {}) {
6
6
  if (!checkConfigFile()) {
7
7
  return;
8
8
  }
9
9
  let config = readConfig();
10
- let available = await getAvailableUpdates(config, undefined, major ? "major" : "caret");
10
+ let available = await getAvailableUpdates(config, undefined, major ? "major" : patch ? "patch" : "caret");
11
11
  if (available.length === 0) {
12
12
  console.log(chalk.green("All dependencies are up to date!"));
13
13
  }
@@ -256,7 +256,6 @@ export async function publish(options = {}) {
256
256
  console.log("Running benchmarks...");
257
257
  try {
258
258
  benchmarks = await bench("", {
259
- replica: config.toolchain?.["pocket-ic"] ? "pocket-ic" : "dfx",
260
259
  gc: "copying",
261
260
  forceGc: true,
262
261
  silent: true,
@@ -10,5 +10,5 @@ type TestOptions = {
10
10
  verbose: boolean;
11
11
  };
12
12
  export declare function test(filter?: string, options?: Partial<TestOptions>): Promise<void>;
13
- export declare function testWithReporter(reporterName: ReporterName | Reporter | undefined, filter: string | undefined, defaultMode: TestMode | undefined, replicaType: ReplicaName, watch?: boolean, signal?: AbortSignal): Promise<boolean>;
13
+ export declare function testWithReporter(reporterName: ReporterName | Reporter | undefined, filter: string | undefined, defaultMode: TestMode | undefined, replicaType: ReplicaName, watch?: boolean, signal?: AbortSignal, explicitReplica?: boolean): Promise<boolean>;
14
14
  export {};
@@ -21,6 +21,7 @@ import { SilentReporter } from "./reporters/silent-reporter.js";
21
21
  import { toolchain } from "../toolchain/index.js";
22
22
  import { Replica } from "../replica.js";
23
23
  import { getDfxVersion } from "../../helpers/get-dfx-version.js";
24
+ import { warnIfDfxReplica } from "../../helpers/deprecate-dfx-replica.js";
24
25
  import { MOTOKO_GLOB_CONFIG, MOTOKO_IGNORE_PATTERNS } from "../../constants.js";
25
26
  let replica = new Replica();
26
27
  let replicaStartPromise;
@@ -47,6 +48,7 @@ export async function test(filter = "", options = {}) {
47
48
  replicaType = "dfx-pocket-ic";
48
49
  }
49
50
  }
51
+ let explicitReplica = options.replica === "dfx";
50
52
  replica.type = replicaType;
51
53
  replica.verbose = !!options.verbose;
52
54
  if (options.watch) {
@@ -78,7 +80,7 @@ export async function test(filter = "", options = {}) {
78
80
  console.clear();
79
81
  process.stdout.write("\x1Bc");
80
82
  controller = new AbortController();
81
- curRun = runAll(options.reporter, filter, options.mode, replicaType, true, controller.signal);
83
+ curRun = runAll(options.reporter, filter, options.mode, replicaType, true, controller.signal, explicitReplica);
82
84
  await curRun;
83
85
  console.log("-".repeat(50));
84
86
  console.log("Waiting for file changes...");
@@ -94,7 +96,7 @@ export async function test(filter = "", options = {}) {
94
96
  run();
95
97
  }
96
98
  else {
97
- let passed = await runAll(options.reporter, filter, options.mode, replicaType);
99
+ let passed = await runAll(options.reporter, filter, options.mode, replicaType, false, undefined, explicitReplica);
98
100
  if (!passed) {
99
101
  process.exit(1);
100
102
  }
@@ -102,11 +104,11 @@ export async function test(filter = "", options = {}) {
102
104
  }
103
105
  let mocPath = "";
104
106
  let wasmtimePath = "";
105
- async function runAll(reporterName, filter = "", mode = "interpreter", replicaType, watch = false, signal) {
106
- let done = await testWithReporter(reporterName, filter, mode, replicaType, watch, signal);
107
+ async function runAll(reporterName, filter = "", mode = "interpreter", replicaType, watch = false, signal, explicitReplica = false) {
108
+ let done = await testWithReporter(reporterName, filter, mode, replicaType, watch, signal, explicitReplica);
107
109
  return done;
108
110
  }
109
- export async function testWithReporter(reporterName, filter = "", defaultMode = "interpreter", replicaType, watch = false, signal) {
111
+ export async function testWithReporter(reporterName, filter = "", defaultMode = "interpreter", replicaType, watch = false, signal, explicitReplica = false) {
110
112
  let rootDir = getRootDir();
111
113
  let files = [];
112
114
  let libFiles = globSync("**/test?(s)/lib.mo", MOTOKO_GLOB_CONFIG);
@@ -174,6 +176,9 @@ export async function testWithReporter(reporterName, filter = "", defaultMode =
174
176
  });
175
177
  let hasWasiTests = filesWithMode.some(({ mode }) => mode === "wasi");
176
178
  let hasReplicaTests = filesWithMode.some(({ mode }) => mode === "replica");
179
+ if (hasReplicaTests) {
180
+ warnIfDfxReplica(replicaType, explicitReplica);
181
+ }
177
182
  // prepare wasmtime path
178
183
  if (hasWasiTests && !wasmtimePath) {
179
184
  // ensure wasmtime is installed or specified in config
@@ -3,6 +3,7 @@ type UpdateOptions = {
3
3
  dev?: boolean;
4
4
  lock?: "update" | "ignore";
5
5
  major?: boolean;
6
+ patch?: boolean;
6
7
  };
7
- export declare function update(pkg?: string, { lock, major }?: UpdateOptions): Promise<void>;
8
+ export declare function update(pkg?: string, { lock, major, patch }?: UpdateOptions): Promise<void>;
8
9
  export {};
@@ -4,7 +4,7 @@ import { add } from "./add.js";
4
4
  import { getAvailableUpdates } from "./available-updates.js";
5
5
  import { checkIntegrity } from "../integrity.js";
6
6
  import { getDepName, getDepPinnedVersion } from "../helpers/get-dep-name.js";
7
- export async function update(pkg, { lock, major } = {}) {
7
+ export async function update(pkg, { lock, major, patch } = {}) {
8
8
  if (!checkConfigFile()) {
9
9
  return;
10
10
  }
@@ -36,7 +36,7 @@ export async function update(pkg, { lock, major } = {}) {
36
36
  }
37
37
  }
38
38
  // update mops packages
39
- let available = await getAvailableUpdates(config, pkg, major ? "major" : "caret");
39
+ let available = await getAvailableUpdates(config, pkg, major ? "major" : patch ? "patch" : "caret");
40
40
  if (available.length === 0) {
41
41
  if (pkg) {
42
42
  console.log(chalk.green(`Package "${pkg}" is up to date!`));
@@ -36,7 +36,10 @@ export class Tester {
36
36
  this.reporter = new SilentReporter(false, onProgress);
37
37
  this.controller = new AbortController();
38
38
  let config = readConfig();
39
- this.currentRun = testWithReporter(this.reporter, "", "interpreter", config.toolchain?.["pocket-ic"] ? "pocket-ic" : "dfx", true, this.controller.signal);
39
+ let replicaType = config.toolchain?.["pocket-ic"]
40
+ ? "pocket-ic"
41
+ : "dfx";
42
+ this.currentRun = testWithReporter(this.reporter, "", "interpreter", replicaType, true, this.controller.signal);
40
43
  await this.currentRun;
41
44
  if (!this.aborted) {
42
45
  this.status = this.reporter.failed > 0 ? "error" : "success";
@@ -16,7 +16,7 @@ export interface MocDiagnostic {
16
16
  spans: MocSpan[];
17
17
  notes: string[];
18
18
  }
19
- export declare function parseDiagnostics(stdout: string): MocDiagnostic[];
19
+ export declare function parseDiagnostics(stdout: string | undefined): MocDiagnostic[];
20
20
  export interface AutofixResult {
21
21
  /** Map of file path → diagnostic codes fixed in that file */
22
22
  fixedFiles: Map<string, string[]>;
@@ -3,6 +3,9 @@ import { resolve } from "node:path";
3
3
  import { execa } from "execa";
4
4
  import { TextDocument, } from "vscode-languageserver-textdocument";
5
5
  export function parseDiagnostics(stdout) {
6
+ if (!stdout) {
7
+ return [];
8
+ }
6
9
  return stdout
7
10
  .split("\n")
8
11
  .filter((l) => l.trim())
@@ -106,14 +109,13 @@ export async function autofixMotoko(mocPath, files, mocArgs) {
106
109
  const fixedFilesCodes = new Map();
107
110
  for (let iteration = 0; iteration < MAX_FIX_ITERATIONS; iteration++) {
108
111
  const fixesByFile = new Map();
109
- for (const file of files) {
110
- const result = await execa(mocPath, [file, ...mocArgs, "--error-format=json"], { stdio: "pipe", reject: false });
111
- const diagnostics = parseDiagnostics(result.stdout);
112
- for (const [targetFile, fixes] of extractDiagnosticFixes(diagnostics)) {
113
- const existing = fixesByFile.get(targetFile) ?? [];
114
- existing.push(...fixes);
115
- fixesByFile.set(targetFile, existing);
116
- }
112
+ // Single invocation: moc dedups shared imports across all files.
113
+ const result = await execa(mocPath, [...files, ...mocArgs, "--error-format=json"], { stdio: "pipe", reject: false });
114
+ const diagnostics = parseDiagnostics(result.stdout);
115
+ for (const [targetFile, fixes] of extractDiagnosticFixes(diagnostics)) {
116
+ const existing = fixesByFile.get(targetFile) ?? [];
117
+ existing.push(...fixes);
118
+ fixesByFile.set(targetFile, existing);
117
119
  }
118
120
  if (fixesByFile.size === 0) {
119
121
  break;
@@ -0,0 +1,2 @@
1
+ export type ReplicaName = "dfx" | "pocket-ic" | "dfx-pocket-ic";
2
+ export declare function warnIfDfxReplica(replicaType: ReplicaName, explicit: boolean): void;
@@ -0,0 +1,20 @@
1
+ import chalk from "chalk";
2
+ let alreadyWarned = false;
3
+ // Prints a deprecation warning (once per process) when `mops bench`/`mops test`/
4
+ // `mops watch` is about to use a dfx-backed replica. Removal is tracked in
5
+ // NEXT-MAJOR.md under "Drop dfx coupling".
6
+ export function warnIfDfxReplica(replicaType, explicit) {
7
+ if (alreadyWarned) {
8
+ return;
9
+ }
10
+ if (replicaType !== "dfx" && replicaType !== "dfx-pocket-ic") {
11
+ return;
12
+ }
13
+ alreadyWarned = true;
14
+ let lead = explicit && replicaType === "dfx"
15
+ ? "`--replica dfx` is deprecated and will be removed in a future release."
16
+ : replicaType === "dfx-pocket-ic"
17
+ ? "Falling back to dfx-bundled PocketIC because no `pocket-ic` version is set in `[toolchain]`. This fallback is deprecated and will be removed in a future release."
18
+ : "Using `dfx` replica because no `pocket-ic` version is set in `[toolchain]`. The `dfx` replica is deprecated and will be removed in a future release.";
19
+ console.log(chalk.yellow(`${lead}\nRun \`mops toolchain use pocket-ic 12.0.0\` to pin a PocketIC version and silence this warning.`));
20
+ }
@@ -0,0 +1 @@
1
+ export declare function withFixLock<T>(fn: () => Promise<T>): Promise<T>;
@@ -0,0 +1,93 @@
1
+ import chalk from "chalk";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { lock, unlockSync } from "proper-lockfile";
5
+ import { getRootDir } from "../mops.js";
6
+ // Serializes Motoko-source-writing commands (`check --fix`, `lint --fix`)
7
+ // across concurrent `mops` invocations in the same project. Two parallel
8
+ // `--fix` runs can otherwise apply stale moc byte offsets to a sibling's
9
+ // already-mutated file, corrupting source. Cargo-style: print a wait
10
+ // message after the first failed acquire, then retry with backoff.
11
+ //
12
+ // Re-entrant within a single process so `mops check --fix` (which calls
13
+ // `mops lint --fix` internally) doesn't deadlock against itself.
14
+ let depth = 0;
15
+ let acquiring = false;
16
+ export async function withFixLock(fn) {
17
+ if (depth > 0) {
18
+ depth++;
19
+ try {
20
+ return await fn();
21
+ }
22
+ finally {
23
+ depth--;
24
+ }
25
+ }
26
+ // Sequential re-entry is handled by `depth`; concurrent top-level calls
27
+ // (`Promise.all([withFixLock(a), withFixLock(b)])`) would otherwise
28
+ // serialize awkwardly via proper-lockfile's ELOCKED retry path — with a
29
+ // confusing intra-process "Waiting..." log line. Fail loudly instead so
30
+ // the next caller finds out at the call site. Must be set synchronously
31
+ // before any await, so concurrent entries can't both pass the check.
32
+ if (acquiring) {
33
+ throw new Error("withFixLock cannot be called concurrently within the same process");
34
+ }
35
+ acquiring = true;
36
+ try {
37
+ const rootDir = getRootDir();
38
+ if (!rootDir) {
39
+ return await fn();
40
+ }
41
+ const lockDir = join(rootDir, ".mops");
42
+ await mkdir(lockDir, { recursive: true });
43
+ const lockTarget = join(lockDir, "fix.lock");
44
+ await writeFile(lockTarget, "", { flag: "a" });
45
+ const stale = 300_000;
46
+ let release;
47
+ try {
48
+ release = await lock(lockTarget, { stale, retries: 0 });
49
+ }
50
+ catch (err) {
51
+ if (err?.code !== "ELOCKED") {
52
+ throw err;
53
+ }
54
+ console.log(chalk.gray("Waiting for another `mops --fix` run to finish..."));
55
+ try {
56
+ release = await lock(lockTarget, {
57
+ stale,
58
+ retries: { retries: 240, minTimeout: 250, maxTimeout: 2_000 },
59
+ });
60
+ }
61
+ catch (err2) {
62
+ // proper-lockfile creates `<lockTarget>.lock/` (a directory) for the actual lock,
63
+ // so manual recovery means removing that, not the empty marker file.
64
+ throw new Error(`Failed to acquire mops fix lock at ${lockTarget} — another --fix process may be stuck. Remove the ${lockTarget}.lock directory to recover.${err2?.message ? `\n${err2.message}` : ""}`);
65
+ }
66
+ }
67
+ // proper-lockfile registers its own signal-exit handler, but it doesn't reliably
68
+ // fire on process.exit(). This manual handler covers that gap. Double-unlock is
69
+ // harmless (the second call throws and is caught).
70
+ const exitCleanup = () => {
71
+ try {
72
+ unlockSync(lockTarget);
73
+ }
74
+ catch { }
75
+ };
76
+ process.on("exit", exitCleanup);
77
+ depth = 1;
78
+ try {
79
+ return await fn();
80
+ }
81
+ finally {
82
+ depth = 0;
83
+ process.removeListener("exit", exitCleanup);
84
+ try {
85
+ await release();
86
+ }
87
+ catch { }
88
+ }
89
+ }
90
+ finally {
91
+ acquiring = false;
92
+ }
93
+ }
@@ -22,6 +22,6 @@ export declare function readLockFile(): LockFile | null;
22
22
  export declare function checkLockFileLight(): boolean;
23
23
  export declare function updateLockFile({ force, }?: {
24
24
  force?: boolean;
25
- }): Promise<void>;
26
- export declare function checkLockFile(force?: boolean): Promise<void>;
25
+ }): Promise<boolean>;
26
+ export declare function checkLockFile(force?: boolean, regenerated?: boolean): Promise<void>;
27
27
  export {};
package/dist/integrity.js CHANGED
@@ -13,8 +13,8 @@ export async function checkIntegrity(lock) {
13
13
  lock = process.env["CI"] ? "check" : "update";
14
14
  }
15
15
  if (lock === "update") {
16
- await updateLockFile({ force });
17
- await checkLockFile(force);
16
+ let regenerated = await updateLockFile({ force });
17
+ await checkLockFile(force, regenerated);
18
18
  }
19
19
  else if (lock === "check") {
20
20
  await checkLockFile(force);
@@ -108,12 +108,14 @@ export function checkLockFileLight() {
108
108
  }
109
109
  return false;
110
110
  }
111
+ // returns true if the lock file was (re)written, false if it was skipped
112
+ // because the existing lock is still valid.
111
113
  export async function updateLockFile({ force = false, } = {}) {
112
114
  // if lock file exists and mops.toml hasn't changed, don't update it
113
115
  // (unless forced: `--lock update` must unconditionally regenerate so users
114
116
  // can recover from a corrupt lockfile without `rm mops.lock`)
115
117
  if (!force && checkLockFileLight()) {
116
- return;
118
+ return false;
117
119
  }
118
120
  let resolvedDeps = await resolvePackages();
119
121
  let fileHashes = await getFileHashesFromRegistry();
@@ -138,9 +140,12 @@ export async function updateLockFile({ force = false, } = {}) {
138
140
  console.log(" Applications: commit this file.");
139
141
  console.log(" Libraries: add mops.lock to .gitignore.");
140
142
  }
143
+ return true;
141
144
  }
142
145
  // compare hashes of local files with hashes from the lock file
143
- export async function checkLockFile(force = false) {
146
+ // `regenerated` indicates the lockfile was just rewritten from the registry
147
+ // (via `updateLockFile`), so any remaining hash mismatch must be a local edit.
148
+ export async function checkLockFile(force = false, regenerated = false) {
144
149
  let supportedVersions = [1, 2, 3];
145
150
  let rootDir = getRootDir();
146
151
  let lockFile = path.join(rootDir, "mops.lock");
@@ -231,8 +236,19 @@ export async function checkLockFile(force = false) {
231
236
  console.error(`Locked hash: ${lockedHash}`);
232
237
  console.error(`Actual hash: ${localHash}`);
233
238
  console.error("");
234
- console.error("If you have not modified files under .mops/, your lockfile may be stale or corrupt.");
235
- console.error("Run `mops install --lock update` to regenerate it.");
239
+ if (regenerated) {
240
+ // The lock was just rewritten from the registry, so the only way
241
+ // for a per-file hash to still differ is that .mops/<file> was
242
+ // edited locally. Point users at the actual fix.
243
+ let pkgDir = fileId.split("/")[0];
244
+ console.error(`.mops/${fileId} differs from the registry — your local copy has been modified.`);
245
+ console.error(`To restore from the global cache, delete the \`.mops/${pkgDir}\` directory and run \`mops install\`.`);
246
+ console.error("To keep custom changes, use a `repo` or `path` entry in mops.toml instead of editing .mops/ directly.");
247
+ }
248
+ else {
249
+ console.error("If you have not modified files under .mops/, your lockfile may be stale or corrupt.");
250
+ console.error("Run `mops install --lock update` to regenerate it.");
251
+ }
236
252
  process.exit(1);
237
253
  }
238
254
  }
package/dist/mops.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import process from "node:process";
2
2
  import path from "node:path";
3
3
  import fs from "node:fs";
4
- import TOML from "@iarna/toml";
4
+ import TOML from "smol-toml";
5
5
  import chalk from "chalk";
6
6
  import prompts from "prompts";
7
7
  import { decodeFile } from "./pem.js";
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.13.2",
3
+ "version": "2.14.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "bin/mops.js",
@@ -29,7 +29,7 @@
29
29
  "node": ">=18.0.0"
30
30
  },
31
31
  "dependencies": {
32
- "@iarna/toml": "2.2.5",
32
+ "smol-toml": "1.6.1",
33
33
  "@icp-sdk/core": "4.0.2",
34
34
  "@noble/hashes": "1.8.0",
35
35
  "as-table": "1.0.55",
@@ -93,6 +93,6 @@
93
93
  "ts-jest": "29.4.5",
94
94
  "tsx": "4.19.2",
95
95
  "typescript": "5.9.2",
96
- "wasm-pack": "0.13.1"
96
+ "wasm-pack": "0.15.0"
97
97
  }
98
98
  }
@@ -1,6 +1,8 @@
1
1
  import { beforeAll, describe, expect, test } from "@jest/globals";
2
2
  import { cpSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
3
+ import { mkdir, writeFile } from "node:fs/promises";
3
4
  import path from "path";
5
+ import { lock } from "proper-lockfile";
4
6
  import { parseDiagnostics } from "../helpers/autofix-motoko";
5
7
  import { cli, normalizePaths } from "./helpers";
6
8
  function countCodes(stdout) {
@@ -36,6 +38,12 @@ describe("check --fix", () => {
36
38
  expect(countCodes(afterResult.stdout)).toEqual(expectedAfterDiagnostics);
37
39
  return runFilePath;
38
40
  }
41
+ test("parseDiagnostics tolerates missing moc output", () => {
42
+ // execa with reject:false yields undefined stdout when moc fails to spawn
43
+ // or is killed (e.g. OOM); parsing must degrade to no diagnostics, not throw.
44
+ expect(parseDiagnostics(undefined)).toEqual([]);
45
+ expect(parseDiagnostics("")).toEqual([]);
46
+ });
39
47
  test("M0223", async () => {
40
48
  await testCheckFix("M0223.mo", { M0223: 1 });
41
49
  });
@@ -103,4 +111,36 @@ describe("check --fix", () => {
103
111
  expect(result.stderr).toMatch(/error/i);
104
112
  expect(result.stdout).not.toMatch(/✓ run/);
105
113
  });
114
+ test("concurrent --fix runs serialize and produce the same output", async () => {
115
+ const runFilePath = copyFixture("edit-suggestions.mo");
116
+ await cli(["check", runFilePath, "--fix", "--", warningFlags], {
117
+ cwd: fixDir,
118
+ });
119
+ const expected = readFileSync(runFilePath, "utf-8");
120
+ copyFixture("edit-suggestions.mo");
121
+ // Hold the lock from the test process itself so both children
122
+ // deterministically hit ELOCKED, print "Waiting...", and queue.
123
+ const lockTarget = path.join(fixDir, ".mops", "fix.lock");
124
+ await mkdir(path.dirname(lockTarget), { recursive: true });
125
+ await writeFile(lockTarget, "", { flag: "a" });
126
+ const release = await lock(lockTarget, { stale: 30_000 });
127
+ const childA = cli(["check", runFilePath, "--fix", "--", warningFlags], {
128
+ cwd: fixDir,
129
+ });
130
+ const childB = cli(["check", runFilePath, "--fix", "--", warningFlags], {
131
+ cwd: fixDir,
132
+ });
133
+ // Hold long enough for both children to spawn (`npm run mops` → bundler →
134
+ // CLI startup) and attempt the non-blocking acquire. 5s is conservative —
135
+ // local runs land well under 2s, the headroom is purely to absorb CI jitter.
136
+ await new Promise((r) => setTimeout(r, 5000));
137
+ await release();
138
+ const [a, b] = await Promise.all([childA, childB]);
139
+ expect(a.exitCode).toBe(0);
140
+ expect(b.exitCode).toBe(0);
141
+ expect(readFileSync(runFilePath, "utf-8")).toBe(expected);
142
+ // At least one child must have hit the held lock and queued; if neither
143
+ // did, the lock didn't actually serialize anything.
144
+ expect(a.stdout + b.stdout).toContain("Waiting for another");
145
+ });
106
146
  });
@@ -12,6 +12,12 @@ describe("check", () => {
12
12
  await cliSnapshot(["check", "Error.mo"], { cwd }, 1);
13
13
  await cliSnapshot(["check", "Ok.mo", "Error.mo"], { cwd }, 1);
14
14
  });
15
+ // The verbose snapshot shows a single "moc ... [both files]" line, proving the
16
+ // whole set is checked in one invocation rather than one moc call per file.
17
+ test("multiple files in a single invocation", async () => {
18
+ const cwd = path.join(import.meta.dirname, "check/success");
19
+ await cliSnapshot(["check", "Ok.mo", "Warning.mo", "--verbose"], { cwd }, 0);
20
+ });
15
21
  test("warning", async () => {
16
22
  const cwd = path.join(import.meta.dirname, "check/success");
17
23
  const result = await cliSnapshot(["check", "Warning.mo"], { cwd }, 0);