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 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 .mops to .gitignore
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
- let lf = gitignoreData.endsWith("\n") ? "\n" : "";
291
+ const additions: string[] = [];
292
292
  if (!gitignoreData.includes(".mops")) {
293
- writeFileSync(gitignore, `${gitignoreData}\n.mops${lf}`.trimStart());
294
- console.log(chalk.green("Added"), ".mops to .gitignore");
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
 
@@ -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 .mops to .gitignore
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
- let lf = gitignoreData.endsWith("\n") ? "\n" : "";
235
+ const additions = [];
236
236
  if (!gitignoreData.includes(".mops")) {
237
- writeFileSync(gitignore, `${gitignoreData}\n.mops${lf}`.trimStart());
238
- console.log(chalk.green("Added"), ".mops to .gitignore");
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
- const MIGRATIONS_TEMP_DIR = ".mops/.migrations";
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 = join(MIGRATIONS_TEMP_DIR, canisterName);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "bin/mops.js",
@@ -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");
@@ -1,12 +1,20 @@
1
- import { existsSync, mkdirSync, readdirSync, symlinkSync } from "node:fs";
2
- import { join, resolve } from "node:path";
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
- const MIGRATIONS_TEMP_DIR = ".mops/.migrations";
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 = join(MIGRATIONS_TEMP_DIR, canisterName);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "dist/bin/mops.js",
@@ -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
- (old : {a : Nat; name : Text}) -> {a : Nat; email : Text; name : Text};
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=.mops/.migrations/backend","-A=M0254"]
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=.mops/.migrations/backend","-A=M0254"]
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,5 +1,7 @@
1
+ import State "../types/State";
2
+
1
3
  module {
2
- public func migration(_ : {}) : { a : Nat } {
4
+ public func migration(_ : State.V0) : State.V1 {
3
5
  { a = 0 };
4
6
  };
5
7
  };
@@ -1,5 +1,7 @@
1
+ import State "../types/State";
2
+
1
3
  module {
2
- public func migration(old : { a : Nat }) : { a : Nat; name : Text } {
4
+ public func migration(old : State.V1) : State.V2 {
3
5
  { old with name = "" };
4
6
  };
5
7
  };
@@ -1,9 +1,7 @@
1
+ import State "../types/State";
2
+
1
3
  module {
2
- public func migration(old : { a : Nat; name : Text }) : {
3
- a : Nat;
4
- name : Text;
5
- email : Text;
6
- } {
4
+ public func migration(old : State.V2) : State.V3 {
7
5
  { old with email = "" };
8
6
  };
9
7
  };
@@ -1,9 +1,7 @@
1
+ import State "../types/State";
2
+
1
3
  module {
2
- public func migration(old : { a : Nat; name : Text; email : Text }) : {
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
  };
@@ -0,0 +1,7 @@
1
+ module {
2
+ public type V0 = {};
3
+ public type V1 = { a : Nat };
4
+ public type V2 = { a : Nat; name : Text };
5
+ public type V3 = { a : Nat; name : Text; email : Text };
6
+ public type V4 = { id : Nat; name : Text; email : Text };
7
+ };
@@ -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");