ic-mops 2.12.0 → 2.12.2

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,12 @@
2
2
 
3
3
  ## Next
4
4
 
5
+ ## 2.12.2
6
+ - Fix `mops install` (and any `--lock check` flow) failing with "Mismatched number of resolved packages" when a project's resolved dependencies include multiple aliases (e.g. `base`, `base@0`, `base@0.16`) that pin to the same `name@version`
7
+
8
+ ## 2.12.1
9
+ - `mops check`/`build`/`check-stable` skip migration staging when only the pending `next` migration is needed, so `moc` diagnostics reference the real `next-migration/<file>` path.
10
+
5
11
  ## 2.12.0
6
12
  - Migration staging directory moved from `.mops/.migrations/<canister>/` to `<parent-of-chain>/.migrations-<canister>/`, so migration files can import shared modules from sibling folders (e.g. a `types/` folder next to `migrations/`) — relative imports now resolve to the same target whether moc reads the original chain dir or the staged one. The staged dir self-stamps a `.gitignore` so it doesn't pollute `git status`; `mops init` now also adds `.migrations-*/` to the project `.gitignore`
7
13
  - `[canisters.<name>.migrations]` now requires `chain` and `next` to share the same parent directory (any layout where the parents differed is rejected with a clear error). The default layout `chain = "migrations"` + `next = "next-migration"` already satisfies this. For per-canister setups, use sibling subdirectories, e.g. `chain = "src/backend/migrations"` + `next = "src/backend/next-migration"`
package/bundle/cli.tgz CHANGED
Binary file
@@ -98,6 +98,15 @@ export async function prepareMigrationArgs(migrations, canisterName, mode, verbo
98
98
  cleanup: async () => { },
99
99
  };
100
100
  }
101
+ // Shortcut: when only the pending next migration is needed (empty chain or
102
+ // trimmed to 1), point moc at next-migration/ so diagnostics use the real path.
103
+ if (nextFile && nextDir && (chainFiles.length === 0 || limit === 1)) {
104
+ const migrationArgs = [`--enhanced-migration=${nextDir}`];
105
+ if (isTrimming) {
106
+ migrationArgs.push("-A=M0254");
107
+ }
108
+ return { migrationArgs, cleanup: async () => { } };
109
+ }
101
110
  const tempDir = stagedMigrationsDir(chainDir, canisterName);
102
111
  await rm(tempDir, { recursive: true, force: true });
103
112
  mkdirSync(tempDir, { recursive: true });
package/dist/integrity.js CHANGED
@@ -31,7 +31,8 @@ async function getResolvedMopsPackageIds() {
31
31
  let packageIds = Object.entries(resolvedPackages)
32
32
  .filter(([_, version]) => getDependencyType(version) === "mops")
33
33
  .map(([name, version]) => getPackageId(name, version));
34
- return packageIds;
34
+ // dedupe: aliases like `base@0`, `base@0.16` collapse to the same packageId
35
+ return [...new Set(packageIds)];
35
36
  }
36
37
  // get hash of local file from '.mops' dir by fileId
37
38
  export function getLocalFileHash(fileId) {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.12.0",
3
+ "version": "2.12.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "bin/mops.js",
@@ -67,4 +67,23 @@ describe("install", () => {
67
67
  });
68
68
  // mops add/remove/update/sync are not separately tested here because they
69
69
  // all route through the same checkIntegrity code path tested above.
70
+ // Regression: aliases pinning the same package@version (e.g. `core` and
71
+ // `core@1` both at "1.0.0") inflated the resolved-packageIds count and
72
+ // tripped the lockfile integrity check with a spurious
73
+ // "Mismatched number of resolved packages" error. See issue #506.
74
+ test("integrity check passes when aliases resolve to the same package@version", async () => {
75
+ const cwd = path.join(import.meta.dirname, "install/aliases");
76
+ const lockFile = path.join(cwd, "mops.lock");
77
+ rmSync(lockFile, { force: true });
78
+ try {
79
+ const result = await cli(["install"], { cwd, env: { CI: undefined } });
80
+ expect(result.stderr).not.toMatch(/Mismatched number of resolved packages/);
81
+ expect(result.exitCode).toBe(0);
82
+ expect(existsSync(lockFile)).toBe(true);
83
+ }
84
+ finally {
85
+ rmSync(lockFile, { force: true });
86
+ rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
87
+ }
88
+ });
70
89
  });
@@ -112,6 +112,41 @@ describe("migrate", () => {
112
112
  await patchMigrations(cwd, "check-limit = 2");
113
113
  await cliSnapshot(["check", "--verbose"], { cwd }, 0);
114
114
  });
