Skip to content

Commit 2147e41

Browse files
authored
feat: auto-create mops.lock on first install (#457)
## Summary - `mops.lock` is now created automatically on the first run of any dependency command — no manual opt-in required - On first creation, a one-time hint is printed recommending that **applications** commit the file and **library authors** add it to `.gitignore` (same convention as Cargo) - Subsequent runs are idempotent — the hint does not repeat and no unnecessary writes occur ## Motivation Previously users had to run `mops i --lock update` once to bootstrap the lock file. This is inconsistent with every major package manager (npm, yarn, pnpm, Cargo, Bundler, Poetry) and meant most projects silently missed the reproducibility and integrity benefits of `mops.lock`. ## Changes - **`cli/integrity.ts`**: Collapse two-branch guard in `checkIntegrity` to a single ternary — non-CI now always defaults to `"update"`; add first-creation message in `updateLockFile` - **`cli/tests/cli.test.ts`**: Integration tests for auto-creation, idempotency, and `--lock ignore` opt-out; unset `CI` env to simulate local behavior - **`cli/tests/helpers.ts`**: Add `env` override field to `CliOptions` to allow unsetting env vars in tests - **`cli/tests/install/success/mops.toml`**: Dedicated fixture — avoids race condition with `build.test.ts` which uses `build/success` in parallel - **`docs/docs/10-mops.lock.md`**: Rewrote — removed "disabled by default" notice, added Libraries vs Applications guidance (Cargo-style), added CI section, fixed "Opting out" to list only commands that accept `--lock` - **`cli/CHANGELOG.md`**: Entry under `## Next` ## Behavior preserved - `--lock ignore` still fully bypasses creation on any command - CI (`CI=1`) still defaults to `"check"` and silently skips if no lock file exists - All internal `silent: true` / `lock: "ignore"` call sites are unaffected ## How to test ```bash # First install creates the lock file rm -f mops.lock && mops install # Prints: "mops.lock created. Applications: commit this file. Libraries: add mops.lock to .gitignore." # Second install is silent — no duplicate hint mops install # Opt-out works rm -f mops.lock && mops install --lock ignore # mops.lock does NOT exist ```
1 parent 0b29ae9 commit 2147e41

6 files changed

Lines changed: 119 additions & 30 deletions

File tree

cli/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Next
44

5+
- `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`.
6+
57
## 2.7.0
68

79
- `mops publish` no longer requires a `repository` field — it is now optional metadata (used by the registry UI for source links)

cli/integrity.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,8 @@ type LockFile = LockFileV1 | LockFileV2 | LockFileV3;
3636
export async function checkIntegrity(lock?: "check" | "update" | "ignore") {
3737
let force = !!lock;
3838

39-
if (
40-
!lock &&
41-
!process.env["CI"] &&
42-
fs.existsSync(path.join(getRootDir(), "mops.lock"))
43-
) {
44-
lock = "update";
45-
}
46-
4739
if (!lock) {
48-
lock = process.env["CI"] ? "check" : "ignore";
40+
lock = process.env["CI"] ? "check" : "update";
4941
}
5042

5143
if (lock === "update") {
@@ -197,7 +189,13 @@ export async function updateLockFile() {
197189

198190
let rootDir = getRootDir();
199191
let lockFile = path.join(rootDir, "mops.lock");
192+
let isNew = !fs.existsSync(lockFile);
200193
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+
}
201199
}
202200

203201
// compare hashes of local files with hashes from the lock file

cli/tests/cli.test.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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";
24
import { cli } from "./helpers";
35

46
describe("cli", () => {
@@ -10,3 +12,63 @@ describe("cli", () => {
1012
expect((await cli(["--help"])).stdout).toMatch(/^Usage: mops/m);
1113
});
1214
});
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+
});

cli/tests/helpers.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,20 @@ import { fileURLToPath } from "url";
55

66
export interface CliOptions {
77
cwd?: string;
8+
env?: Record<string, string | undefined>;
89
}
910

1011
// When MOPS_TEST_GLOBAL is set, invoke the globally-installed `mops` binary
1112
// directly rather than the npm script. This exercises the real global-install
1213
// code path where the binary lives outside the project tree.
1314
const useGlobalBinary = Boolean(process.env.MOPS_TEST_GLOBAL);
1415

