ic-mops 2.6.0 → 2.8.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,18 @@
2
2
 
3
3
  ## Next
4
4
 
5
+ ## 2.8.0
6
+
7
+ - `mops build` now generates a `.most` (Motoko stable types) file alongside `.wasm` and `.did` for each canister; the `.most` file can be passed directly to `mops check-stable` to verify upgrade compatibility
8
+ - `mops.lock` is now created automatically the first time dependencies are installed — no need to run `mops i --lock update` once to opt in. Triggered by `mops install`, `mops add`, `mops remove`, `mops update`, `mops sync`, and `mops init` (when it installs dependencies). Applications should commit `mops.lock`; library authors should add it to `.gitignore`.
9
+
10
+ ## 2.7.0
11
+
12
+ - `mops publish` no longer requires a `repository` field — it is now optional metadata (used by the registry UI for source links)
13
+ - `mops publish` now hard-errors on GitHub `[dependencies]` instead of prompting; the backend has rejected them for some time and the prompt was misleading
14
+ - `mops publish` now fails fast with a clear error when unsupported fields (`dfx`, `moc`, `homepage`, `documentation`, `donation`) are set in `mops.toml`
15
+ - Fix `mops publish` reporting incorrect max length for `license` field (was 30, now matches backend limit of 40)
16
+
5
17
  ## 2.6.0
6
18
 
7
19
  - Packages can ship lintoko rules for consumers in a `rules/` directory (distinct from `lint/`/`lints/` which check the package itself); `rules/*.toml` files are included automatically when running `mops publish`
package/RELEASE.md CHANGED
@@ -21,33 +21,36 @@ cd cli
21
21
  npm version patch --no-git-tag-version # or: minor / major
22
22
  ```
23
23
 
24
- ## 3. Create a release PR
24
+ ## 3. Create a release PR and enable auto-merge
25
25
 
26
26
  ```bash
27
27
  git checkout -b <username>/release-X.Y.Z
28
28
  git add cli/CHANGELOG.md cli/package.json cli/package-lock.json
29
29
  git commit -m "release: CLI vX.Y.Z"
30
30
  git push -u origin <username>/release-X.Y.Z