115
+ test("error inside a chain migration reports its file location", async () => {
116
+ const cwd = await makeTempFixture("with-next");
117
+ await writeFile(path.join(cwd, "migrations", "20250301_000000_AddEmail.mo"), 'import State "../types/State";\n' +
118
+ "module {\n" +
119
+ " public func migration(old : State.V2) : State.V3 {\n" +
120
+ " { old with email = 42 };\n" +
121
+ " };\n" +
122
+ "};\n");
123
+ await cliSnapshot(["check"], { cwd }, 1);
124
+ });
125
+ async function corruptNextMigration(cwd) {
126
+ const nextDir = path.join(cwd, "next-migration");
127
+ const nextFile = readdirSync(nextDir).find((f) => f.endsWith(".mo"));
128
+ await writeFile(path.join(nextDir, nextFile), 'import State "../types/State";\n' +
129
+ "module {\n" +
130
+ " public func migration(old : State.V3) : State.V4 {\n" +
131
+ ' { id = "wrong"; name = old.name; email = old.email };\n' +
132
+ " };\n" +
133
+ "};\n");
134
+ }
135
+ test("check-limit=1 with pending next reports real next-migration path on error", async () => {
136
+ const cwd = await makeTempFixture("with-next");
137
+ await patchMigrations(cwd, "check-limit = 1");
138
+ await corruptNextMigration(cwd);
139
+ await cliSnapshot(["check"], { cwd }, 1);
140
+ });
141
+ test("empty chain with pending next reports real next-migration path on error", async () => {
142
+ const cwd = await makeTempFixture("with-next");
143
+ const chainDir = path.join(cwd, "migrations");
144
+ for (const f of readdirSync(chainDir).filter((f) => f.endsWith(".mo"))) {
145
+ await rm(path.join(chainDir, f));
146
+ }
147
+ await corruptNextMigration(cwd);
148
+ await cliSnapshot(["check"], { cwd }, 1);
149
+ });
115
150
  });
