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 +6 -0
- package/bundle/cli.tgz +0 -0
- package/dist/helpers/migrations.js +9 -0
- package/dist/integrity.js +2 -1
- package/dist/package.json +1 -1
- package/dist/tests/cli.test.js +19 -0
- package/dist/tests/migrate.test.js +35 -0
- package/helpers/migrations.ts +10 -0
- package/integrity.ts +2 -1
- package/package.json +1 -1
- package/tests/__snapshots__/migrate.test.ts.snap +36 -0
- package/tests/cli.test.ts +21 -0
- package/tests/install/aliases/mops.toml +3 -0
- package/tests/migrate.test.ts +45 -0
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
|
-
|
|
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
package/dist/tests/cli.test.js
CHANGED
|
@@ -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 () => {
|
package/helpers/migrations.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -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
|
});
|
package/tests/migrate.test.ts
CHANGED
|
@@ -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", () => {
|