31
- gh pr create --title "release: CLI vX.Y.Z" --body "..."
31
+ gh pr create \
32
+ --title "release: CLI vX.Y.Z" \
33
+ --body "Release CLI vX.Y.Z." \
34
+ --label release
35
+ gh pr merge --auto --squash
32
36
  ```
33
37
 
34
- Wait for CI to pass, then merge.
38
+ The [`release-pr.yml`](../.github/workflows/release-pr.yml) workflow runs on every update and validates:
39
+ - PR title matches `release: CLI vX.Y.Z`
40
+ - `cli/CHANGELOG.md` has an entry for the version
41
+ - `cli/package.json` version matches
35
42
 
36
- ## 4. Tag and push
43
+ Once all required checks pass the PR merges automatically. On merge, `release-pr.yml` pushes the `cli-vX.Y.Z` tag, which triggers the [`release.yml`](../.github/workflows/release.yml) workflow — it builds, publishes to npm, creates a GitHub Release, and deploys canisters (`cli.mops.one` and `docs.mops.one`).
37
44
 
38
- ```bash
39
- git checkout main && git pull
40
- git tag cli-vX.Y.Z
41
- git push origin cli-vX.Y.Z
42
- ```
43
-
44
- This triggers the [`release.yml`](../.github/workflows/release.yml) workflow which builds, publishes to npm, creates a GitHub Release, deploys canisters (`cli.mops.one` and `docs.mops.one`), and opens a PR with on-chain release artifacts.
45
-
46
- Monitor at [Actions → Release CLI](https://github.com/caffeinelabs/mops/actions/workflows/release.yml).
45
+ > **Note:** This workflow only deploys the `cli` and `docs` canisters. The `main`, `assets`, `blog`, and `play-frontend` canisters require a manual deploy. If a release includes changes to any of those (e.g. `backend/main/` or `frontend/`), upgrade them manually (staging first, then `ic`):
46
+ >
47
+ > ```bash
48
+ > NODE_ENV=production dfx deploy --no-wallet --identity mops --network <staging|ic> <canister>
49
+ > ```
47
50
 
48
- ## 5. Merge artifacts PR
51
+ ## 5. Artifacts PR
49
52
 
50
- After the workflow completes, merge the `cli-releases: vX.Y.Z artifacts` PR.
53
+ After the release pipeline completes, it creates and auto-merges a `cli-releases: vX.Y.Z artifacts` PR. No action needed unless it fails — monitor at [Actions → Release CLI](https://github.com/caffeinelabs/mops/actions/workflows/release.yml) and merge the artifacts PR manually if needed.
51
54
 
52
55
  ## Verify build
53
56
 
package/bundle/cli.tgz CHANGED
Binary file
package/commands/build.ts CHANGED
@@ -69,9 +69,11 @@ export async function build(
69
69
  }
70
70
  motokoPath = resolveConfigPath(motokoPath);
71
71
  const wasmPath = join(outputDir, `${canisterName}.wasm`);
72
+ const mostPath = join(outputDir, `${canisterName}.most`);
72
73
  let args = [
73
74
  "-c",
74
75
  "--idl",
76
+ "--stable-types",
75
77
  "-o",
76
78
  wasmPath,
77
79
  motokoPath,
@@ -116,6 +118,9 @@ export async function build(
116
118
  console.log(result.stdout);
117
119
  }
118
120
 
121
+ options.verbose &&
122
+ console.log(chalk.gray(`Stable types written to ${mostPath}`));
123
+
119
124
  const generatedDidPath = join(outputDir, `${canisterName}.did`);
120
125
  const resolvedCandidPath = canister.candid
121
126
  ? resolveConfigPath(canister.candid)
@@ -188,6 +193,7 @@ const managedFlags: Record<string, string> = {
188
193
  "-o": "use [build].outputDir in mops.toml or --output flag instead",
189
194
  "-c": "this flag is always set by mops build",
190
195
  "--idl": "this flag is always set by mops build",
196
+ "--stable-types": "this flag is always set by mops build",
191
197
  "--public-metadata": "this flag is managed by mops build",
192
198
  };
193
199
 
@@ -83,7 +83,7 @@ export async function publish(
83
83
  }
84
84
 
85
85
  // desired fields
86
- for (let key of ["description", "repository"]) {
86
+ for (let key of ["description"]) {
87
87
  // @ts-ignore
88
88
  if (!config.package[key] && !process.env.CI) {
89
89
  let res = await prompts({
@@ -120,6 +120,14 @@ export async function publish(
120
120
  }
121
121
  }
122
122
 
123
+ // disabled fields
124
+ for (let key of ["dfx", "moc", "homepage", "documentation", "donation"]) {
125
+ if ((config.package as any)[key]) {
126
+ console.log(chalk.red("Error: ") + `package.${key} is not supported yet`);
127
+ process.exit(1);
128
+ }
129
+ }
130
+
123
131
  // check lengths
124
132
  let keysMax = {
125
133
  name: 50,
@@ -130,7 +138,7 @@ export async function publish(
130
138
  documentation: 300,
131
139
  homepage: 300,
132
140
  readme: 100,
133
- license: 30,
141
+ license: 40,
134
142
  files: 20,
135
143
  dfx: 10,
136
144
  moc: 10,
@@ -166,18 +174,12 @@ export async function publish(
166
174
  }
167
175
 
168
176
  for (let dep of Object.values(config.dependencies)) {
169
- if (dep.repo && !process.env.CI) {
170
- let res = await prompts({
171
- type: "confirm",
172
- name: "ok",
173
- message:
174
- chalk.yellow(
175
- "GitHub dependencies make the registry less reliable and limit its capabilities.\nIf you are the owner of the dependency, please consider publishing it to the Mops registry.",
176
- ) + "\n\nPublish anyway?",
177
- });
178
- if (!res.ok) {
179
- return;
180
- }
177
+ if (dep.repo) {
178
+ console.log(
179
+ chalk.red("Error: ") +
180
+ "GitHub dependencies are no longer supported.\nIf you are the owner of the dependency, please publish it to the Mops registry.",
181
+ );
182
+ process.exit(1);
181
183
  }
182
184
  }
183
185
  }
@@ -345,7 +347,10 @@ export async function publish(
345
347
  // parse changelog
346
348
  console.log("Parsing CHANGELOG.md...");
347
349
  let changelog = parseChangelog(config.package.version);
348
- if (!changelog && config.package.repository) {
350
+ if (
351
+ !changelog &&
352
+ config.package.repository?.startsWith("https://github.com/")
353
+ ) {
349
354
  console.log("Fetching release notes from GitHub...");
350
355
  changelog = await fetchGitHubReleaseNotes(
351
356
  config.package.repository,
@@ -45,9 +45,11 @@ export async function build(canisterNames, options) {
45
45
  }
46
46
  motokoPath = resolveConfigPath(motokoPath);
47
47
  const wasmPath = join(outputDir, `${canisterName}.wasm`);
48
+ const mostPath = join(outputDir, `${canisterName}.most`);
48
49
  let args = [
49
50
  "-c",
50
51
  "--idl",
52
+ "--stable-types",
51
53
  "-o",
52
54
  wasmPath,
53
55
  motokoPath,
@@ -84,6 +86,8 @@ export async function build(canisterNames, options) {
84
86
  if (options.verbose && result.stdout && result.stdout.trim()) {
85
87
  console.log(result.stdout);
86
88
  }
89
+ options.verbose &&
90
+ console.log(chalk.gray(`Stable types written to ${mostPath}`));
87
91
  const generatedDidPath = join(outputDir, `${canisterName}.did`);
88
92
  const resolvedCandidPath = canister.candid
89
93
  ? resolveConfigPath(canister.candid)
@@ -132,6 +136,7 @@ const managedFlags = {
132
136
  "-o": "use [build].outputDir in mops.toml or --output flag instead",
133
137
  "-c": "this flag is always set by mops build",
134
138
  "--idl": "this flag is always set by mops build",
139
+ "--stable-types": "this flag is always set by mops build",
135
140
  "--public-metadata": "this flag is managed by mops build",
136
141
  };
137
142
  function collectExtraArgs(config, canister, canisterName, extraArgs) {
@@ -50,7 +50,7 @@ export async function publish(options = {}) {
50
50
  }
51
51
  }
52
52
  // desired fields
53
- for (let key of ["description", "repository"]) {
53
+ for (let key of ["description"]) {
54
54
  // @ts-ignore
55
55
  if (!config.package[key] && !process.env.CI) {
56
56
  let res = await prompts({
@@ -85,6 +85,13 @@ export async function publish(options = {}) {
85
85
  process.exit(1);
86
86
  }
87
87
  }
88
+ // disabled fields
89
+ for (let key of ["dfx", "moc", "homepage", "documentation", "donation"]) {
90
+ if (config.package[key]) {
91
+ console.log(chalk.red("Error: ") + `package.${key} is not supported yet`);
92
+ process.exit(1);
93
+ }
94
+ }
88
95
  // check lengths
89
96
  let keysMax = {
90
97
  name: 50,
@@ -95,7 +102,7 @@ export async function publish(options = {}) {
95
102
  documentation: 300,
96
103
  homepage: 300,
97
104
  readme: 100,
98
- license: 30,
105
+ license: 40,
99
106
  files: 20,
100
107
  dfx: 10,
101
108
  moc: 10,
@@ -123,15 +130,10 @@ export async function publish(options = {}) {
123
130
  delete dep.path;
124
131
  }
125
132
  for (let dep of Object.values(config.dependencies)) {
126
- if (dep.repo && !process.env.CI) {
127
- let res = await prompts({
128
- type: "confirm",
129
- name: "ok",
130
- message: chalk.yellow("GitHub dependencies make the registry less reliable and limit its capabilities.\nIf you are the owner of the dependency, please consider publishing it to the Mops registry.") + "\n\nPublish anyway?",
131
- });
132
- if (!res.ok) {
133
- return;
134
- }
133
+ if (dep.repo) {
134
+ console.log(chalk.red("Error: ") +
135
+ "GitHub dependencies are no longer supported.\nIf you are the owner of the dependency, please publish it to the Mops registry.");
136
+ process.exit(1);
135
137
  }
136
138
  }
137
139
  }
@@ -272,7 +274,8 @@ export async function publish(options = {}) {
272
274
  // parse changelog
273
275
  console.log("Parsing CHANGELOG.md...");
274
276
  let changelog = parseChangelog(config.package.version);
275
- if (!changelog && config.package.repository) {
277
+ if (!changelog &&
278
+ config.package.repository?.startsWith("https://github.com/")) {
276
279
  console.log("Fetching release notes from GitHub...");
277
280
  changelog = await fetchGitHubReleaseNotes(config.package.repository, config.package.version);
278
281
  }
package/dist/integrity.js CHANGED
@@ -9,13 +9,8 @@ import { resolvePackages } from "./resolve-packages.js";
9
9
  import { getPackageId } from "./helpers/get-package-id.js";
10
10
  export async function checkIntegrity(lock) {
11
11
  let force = !!lock;
12
- if (!lock &&
13
- !process.env["CI"] &&
14
- fs.existsSync(path.join(getRootDir(), "mops.lock"))) {
15
- lock = "update";
16
- }
17
12
  if (!lock) {
18
- lock = process.env["CI"] ? "check" : "ignore";
13
+ lock = process.env["CI"] ? "check" : "update";
19
14
  }
20
15
  if (lock === "update") {
21
16
  await updateLockFile();
@@ -133,7 +128,13 @@ export async function updateLockFile() {
133
128
  };
134
129
  let rootDir = getRootDir();
135
130
  let lockFile = path.join(rootDir, "mops.lock");
131
+ let isNew = !fs.existsSync(lockFile);
136
132
  fs.writeFileSync(lockFile, JSON.stringify(lockFileJson, null, 2));
133
+ if (isNew) {
134
+ console.log("mops.lock created.");
135
+ console.log(" Applications: commit this file.");
136
+ console.log(" Libraries: add mops.lock to .gitignore.");
137
+ }
137
138
  }
138
139
  // compare hashes of local files with hashes from the lock file
139
140
  export async function checkLockFile(force = false) {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "bin/mops.js",
@@ -42,12 +42,14 @@ describe("build", () => {
42
42
  const customOut = path.join(cwd, "custom-out");
43
43
  const customWasm = path.join(customOut, "main.wasm");
44
44
  const customDid = path.join(customOut, "main.did");
45
+ const customMost = path.join(customOut, "main.most");
45
46
  const defaultDid = path.join(cwd, ".mops/.build/main.did");
46
47
  try {
47
48
  const result = await cli(["build"], { cwd });
48
49
  expect(result.exitCode).toBe(0);
49
50
  expect(existsSync(customWasm)).toBe(true);
50
51
  expect(existsSync(customDid)).toBe(true);
52
+ expect(existsSync(customMost)).toBe(true);
51
53
  expect(existsSync(defaultDid)).toBe(false);
52
54
  }
53
55
  finally {
@@ -66,6 +68,7 @@ describe("build", () => {
66
68
  expect(result.exitCode).toBe(0);
67
69
  expect(existsSync(path.join(outputDir, "foo.wasm"))).toBe(true);
68
70
  expect(existsSync(path.join(outputDir, "foo.did"))).toBe(true);
71
+ expect(existsSync(path.join(outputDir, "foo.most"))).toBe(true);
69
72
  }
70
73
  finally {
71
74
  cleanFixture(cwd, outputDir);
@@ -76,7 +79,7 @@ describe("build", () => {
76
79
  const artifact = path.join(cwd, "x");
77
80
  const artifactDid = path.join(cwd, "x.did");
78
81
  try {
79
- await cliSnapshot(["build", "foo", "--", "-o", "x", "-c", "--idl"], { cwd }, 1);
82
+ await cliSnapshot(["build", "foo", "--", "-o", "x", "-c", "--idl", "--stable-types"], { cwd }, 1);
80
83
  }
81
84
  finally {
82
85
  cleanFixture(cwd, artifact, artifactDid);
@@ -1,4 +1,6 @@
1
- import { describe, expect, test } from "@jest/globals";
1
+ import { describe, expect, jest, test } from "@jest/globals";
2
+ import { existsSync, rmSync } from "node:fs";
3
+ import path from "path";
2
4
  import { cli } from "./helpers";
3
5
  describe("cli", () => {
4
6
  test("--version", async () => {
@@ -8,3 +10,61 @@ describe("cli", () => {
8
10
  expect((await cli(["--help"])).stdout).toMatch(/^Usage: mops/m);
9
11
  });
10
12
  });
13
+ describe("install", () => {
14
+ jest.setTimeout(120_000);
15
+ test("creates mops.lock automatically on first install", async () => {
16
+ const cwd = path.join(import.meta.dirname, "install/success");
17
+ const lockFile = path.join(cwd, "mops.lock");
18
+ rmSync(lockFile, { force: true });
19
+ try {
20
+ // Unset CI so checkIntegrity uses the local default ("update")
21
+ const result = await cli(["install"], { cwd, env: { CI: undefined } });
22
+ expect(result.exitCode).toBe(0);
23
+ expect(existsSync(lockFile)).toBe(true);
24
+ expect(result.stdout).toMatch(/mops\.lock created/);
25
+ }
26
+ finally {
27
+ rmSync(lockFile, { force: true });
28
+ rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
29
+ }
30
+ });
31
+ test("does not print 'mops.lock created' on subsequent installs", async () => {
32
+ const cwd = path.join(import.meta.dirname, "install/success");
33
+ const lockFile = path.join(cwd, "mops.lock");
34
+ rmSync(lockFile, { force: true });
35
+ try {
36
+ // Unset CI so checkIntegrity uses the local default ("update")
37
+ const first = await cli(["install"], { cwd, env: { CI: undefined } });
38
+ expect(first.exitCode).toBe(0);
39
+ expect(first.stdout).toMatch(/mops\.lock created/);
40
+ const result = await cli(["install"], { cwd, env: { CI: undefined } });
41
+ expect(result.exitCode).toBe(0);
42
+ expect(existsSync(lockFile)).toBe(true);
43
+ expect(result.stdout).not.toMatch(/mops\.lock created/);
44
+ }
45
+ finally {
46
+ rmSync(lockFile, { force: true });
47
+ rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
48
+ }
49
+ });
50
+ test("does not create mops.lock when --lock ignore is passed", async () => {
51
+ const cwd = path.join(import.meta.dirname, "install/success");
52
+ const lockFile = path.join(cwd, "mops.lock");
53
+ rmSync(lockFile, { force: true });
54
+ try {
55
+ // Unset CI for consistency; --lock ignore bypasses auto-detection regardless
56
+ const result = await cli(["install", "--lock", "ignore"], {
57
+ cwd,
58
+ env: { CI: undefined },
59
+ });
60
+ expect(result.exitCode).toBe(0);
61
+ expect(existsSync(lockFile)).toBe(false);
62
+ }
63
+ finally {
64
+ rmSync(lockFile, { force: true });
65
+ rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
66
+ }
67
+ });
68
+ // mops add/remove/update/sync are not separately tested here because they
69
+ // all route through the same checkIntegrity code path tested above.
70
+ });
@@ -1,7 +1,8 @@
1
1
  export interface CliOptions {
2
2
  cwd?: string;
3
+ env?: Record<string, string | undefined>;
3
4
  }
4
- export declare const cli: (args: string[], { cwd }?: CliOptions) => Promise<import("execa").Result<{
5
+ export declare const cli: (args: string[], { cwd, env }?: CliOptions) => Promise<import("execa").Result<{
5
6
  stdio: "pipe";
6
7
  reject: false;
7
8
  cwd?: string | undefined;
@@ -6,12 +6,12 @@ import { fileURLToPath } from "url";
6
6
  // directly rather than the npm script. This exercises the real global-install
7
7
  // code path where the binary lives outside the project tree.
8
8
  const useGlobalBinary = Boolean(process.env.MOPS_TEST_GLOBAL);
9
- export const cli = async (args, { cwd } = {}) => {
9
+ export const cli = async (args, { cwd, env } = {}) => {
10
10
  const [cmd, cmdArgs] = useGlobalBinary
11
11
  ? ["mops", args]
12
12
  : ["npm", ["run", "--silent", "mops", "--", ...args]];
13
13
  return await execa(cmd, cmdArgs, {
14
- env: { ...process.env, ...(cwd != null && { MOPS_CWD: cwd }) },
14
+ env: { ...process.env, ...(cwd != null && { MOPS_CWD: cwd }), ...env },
15
15
  ...(cwd != null && { cwd }),
16
16
  stdio: "pipe",
17
17
  reject: false,
package/integrity.ts CHANGED
@@ -36,16 +36,8 @@ type LockFile = LockFileV1 | LockFileV2 | LockFileV3;
36
36
  export async function checkIntegrity(lock?: "check" | "update" | "ignore") {
37
37
  let force = !!lock;
38
38
 
39
- if (
40
- !lock &&
41
- !process.env["CI"] &&
42
- fs.existsSync(path.join(getRootDir(), "mops.lock"))
43
- ) {
44
- lock = "update";
45
- }
46
-
47
39
  if (!lock) {
48
- lock = process.env["CI"] ? "check" : "ignore";
40
+ lock = process.env["CI"] ? "check" : "update";
49
41
  }
50
42
 
51
43
  if (lock === "update") {
@@ -197,7 +189,13 @@ export async function updateLockFile() {
197
189
 
198
190
  let rootDir = getRootDir();
199
191
  let lockFile = path.join(rootDir, "mops.lock");
192
+ let isNew = !fs.existsSync(lockFile);
200
193
  fs.writeFileSync(lockFile, JSON.stringify(lockFileJson, null, 2));
194
+ if (isNew) {
195
+ console.log("mops.lock created.");
196
+ console.log(" Applications: commit this file.");
197
+ console.log(" Libraries: add mops.lock to .gitignore.");
198
+ }
201
199
  }
202
200
 
203
201
  // compare hashes of local files with hashes from the lock file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "dist/bin/mops.js",
@@ -5,7 +5,8 @@ exports[`build error 1`] = `
5
5
  "exitCode": 0,