15-
export const cli = async (args: string[], { cwd }: CliOptions = {}) => {
16+
export const cli = async (args: string[], { cwd, env }: CliOptions = {}) => {
1617
const [cmd, cmdArgs] = useGlobalBinary
1718
? ["mops", args]
1819
: ["npm", ["run", "--silent", "mops", "--", ...args]];
1920
return await execa(cmd, cmdArgs, {
20-
env: { ...process.env, ...(cwd != null && { MOPS_CWD: cwd }) },
21+
env: { ...process.env, ...(cwd != null && { MOPS_CWD: cwd }), ...env },
2122
...(cwd != null && { cwd }),
2223
stdio: "pipe",
2324
reject: false,
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[dependencies]
2+
core = "1.0.0"

docs/docs/10-mops.lock.md

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,54 @@ sidebar_label: mops.lock
55

66
# `mops.lock` file
77

8-
:::info
9-
Currently lockfile is disabled by default. You can enable it by running `mops i --lock update` once.
8+
`mops.lock` is used to ensure integrity of dependencies, so that you can be sure that all dependencies have exactly the same source code as they had when the package author published them to the Mops Registry.
109

11-
When `mops.lock` file exists, no need to specify `--lock` flag.
12-
:::
10+
`mops.lock` is created automatically the first time dependencies are installed, and kept up to date on every subsequent run. It is triggered by:
11+
- `mops install`
12+
- `mops add`
13+
- `mops remove`
14+
- `mops update`
15+
- `mops sync`
16+
- `mops init` (when it installs dependencies)
1317

14-
`mops.lock` is used to ensure integrity of dependencies, so that you can be sure that all dependencies have exactly the same source code as they had when the package author published them to the Mops Registry.
18+
`mops.lock` is maintained by Mops and should not be manually edited.
19+
20+
## Should you commit `mops.lock`?
21+
22+
The answer depends on whether your project is an **application** or a **library**.
23+
24+
**Applications** (canisters, scripts, frontends) — commit `mops.lock`. It guarantees that every developer and CI environment installs the exact same dependency versions.
25+
26+
**Libraries** (packages published to the Mops registry) — add `mops.lock` to `.gitignore`. Your library will be used as a dependency inside other projects, and those projects will resolve their own dependency graph. Committing your lock file could mislead contributors into thinking the locked versions are significant.
1527

16-
A valid `mops.lock` speeds up `mops install` command because it avoids downloading intermediate versions of dependencies.
28+
```bash
29+
# .gitignore entry for library authors
30+
mops.lock
31+
```
1732

18-
_It's only faster when there are no global cached packages. For example you are running `mops install` inside a fresh Docker container. Or when you call `mops install` for the first time in a project._
33+
This is the same convention used by [Cargo](https://doc.rust-lang.org/cargo/faq.html#why-do-binaries-have-cargolock-in-version-control-but-not-libraries).
1934

20-
`mops.lock` contains the following information:
21-
- Hash of `[dependencies]` and `[dev-dependencies]` section of `mops.toml` file
35+
## Performance
36+
37+
A valid `mops.lock` speeds up `mops install` because it avoids resolving intermediate dependency versions.
38+
39+
_It's only faster when there are no globally cached packages — for example when running `mops install` inside a fresh Docker container or for the first time in a project._
40+
41+
## What `mops.lock` contains
42+
43+
- Hash of the `[dependencies]` and `[dev-dependencies]` sections of `mops.toml`
2244
- All transitive dependencies with the final resolved versions
23-
- Hash of each file of each dependency
45+
- Hash of each file of each dependency (retrieved from the Mops registry canister)
2446

25-
File hashes are retrieved from the mops registry canister.
47+
## CI environments
2648

27-
When `mops.lock` exists, it is updated(and checked) automatically when you run any of the following commands:
28-
- `mops add`
29-
- `mops remove`
30-
- `mops install`
31-
- `mops update`
32-
- `mops sync`
49+
In CI, if `mops.lock` does not exist, integrity checking is skipped and no lock file is created. To enforce the lock in CI, commit `mops.lock` to your repository before running CI.
50+
51+
## Opting out
52+
53+
To skip lock file creation and checks for a single run, pass `--lock ignore` to `mops install`, `mops add`, `mops remove`, `mops update`, or `mops sync`:
3354

34-
`mops.lock` maintained by Mops and should not be manually edited.
55+
```bash
56+
mops install --lock ignore
57+
mops add <package> --lock ignore
58+
```

0 commit comments

Comments
 (0)