ic-mops 2.11.0 → 2.12.0
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 +4 -0
- package/bundle/cli.tgz +0 -0
- package/commands/init.ts +17 -4
- package/dist/commands/init.js +11 -4
- package/dist/helpers/migrations.js +21 -5
- package/dist/package.json +1 -1
- package/dist/tests/migrate.test.js +21 -0
- package/helpers/migrations.ts +29 -5
- package/package.json +1 -1
- package/tests/__snapshots__/migrate.test.ts.snap +7 -7
- package/tests/migrate/with-next/migrations/20250101_000000_Init.mo +3 -1
- package/tests/migrate/with-next/migrations/20250201_000000_AddName.mo +3 -1
- package/tests/migrate/with-next/migrations/20250301_000000_AddEmail.mo +3 -5
- package/tests/migrate/with-next/next-migration/20250401_000000_RenameId.mo +3 -5
- package/tests/migrate/with-next/types/State.mo +7 -0
- package/tests/migrate.test.ts +27 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Next
|
|
4
4
|
|
|
5
|
+
## 2.12.0
|
|
6
|
+
- 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
|
+
- `[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"`
|
|
8
|
+
|
|
5
9
|
## 2.11.0
|
|
6
10
|
- Add `mops migrate new <Name>` and `mops migrate freeze` commands for managing enhanced migration chains
|
|
7
11
|
- Add `[canisters.<name>.migrations]` config section with `chain`, `next`, `check-limit`, and `build-limit` fields
|
package/bundle/cli.tgz
CHANGED
|
Binary file
|
package/commands/init.ts
CHANGED
|
@@ -282,16 +282,29 @@ async function applyInit({
|
|
|
282
282
|
await template("github-workflow:mops-test");
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
// add
|
|
285
|
+
// add mops-managed paths to .gitignore
|
|
286
286
|
{
|
|
287
287
|
let gitignore = path.join(process.cwd(), ".gitignore");
|
|
288
288
|
let gitignoreData = existsSync(gitignore)
|
|
289
289
|
? readFileSync(gitignore).toString()
|
|
290
290
|
: "";
|
|
291
|
-
|
|
291
|
+
const additions: string[] = [];
|
|
292
292
|
if (!gitignoreData.includes(".mops")) {
|
|
293
|
-
|
|
294
|
-
|
|
293
|
+
additions.push(".mops");
|
|
294
|
+
}
|
|
295
|
+
if (!gitignoreData.includes(".migrations-")) {
|
|
296
|
+
additions.push(".migrations-*/");
|
|
297
|
+
}
|
|
298
|
+
if (additions.length > 0) {
|
|
299
|
+
let lf = gitignoreData.endsWith("\n") ? "\n" : "";
|
|
300
|
+
writeFileSync(
|
|
301
|
+
gitignore,
|
|
302
|
+
`${gitignoreData}\n${additions.join("\n")}${lf}`.trimStart(),
|
|
303
|
+
);
|
|
304
|
+
console.log(
|
|
305
|
+
chalk.green("Added"),
|
|
306
|
+
`${additions.join(", ")} to .gitignore`,
|
|
307
|
+
);
|
|
295
308
|
}
|
|
296
309
|
}
|
|
297
310
|
|
package/dist/commands/init.js
CHANGED
|
@@ -226,16 +226,23 @@ async function applyInit({ type, config, setupWorkflow, addTest, copyrightOwner,
|
|
|
226
226
|
if (setupWorkflow) {
|
|
227
227
|
await template("github-workflow:mops-test");
|
|
228
228
|
}
|
|
229
|
-
// add
|
|
229
|
+
// add mops-managed paths to .gitignore
|
|
230
230
|
{
|
|
231
231
|
let gitignore = path.join(process.cwd(), ".gitignore");
|
|
232
232
|
let gitignoreData = existsSync(gitignore)
|
|
233
233
|
? readFileSync(gitignore).toString()
|
|
234
234
|
: "";
|
|
235
|
-
|
|
235
|
+
const additions = [];
|
|
236
236
|
if (!gitignoreData.includes(".mops")) {
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
additions.push(".mops");
|
|
238
|
+
}
|
|
239
|
+
if (!gitignoreData.includes(".migrations-")) {
|
|
240
|
+
additions.push(".migrations-*/");
|
|
241
|
+
}
|
|
242
|
+
if (additions.length > 0) {
|
|
243
|
+
let lf = gitignoreData.endsWith("\n") ? "\n" : "";
|
|
244
|
+
writeFileSync(gitignore, `${gitignoreData}\n${additions.join("\n")}${lf}`.trimStart());
|
|
245
|
+
console.log(chalk.green("Added"), `${additions.join(", ")} to .gitignore`);
|
|
239
246
|
}
|
|
240
247
|
}
|
|
241
248
|
// install deps
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, symlinkSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { rm } from "node:fs/promises";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { cliError } from "../error.js";
|
|
6
|
-
import { resolveConfigPath } from "../mops.js";
|
|
7
|
-
|
|
6
|
+
import { getRootDir, resolveConfigPath } from "../mops.js";
|
|
7
|
+
function stagedMigrationsDir(chainDir, canisterName) {
|
|
8
|
+
return join(dirname(chainDir), `.migrations-${canisterName}`);
|
|
9
|
+
}
|
|
8
10
|
export function getMigrationFiles(dir) {
|
|
9
11
|
if (!existsSync(dir)) {
|
|
10
12
|
return [];
|
|
@@ -44,6 +46,19 @@ export function validateMigrationsConfig(migrations, canisterName) {
|
|
|
44
46
|
cliError(`[canisters.${canisterName}.migrations] ${field} must be a positive integer`);
|
|
45
47
|
}
|
|
46
48
|
}
|
|
49
|
+
if (migrations.next) {
|
|
50
|
+
const parentOf = (p) => dirname(resolve(getRootDir(), p));
|
|
51
|
+
const chainParent = parentOf(migrations.chain);
|
|
52
|
+
const nextParent = parentOf(migrations.next);
|
|
53
|
+
if (chainParent !== nextParent) {
|
|
54
|
+
cliError(`[canisters.${canisterName}.migrations] "chain" and "next" must live in the same parent directory.\n` +
|
|
55
|
+
` chain = "${migrations.chain}" (parent: ${chainParent})\n` +
|
|
56
|
+
` next = "${migrations.next}" (parent: ${nextParent})\n` +
|
|
57
|
+
"Place them in the same parent directory, e.g.:\n" +
|
|
58
|
+
' chain = "migrations"\n' +
|
|
59
|
+
' next = "next-migration"');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
47
62
|
}
|
|
48
63
|
export async function prepareMigrationArgs(migrations, canisterName, mode, verbose) {
|
|
49
64
|
const noOp = {
|
|
@@ -83,9 +98,10 @@ export async function prepareMigrationArgs(migrations, canisterName, mode, verbo
|
|
|
83
98
|
cleanup: async () => { },
|
|
84
99
|
};
|
|
85
100
|
}
|
|
86
|
-
const tempDir =
|
|
101
|
+
const tempDir = stagedMigrationsDir(chainDir, canisterName);
|
|
87
102
|
await rm(tempDir, { recursive: true, force: true });
|
|
88
103
|
mkdirSync(tempDir, { recursive: true });
|
|
104
|
+
writeFileSync(join(tempDir, ".gitignore"), "*\n");
|
|
89
105
|
const filesToInclude = isTrimming
|
|
90
106
|
? allMigrations.slice(-limit)
|
|
91
107
|
: allMigrations;
|
package/dist/package.json
CHANGED
|
@@ -145,6 +145,27 @@ describe("migrate", () => {
|
|
|
145
145
|
await cliSnapshot(["check"], { cwd }, 1);
|
|
146
146
|
});
|
|
147
147
|
});
|
|
148
|
+
describe("sibling validation", () => {
|
|
149
|
+
async function patchNextDir(cwd, nextValue) {
|
|
150
|
+
const tomlPath = path.join(cwd, "mops.toml");
|
|
151
|
+
const toml = readFileSync(tomlPath, "utf-8");
|
|
152
|
+
await writeFile(tomlPath, toml.replace('next = "next-migration"', `next = "${nextValue}"`));
|
|
153
|
+
}
|
|
154
|
+
test("errors from `mops check` when chain and next have different parents", async () => {
|
|
155
|
+
const cwd = await makeTempFixture("with-next");
|
|
156
|
+
await patchNextDir(cwd, "other/next-migration");
|
|
157
|
+
const result = await cli(["check"], { cwd });
|
|
158
|
+
expect(result.exitCode).toBe(1);
|
|
159
|
+
expect(result.stderr).toMatch(/same parent directory/i);
|
|
160
|
+
});
|
|
161
|
+
test("errors from `mops migrate new` too — validation runs in every entry point", async () => {
|
|
162
|
+
const cwd = await makeTempFixture("basic");
|
|
163
|
+
await patchNextDir(cwd, "other/next-migration");
|
|
164
|
+
const result = await cli(["migrate", "new", "Test"], { cwd });
|
|
165
|
+
expect(result.exitCode).toBe(1);
|
|
166
|
+
expect(result.stderr).toMatch(/same parent directory/i);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
148
169
|
describe("conflict detection", () => {
|
|
149
170
|
test("errors when both [migrations] and --enhanced-migration in args", async () => {
|
|
150
171
|
const cwd = await makeTempFixture("basic");
|
package/helpers/migrations.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
symlinkSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
9
|
import { rm } from "node:fs/promises";
|
|
4
10
|
import chalk from "chalk";
|
|
5
11
|
import { cliError } from "../error.js";
|
|
6
|
-
import { resolveConfigPath } from "../mops.js";
|
|
12
|
+
import { getRootDir, resolveConfigPath } from "../mops.js";
|
|
7
13
|
import { MigrationsConfig } from "../types.js";
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
function stagedMigrationsDir(chainDir: string, canisterName: string): string {
|
|
16
|
+
return join(dirname(chainDir), `.migrations-${canisterName}`);
|
|
17
|
+
}
|
|
10
18
|
|
|
11
19
|
export interface MigrationArgsResult {
|
|
12
20
|
migrationArgs: string[];
|
|
@@ -70,6 +78,21 @@ export function validateMigrationsConfig(
|
|
|
70
78
|
);
|
|
71
79
|
}
|
|
72
80
|
}
|
|
81
|
+
if (migrations.next) {
|
|
82
|
+
const parentOf = (p: string) => dirname(resolve(getRootDir(), p));
|
|
83
|
+
const chainParent = parentOf(migrations.chain);
|
|
84
|
+
const nextParent = parentOf(migrations.next);
|
|
85
|
+
if (chainParent !== nextParent) {
|
|
86
|
+
cliError(
|
|
87
|
+
`[canisters.${canisterName}.migrations] "chain" and "next" must live in the same parent directory.\n` +
|
|
88
|
+
` chain = "${migrations.chain}" (parent: ${chainParent})\n` +
|
|
89
|
+
` next = "${migrations.next}" (parent: ${nextParent})\n` +
|
|
90
|
+
"Place them in the same parent directory, e.g.:\n" +
|
|
91
|
+
' chain = "migrations"\n' +
|
|
92
|
+
' next = "next-migration"',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
export async function prepareMigrationArgs(
|
|
@@ -130,9 +153,10 @@ export async function prepareMigrationArgs(
|
|
|
130
153
|
};
|
|
131
154
|
}
|
|
132
155
|
|
|
133
|
-
const tempDir =
|
|
156
|
+
const tempDir = stagedMigrationsDir(chainDir, canisterName);
|
|
134
157
|
await rm(tempDir, { recursive: true, force: true });
|
|
135
158
|
mkdirSync(tempDir, { recursive: true });
|
|
159
|
+
writeFileSync(join(tempDir, ".gitignore"), "*\n");
|
|
136
160
|
|
|
137
161
|
const filesToInclude = isTrimming
|
|
138
162
|
? allMigrations.slice(-limit)
|
package/package.json
CHANGED
|
@@ -33,12 +33,12 @@ actor {
|
|
|
33
33
|
|
|
34
34
|
exports[`migrate build build-limit counts next migration as part of the chain 1`] = `
|
|
35
35
|
"// Version: 4.0.0
|
|
36
|
+
type V2__933402648 = {a : Nat; name : Text};
|
|
37
|
+
type V3__40989327 = {a : Nat; email : Text; name : Text};
|
|
38
|
+
type V4__364034734 = {email : Text; id : Nat; name : Text};
|
|
36
39
|
{
|
|
37
|
-
"20250301_000000_AddEmail" :
|
|
38
|
-
|
|
39
|
-
"20250401_000000_RenameId" :
|
|
40
|
-
(old : {a : Nat; email : Text; name : Text}) ->
|
|
41
|
-
{email : Text; id : Nat; name : Text}
|
|
40
|
+
"20250301_000000_AddEmail" : (old : V2__933402648) -> V3__40989327;
|
|
41
|
+
"20250401_000000_RenameId" : (old : V3__40989327) -> V4__364034734
|
|
42
42
|
}
|
|
43
43
|
actor {
|
|
44
44
|
stable email : Text;
|
|
@@ -75,10 +75,10 @@ exports[`migrate check check with trimming shows reduced chain 1`] = `
|
|
|
75
75
|
"stdout": "check Using --all-libs for richer diagnostics
|
|
76
76
|
migrations Prepared 2 migration(s) for backend (trimmed from 3)
|
|
77
77
|
check Checking canister backend:
|
|
78
|
-
<CACHE>moc-wrapper ["src/main.mo","--check","--all-libs","--default-persistent-actors","--enhanced-migration=.
|
|
78
|
+
<CACHE>moc-wrapper ["src/main.mo","--check","--all-libs","--default-persistent-actors","--enhanced-migration=.migrations-backend","-A=M0254"]
|
|
79
79
|
✓ backend
|
|
80
80
|
check-stable Generating stable types for src/main.mo
|
|
81
|
-
<CACHE>moc-wrapper ["--stable-types","-o",".mops/.check-stable/new.wasm","src/main.mo","--default-persistent-actors","--enhanced-migration=.
|
|
81
|
+
<CACHE>moc-wrapper ["--stable-types","-o",".mops/.check-stable/new.wasm","src/main.mo","--default-persistent-actors","--enhanced-migration=.migrations-backend","-A=M0254"]
|
|
82
82
|
check-stable Comparing deployed.most ↔ .mops/.check-stable/new.most
|
|
83
83
|
<CACHE>moc-wrapper ["--stable-compatible","deployed.most",".mops/.check-stable/new.most"]
|
|
84
84
|
✓ Stable compatibility check passed for canister 'backend'",
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
+
import State "../types/State";
|
|
2
|
+
|
|
1
3
|
module {
|
|
2
|
-
public func migration(old :
|
|
3
|
-
id : Nat;
|
|
4
|
-
name : Text;
|
|
5
|
-
email : Text;
|
|
6
|
-
} {
|
|
4
|
+
public func migration(old : State.V3) : State.V4 {
|
|
7
5
|
{ id = old.a; name = old.name; email = old.email };
|
|
8
6
|
};
|
|
9
7
|
};
|
package/tests/migrate.test.ts
CHANGED
|
@@ -207,6 +207,33 @@ describe("migrate", () => {
|
|
|
207
207
|
});
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
+
describe("sibling validation", () => {
|
|
211
|
+
async function patchNextDir(cwd: string, nextValue: string): Promise<void> {
|
|
212
|
+
const tomlPath = path.join(cwd, "mops.toml");
|
|
213
|
+
const toml = readFileSync(tomlPath, "utf-8");
|
|
214
|
+
await writeFile(
|
|
215
|
+
tomlPath,
|
|
216
|
+
toml.replace('next = "next-migration"', `next = "${nextValue}"`),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
test("errors from `mops check` when chain and next have different parents", async () => {
|
|
221
|
+
const cwd = await makeTempFixture("with-next");
|
|
222
|
+
await patchNextDir(cwd, "other/next-migration");
|
|
223
|
+
const result = await cli(["check"], { cwd });
|
|
224
|
+
expect(result.exitCode).toBe(1);
|
|
225
|
+
expect(result.stderr).toMatch(/same parent directory/i);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("errors from `mops migrate new` too — validation runs in every entry point", async () => {
|
|
229
|
+
const cwd = await makeTempFixture("basic");
|
|
230
|
+
await patchNextDir(cwd, "other/next-migration");
|
|
231
|
+
const result = await cli(["migrate", "new", "Test"], { cwd });
|
|
232
|
+
expect(result.exitCode).toBe(1);
|
|
233
|
+
expect(result.stderr).toMatch(/same parent directory/i);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
210
237
|
describe("conflict detection", () => {
|
|
211
238
|
test("errors when both [migrations] and --enhanced-migration in args", async () => {
|
|
212
239
|
const cwd = await makeTempFixture("basic");
|