6
6
  "stderr": "",
7
7
  "stdout": "build canister foo
8
- moc-wrapper ["-c","--idl","-o",".mops/.build/foo.wasm","src/Foo.mo","--package","core",".mops/core@1.0.0/src","--release","--public-metadata","candid:service","--public-metadata","candid:args"]
8
+ moc-wrapper ["-c","--idl","--stable-types","-o",".mops/.build/foo.wasm","src/Foo.mo","--package","core",".mops/core@1.0.0/src","--release","--public-metadata","candid:service","--public-metadata","candid:args"]
9
+ Stable types written to .mops/.build/foo.most
9
10
  Adding metadata to .mops/.build/foo.wasm
10
11
 
11
12
  ✓ Built 1 canister successfully",
@@ -34,10 +35,12 @@ exports[`build ok 1`] = `
34
35
  "exitCode": 0,
35
36
  "stderr": "",
36
37
  "stdout": "build canister foo
37
- moc-wrapper ["-c","--idl","-o",".mops/.build/foo.wasm","src/Foo.mo","--package","core",".mops/core@1.0.0/src","--release","--public-metadata","candid:service","--public-metadata","candid:args"]
38
+ moc-wrapper ["-c","--idl","--stable-types","-o",".mops/.build/foo.wasm","src/Foo.mo","--package","core",".mops/core@1.0.0/src","--release","--public-metadata","candid:service","--public-metadata","candid:args"]
39
+ Stable types written to .mops/.build/foo.most
38
40
  Adding metadata to .mops/.build/foo.wasm
39
41
  build canister bar
40
- moc-wrapper ["-c","--idl","-o",".mops/.build/bar.wasm","src/Bar.mo","--package","core",".mops/core@1.0.0/src","--release","--incremental-gc","--public-metadata","candid:service","--public-metadata","candid:args"]
42
+ moc-wrapper ["-c","--idl","--stable-types","-o",".mops/.build/bar.wasm","src/Bar.mo","--package","core",".mops/core@1.0.0/src","--release","--incremental-gc","--public-metadata","candid:service","--public-metadata","candid:args"]
43
+ Stable types written to .mops/.build/bar.most
41
44
  Candid compatibility check passed for canister bar
42
45
  Adding metadata to .mops/.build/bar.wasm
43
46
 
@@ -82,6 +85,7 @@ exports[`build warns when args contain managed flags 1`] = `
82
85
  "stderr": "Warning: '-o' in args for canister foo may conflict with mops build — use [build].outputDir in mops.toml or --output flag instead