116
151
  describe("build", () => {
117
152
  test("build produces .most with full migration chain", async () => {
@@ -153,6 +153,16 @@ export async function prepareMigrationArgs(
153
153
  };
154
154
  }
155
155
 
156
+ // Shortcut: when only the pending next migration is needed (empty chain or
157
+ // trimmed to 1), point moc at next-migration/ so diagnostics use the real path.
158
+ if (nextFile && nextDir && (chainFiles.length === 0 || limit === 1)) {
159
+ const migrationArgs = [`--enhanced-migration=${nextDir}`];
160
+ if (isTrimming) {
161
+ migrationArgs.push("-A=M0254");
162
+ }
163
+ return { migrationArgs, cleanup: async () => {} };
164
+ }
165
+
156
166
  const tempDir = stagedMigrationsDir(chainDir, canisterName);
157
167
  await rm(tempDir, { recursive: true, force: true });
158
168
  mkdirSync(tempDir, { recursive: true });
package/integrity.ts CHANGED
@@ -63,7 +63,8 @@ async function getResolvedMopsPackageIds(): Promise<string[]> {
63
63
  let packageIds = Object.entries(resolvedPackages)
64
64
  .filter(([_, version]) => getDependencyType(version) === "mops")
65
65
  .map(([name, version]) => getPackageId(name, version));
66
- return packageIds;
66
+ // dedupe: aliases like `base@0`, `base@0.16` collapse to the same packageId
67
+ return [...new Set(packageIds)];
67
68
  }
68
69
 
69
70
  // get hash of local file from '.mops' dir by fileId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.12.0",
3
+ "version": "2.12.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "dist/bin/mops.js",
@@ -85,6 +85,42 @@ check-stable Comparing deployed.most ↔ .mops/.check-stable/new.most
85
85
  }
86
86
  `;
87
87
 
88
+ exports[`migrate check check-limit=1 with pending next reports real next-migration path on error 1`] = `
89
+ {
90
+ "exitCode": 1,
91
+ "stderr": "next-migration/20250401_000000_RenameId.mo:4.12-4.19: type error [M0050], literal of type
92
+ Text
93
+ does not have expected type
94
+ Nat
95
+ ✗ Check failed for canister backend (exit code: 1)",
96
+ "stdout": "",
97
+ }
98
+ `;
99
+
100
+ exports[`migrate check empty chain with pending next reports real next-migration path on error 1`] = `
101
+ {
102
+ "exitCode": 1,
103
+ "stderr": "next-migration/20250401_000000_RenameId.mo:4.12-4.19: type error [M0050], literal of type
104
+ Text
105
+ does not have expected type
106
+ Nat
107
+ ✗ Check failed for canister backend (exit code: 1)",
108
+ "stdout": "",
109
+ }
110
+ `;
111
+
112
+ exports[`migrate check error inside a chain migration reports its file location 1`] = `
113
+ {
114
+ "exitCode": 1,
115
+ "stderr": ".migrations-backend/20250301_000000_AddEmail.mo:4.24-4.26: type error [M0050], literal of type
116
+ Nat
117
+ does not have expected type
118
+ Text
119
+ ✗ Check failed for canister backend (exit code: 1)",
120
+ "stdout": "",
121
+ }
122
+ `;
123
+
88
124
  exports[`migrate migrate freeze moves the file from next to chain 1`] = `
89
125
  {
90
126
  "exitCode": 0,
package/tests/cli.test.ts CHANGED
@@ -71,4 +71,25 @@ describe("install", () => {
71
71
 
72
72
  // mops add/remove/update/sync are not separately tested here because they
73
73
  // all route through the same checkIntegrity code path tested above.
74
+
75
+ // Regression: aliases pinning the same package@version (e.g. `core` and
76
+ // `core@1` both at "1.0.0") inflated the resolved-packageIds count and
77
+ // tripped the lockfile integrity check with a spurious
78
+ // "Mismatched number of resolved packages" error. See issue #506.
79
+ test("integrity check passes when aliases resolve to the same package@version", async () => {
80
+ const cwd = path.join(import.meta.dirname, "install/aliases");
81
+ const lockFile = path.join(cwd, "mops.lock");
82
+ rmSync(lockFile, { force: true });
83
+ try {
84
+ const result = await cli(["install"], { cwd, env: { CI: undefined } });
85
+ expect(result.stderr).not.toMatch(
86
+ /Mismatched number of resolved packages/,
87
+ );
88
+ expect(result.exitCode).toBe(0);
89
+ expect(existsSync(lockFile)).toBe(true);
90
+ } finally {
91
+ rmSync(lockFile, { force: true });
92
+ rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
93
+ }
94
+ });
74
95
  });
@@ -0,0 +1,3 @@
1
+ [dependencies]
2
+ core = "1.0.0"
3
+ "core@1" = "1.0.0"
@@ -154,6 +154,51 @@ describe("migrate", () => {
154
154
  await patchMigrations(cwd, "check-limit = 2");
155
155
  await cliSnapshot(["check", "--verbose"], { cwd }, 0);
156
156
  });
157
+
158
+ test("error inside a chain migration reports its file location", async () => {
159
+ const cwd = await makeTempFixture("with-next");
160
+ await writeFile(
161
+ path.join(cwd, "migrations", "20250301_000000_AddEmail.mo"),
162
+ 'import State "../types/State";\n' +
163
+ "module {\n" +
164
+ " public func migration(old : State.V2) : State.V3 {\n" +
165
+ " { old with email = 42 };\n" +
166
+ " };\n" +
167
+ "};\n",
168
+ );
169
+ await cliSnapshot(["check"], { cwd }, 1);
170
+ });
171
+
172
+ async function corruptNextMigration(cwd: string): Promise<void> {
173
+ const nextDir = path.join(cwd, "next-migration");
174
+ const nextFile = readdirSync(nextDir).find((f) => f.endsWith(".mo"))!;
175
+ await writeFile(
176
+ path.join(nextDir, nextFile),
177
+ 'import State "../types/State";\n' +
178
+ "module {\n" +
179
+ " public func migration(old : State.V3) : State.V4 {\n" +
180
+ ' { id = "wrong"; name = old.name; email = old.email };\n' +
181
+ " };\n" +
182
+ "};\n",
183
+ );
184
+ }
185
+
186
+ test("check-limit=1 with pending next reports real next-migration path on error", async () => {
187
+ const cwd = await makeTempFixture("with-next");
188
+ await patchMigrations(cwd, "check-limit = 1");
189
+ await corruptNextMigration(cwd);
190
+ await cliSnapshot(["check"], { cwd }, 1);
191
+ });
192
+
193
+ test("empty chain with pending next reports real next-migration path on error", async () => {
194
+ const cwd = await makeTempFixture("with-next");
195
+ const chainDir = path.join(cwd, "migrations");
196
+ for (const f of readdirSync(chainDir).filter((f) => f.endsWith(".mo"))) {
197
+ await rm(path.join(chainDir, f));
198
+ }
199
+ await corruptNextMigration(cwd);
200
+ await cliSnapshot(["check"], { cwd }, 1);
201
+ });
157
202
  });
158
203
 
159
204
  describe("build", () => {