83
86
  Warning: '-c' in args for canister foo may conflict with mops build — this flag is always set by mops build
84
87
  Warning: '--idl' in args for canister foo may conflict with mops build — this flag is always set by mops build
88
+ Warning: '--stable-types' in args for canister foo may conflict with mops build — this flag is always set by mops build
85
89
  Error while compiling canister foo
86
90
  ENOENT: no such file or directory, open '.mops/.build/foo.did'",
87
91
  "stdout": "build canister foo",
@@ -50,6 +50,7 @@ describe("build", () => {
50
50
  const customOut = path.join(cwd, "custom-out");
51
51
  const customWasm = path.join(customOut, "main.wasm");
52
52
  const customDid = path.join(customOut, "main.did");
53
+ const customMost = path.join(customOut, "main.most");
53
54
  const defaultDid = path.join(cwd, ".mops/.build/main.did");
54
55
 
55
56
  try {
@@ -57,6 +58,7 @@ describe("build", () => {
57
58
  expect(result.exitCode).toBe(0);
58
59
  expect(existsSync(customWasm)).toBe(true);
59
60
  expect(existsSync(customDid)).toBe(true);
61
+ expect(existsSync(customMost)).toBe(true);
60
62
  expect(existsSync(defaultDid)).toBe(false);
61
63
  } finally {
62
64
  cleanFixture(cwd, customOut);
@@ -76,6 +78,7 @@ describe("build", () => {
76
78
  expect(result.exitCode).toBe(0);
77
79
  expect(existsSync(path.join(outputDir, "foo.wasm"))).toBe(true);
78
80
  expect(existsSync(path.join(outputDir, "foo.did"))).toBe(true);
81
+ expect(existsSync(path.join(outputDir, "foo.most"))).toBe(true);
79
82
  } finally {
80
83
  cleanFixture(cwd, outputDir);
81
84
  }
@@ -88,7 +91,7 @@ describe("build", () => {
88
91
 
89
92
  try {
90
93
  await cliSnapshot(
91
- ["build", "foo", "--", "-o", "x", "-c", "--idl"],
94
+ ["build", "foo", "--", "-o", "x", "-c", "--idl", "--stable-types"],
92
95
  { cwd },
93
96
  1,
94
97
  );
package/tests/cli.test.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { describe, expect, test } from "@jest/globals";
1
+ import { describe, expect, jest, test } from "@jest/globals";
2
+ import { existsSync, rmSync } from "node:fs";
3
+ import path from "path";
2
4
  import { cli } from "./helpers";
3
5
 
4
6
  describe("cli", () => {
@@ -10,3 +12,63 @@ describe("cli", () => {
10
12
  expect((await cli(["--help"])).stdout).toMatch(/^Usage: mops/m);
11
13
  });
12
14
  });
15
+
16
+ describe("install", () => {
17
+ jest.setTimeout(120_000);
18
+
19
+ test("creates mops.lock automatically on first install", async () => {
20
+ const cwd = path.join(import.meta.dirname, "install/success");
21
+ const lockFile = path.join(cwd, "mops.lock");
22
+ rmSync(lockFile, { force: true });
23
+ try {
24
+ // Unset CI so checkIntegrity uses the local default ("update")
25
+ const result = await cli(["install"], { cwd, env: { CI: undefined } });
26
+ expect(result.exitCode).toBe(0);
27
+ expect(existsSync(lockFile)).toBe(true);
28
+ expect(result.stdout).toMatch(/mops\.lock created/);
29
+ } finally {
30
+ rmSync(lockFile, { force: true });
31
+ rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
32
+ }
33
+ });
34
+
35
+ test("does not print 'mops.lock created' on subsequent installs", async () => {
36
+ const cwd = path.join(import.meta.dirname, "install/success");
37
+ const lockFile = path.join(cwd, "mops.lock");
38
+ rmSync(lockFile, { force: true });
39
+ try {
40
+ // Unset CI so checkIntegrity uses the local default ("update")
41
+ const first = await cli(["install"], { cwd, env: { CI: undefined } });
42
+ expect(first.exitCode).toBe(0);
43
+ expect(first.stdout).toMatch(/mops\.lock created/);
44
+ const result = await cli(["install"], { cwd, env: { CI: undefined } });
45
+ expect(result.exitCode).toBe(0);
46
+ expect(existsSync(lockFile)).toBe(true);
47
+ expect(result.stdout).not.toMatch(/mops\.lock created/);
48
+ } finally {
49
+ rmSync(lockFile, { force: true });
50
+ rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
51
+ }
52
+ });
53
+
54
+ test("does not create mops.lock when --lock ignore is passed", async () => {
55
+ const cwd = path.join(import.meta.dirname, "install/success");
56
+ const lockFile = path.join(cwd, "mops.lock");
57
+ rmSync(lockFile, { force: true });
58
+ try {
59
+ // Unset CI for consistency; --lock ignore bypasses auto-detection regardless
60
+ const result = await cli(["install", "--lock", "ignore"], {
61
+ cwd,
62
+ env: { CI: undefined },
63
+ });
64
+ expect(result.exitCode).toBe(0);
65
+ expect(existsSync(lockFile)).toBe(false);
66
+ } finally {
67
+ rmSync(lockFile, { force: true });
68
+ rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
69
+ }
70
+ });
71
+
72
+ // mops add/remove/update/sync are not separately tested here because they
73
+ // all route through the same checkIntegrity code path tested above.
74
+ });
package/tests/helpers.ts CHANGED
@@ -5,6 +5,7 @@ import { fileURLToPath } from "url";
5
5
 
6
6
  export interface CliOptions {
7
7
  cwd?: string;
8
+ env?: Record<string, string | undefined>;
8
9
  }
9
10
 
10
11
  // When MOPS_TEST_GLOBAL is set, invoke the globally-installed `mops` binary
@@ -12,12 +13,12 @@ export interface CliOptions {
12
13
  // code path where the binary lives outside the project tree.
13
14
  const useGlobalBinary = Boolean(process.env.MOPS_TEST_GLOBAL);
14
15
 
15
- export const cli = async (args: string[], { cwd }: CliOptions = {}) => {
16
+ export const cli = async (args: string[], { cwd, env }: CliOptions = {}) => {
16
17
  const [cmd, cmdArgs] = useGlobalBinary
17
18
  ? ["mops", args]
18
19
  : ["npm", ["run", "--silent", "mops", "--", ...args]];
19
20
  return await execa(cmd, cmdArgs, {
20
- env: { ...process.env, ...(cwd != null && { MOPS_CWD: cwd }) },
21
+ env: { ...process.env, ...(cwd != null && { MOPS_CWD: cwd }), ...env },
21
22
  ...(cwd != null && { cwd }),
22
23
  stdio: "pipe",
23
24
  reject: false,
@@ -0,0 +1,2 @@
1
+ [dependencies]
2
+ core = "1.0.0"