ic-mops 2.14.1 → 2.15.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 +14 -0
- package/bundle/cli.tgz +0 -0
- package/cli.ts +143 -3
- package/commands/build.ts +30 -77
- package/commands/check-stable.ts +4 -0
- package/commands/check.ts +26 -22
- package/commands/deployed.ts +162 -0
- package/commands/generate.ts +201 -0
- package/commands/lint.ts +6 -3
- package/dist/cli.js +75 -3
- package/dist/commands/build.d.ts +6 -0
- package/dist/commands/build.js +26 -52
- package/dist/commands/check-stable.d.ts +2 -0
- package/dist/commands/check-stable.js +2 -2
- package/dist/commands/check.d.ts +2 -0
- package/dist/commands/check.js +20 -21
- package/dist/commands/deployed.d.ts +10 -0
- package/dist/commands/deployed.js +97 -0
- package/dist/commands/generate.d.ts +6 -0
- package/dist/commands/generate.js +124 -0
- package/dist/commands/lint.d.ts +2 -0
- package/dist/commands/lint.js +1 -1
- package/dist/helpers/autofix-motoko.d.ts +2 -0
- package/dist/helpers/autofix-motoko.js +52 -49
- package/dist/helpers/migrations.d.ts +1 -1
- package/dist/helpers/migrations.js +8 -4
- package/dist/helpers/moc-args.d.ts +25 -0
- package/dist/helpers/moc-args.js +58 -0
- package/dist/package.json +6 -6
- package/dist/tests/check-fix-utf8.test.d.ts +1 -0
- package/dist/tests/check-fix-utf8.test.js +38 -0
- package/dist/tests/check.test.js +21 -6
- package/dist/tests/deployed.test.d.ts +1 -0
- package/dist/tests/deployed.test.js +173 -0
- package/dist/tests/generate.test.d.ts +1 -0
- package/dist/tests/generate.test.js +107 -0
- package/dist/tests/helpers.d.ts +8 -0
- package/dist/tests/helpers.js +28 -3
- package/dist/tests/migrate.test.js +5 -19
- package/dist/types.d.ts +3 -0
- package/helpers/autofix-motoko.ts +73 -64
- package/helpers/migrations.ts +8 -3
- package/helpers/moc-args.ts +123 -0
- package/package.json +6 -6
- package/tests/__snapshots__/check-fix-utf8.test.ts.snap +27 -0
- package/tests/__snapshots__/check.test.ts.snap +4 -17
- package/tests/__snapshots__/deployed.test.ts.snap +10 -0
- package/tests/__snapshots__/generate.test.ts.snap +39 -0
- package/tests/check/fix-utf8/mops.toml +5 -0
- package/tests/check/fix-utf8/multibyte.mo +15 -0
- package/tests/check-fix-utf8.test.ts +51 -0
- package/tests/check.test.ts +24 -11
- package/tests/deployed/basic/main.mo +4 -0
- package/tests/deployed/basic/mops.toml +8 -0
- package/tests/deployed/multi/bar.mo +1 -0
- package/tests/deployed/multi/foo.mo +1 -0
- package/tests/deployed/multi/mops.toml +11 -0
- package/tests/deployed.test.ts +249 -0
- package/tests/generate/basic/candid/bar.did +4 -0
- package/tests/generate/basic/mops.toml +12 -0
- package/tests/generate/basic/src/Bar.mo +5 -0
- package/tests/generate/basic/src/Foo.mo +5 -0
- package/tests/generate/error/mops.toml +8 -0
- package/tests/generate/error/src/Broken.mo +5 -0
- package/tests/generate.test.ts +140 -0
- package/tests/helpers.ts +31 -3
- package/tests/migrate.test.ts +7 -22
- package/types.ts +3 -0
package/dist/tests/check.test.js
CHANGED
|
@@ -12,12 +12,6 @@ describe("check", () => {
|
|
|
12
12
|
await cliSnapshot(["check", "Error.mo"], { cwd }, 1);
|
|
13
13
|
await cliSnapshot(["check", "Ok.mo", "Error.mo"], { cwd }, 1);
|
|
14
14
|
});
|
|
15
|
-
// The verbose snapshot shows a single "moc ... [both files]" line, proving the
|
|
16
|
-
// whole set is checked in one invocation rather than one moc call per file.
|
|
17
|
-
test("multiple files in a single invocation", async () => {
|
|
18
|
-
const cwd = path.join(import.meta.dirname, "check/success");
|
|
19
|
-
await cliSnapshot(["check", "Ok.mo", "Warning.mo", "--verbose"], { cwd }, 0);
|
|
20
|
-
});
|
|
21
15
|
test("warning", async () => {
|
|
22
16
|
const cwd = path.join(import.meta.dirname, "check/success");
|
|
23
17
|
const result = await cliSnapshot(["check", "Warning.mo"], { cwd }, 0);
|
|
@@ -143,4 +137,25 @@ describe("check", () => {
|
|
|
143
137
|
expect(result.exitCode).toBe(0);
|
|
144
138
|
expect(result.stdout).toMatch(/✓ Lint fixes applied/);
|
|
145
139
|
});
|
|
140
|
+
// The migrations-chain fixture has 4 migrations with check-limit=3, so the
|
|
141
|
+
// oldest is trimmed by default (staged dir + M0254 suppression). --no-check-limit
|
|
142
|
+
// must point moc at the real chain dir and drop the trimming side effects.
|
|
143
|
+
const migrationsChain = "check-stable/migrations-chain";
|
|
144
|
+
test("check-limit trims the migration chain by default", async () => {
|
|
145
|
+
const cwd = path.join(import.meta.dirname, migrationsChain);
|
|
146
|
+
const result = await cli(["check", "--verbose"], { cwd });
|
|
147
|
+
expect(result.exitCode).toBe(0);
|
|
148
|
+
expect(result.stdout).toMatch(/--enhanced-migration=[^"]*\.migrations-/);
|
|
149
|
+
expect(result.stdout).toMatch(/-A=M0254/);
|
|
150
|
+
});
|
|
151
|
+
test("--no-check-limit uses the full migration chain", async () => {
|
|
152
|
+
const cwd = path.join(import.meta.dirname, migrationsChain);
|
|
153
|
+
const result = await cli(["check", "--verbose", "--no-check-limit"], {
|
|
154
|
+
cwd,
|
|
155
|
+
});
|
|
156
|
+
expect(result.exitCode).toBe(0);
|
|
157
|
+
expect(result.stdout).toMatch(/--enhanced-migration=/);
|
|
158
|
+
expect(result.stdout).not.toMatch(/\.migrations-/);
|
|
159
|
+
expect(result.stdout).not.toMatch(/-A=M0254/);
|
|
160
|
+
});
|
|
146
161
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { cli, normalizePaths, useTempFixtures } from "./helpers";
|
|
6
|
+
describe("deployed", () => {
|
|
7
|
+
const makeTempFixture = useTempFixtures(path.join(import.meta.dirname, "deployed"));
|
|
8
|
+
describe("init", () => {
|
|
9
|
+
test("creates baseline .most and sets [check-stable].path", async () => {
|
|
10
|
+
const cwd = await makeTempFixture("basic");
|
|
11
|
+
const result = await cli(["deployed", "init"], { cwd });
|
|
12
|
+
expect(result.exitCode).toBe(0);
|
|
13
|
+
const baseline = path.join(cwd, "deployed", "backend.most");
|
|
14
|
+
expect(existsSync(baseline)).toBe(true);
|
|
15
|
+
expect(readFileSync(baseline, "utf-8")).toBe("// Version: 1.0.0\nactor { };\n");
|
|
16
|
+
const toml = readFileSync(path.join(cwd, "mops.toml"), "utf-8");
|
|
17
|
+
expect(toml).toMatch(/\[canisters\.backend\.check-stable\]/);
|
|
18
|
+
expect(toml).toMatch(/path\s*=\s*"deployed\/backend\.most"/);
|
|
19
|
+
expect({
|
|
20
|
+
exitCode: result.exitCode,
|
|
21
|
+
stdout: normalizePaths(result.stdout),
|
|
22
|
+
stderr: normalizePaths(result.stderr),
|
|
23
|
+
}).toMatchSnapshot();
|
|
24
|
+
});
|
|
25
|
+
test("idempotent — second run is a no-op", async () => {
|
|
26
|
+
const cwd = await makeTempFixture("basic");
|
|
27
|
+
await cli(["deployed", "init"], { cwd });
|
|
28
|
+
const tomlBefore = readFileSync(path.join(cwd, "mops.toml"), "utf-8");
|
|
29
|
+
const result = await cli(["deployed", "init"], { cwd });
|
|
30
|
+
expect(result.exitCode).toBe(0);
|
|
31
|
+
const tomlAfter = readFileSync(path.join(cwd, "mops.toml"), "utf-8");
|
|
32
|
+
expect(tomlAfter).toBe(tomlBefore);
|
|
33
|
+
});
|
|
34
|
+
test("warns when [check-stable].path is set elsewhere", async () => {
|
|
35
|
+
const cwd = await makeTempFixture("basic");
|
|
36
|
+
const tomlPath = path.join(cwd, "mops.toml");
|
|
37
|
+
const toml = readFileSync(tomlPath, "utf-8");
|
|
38
|
+
await writeFile(tomlPath, toml +
|
|
39
|
+
'\n[canisters.backend.check-stable]\npath = "elsewhere/backend.most"\n');
|
|
40
|
+
const result = await cli(["deployed", "init"], { cwd });
|
|
41
|
+
expect(result.exitCode).toBe(0);
|
|
42
|
+
expect(result.stderr).toMatch(/check-stable\]\.path = "elsewhere\/backend\.most"/);
|
|
43
|
+
expect(result.stderr).toMatch(/does not match where `mops deployed`/);
|
|
44
|
+
expect(existsSync(path.join(cwd, "deployed", "backend.most"))).toBe(true);
|
|
45
|
+
// mops.toml should not have been rewritten
|
|
46
|
+
expect(readFileSync(tomlPath, "utf-8")).toBe(toml +
|
|
47
|
+
'\n[canisters.backend.check-stable]\npath = "elsewhere/backend.most"\n');
|
|
48
|
+
});
|
|
49
|
+
test("respects [deployed].dir", async () => {
|
|
50
|
+
const cwd = await makeTempFixture("basic");
|
|
51
|
+
const tomlPath = path.join(cwd, "mops.toml");
|
|
52
|
+
const toml = readFileSync(tomlPath, "utf-8");
|
|
53
|
+
await writeFile(tomlPath, `${toml}\n[deployed]\ndir = "snapshots"\n`);
|
|
54
|
+
const result = await cli(["deployed", "init"], { cwd });
|
|
55
|
+
expect(result.exitCode).toBe(0);
|
|
56
|
+
expect(existsSync(path.join(cwd, "snapshots", "backend.most"))).toBe(true);
|
|
57
|
+
expect(readFileSync(tomlPath, "utf-8")).toMatch(/path\s*=\s*"snapshots\/backend\.most"/);
|
|
58
|
+
});
|
|
59
|
+
test("init for shorthand canister entry promotes it to a table", async () => {
|
|
60
|
+
const cwd = await makeTempFixture("basic");
|
|
61
|
+
const tomlPath = path.join(cwd, "mops.toml");
|
|
62
|
+
await writeFile(tomlPath, '[toolchain]\nmoc = "1.5.0"\n\n[canisters]\nbackend = "main.mo"\n');
|
|
63
|
+
const result = await cli(["deployed", "init"], { cwd });
|
|
64
|
+
expect(result.exitCode).toBe(0);
|
|
65
|
+
const after = readFileSync(tomlPath, "utf-8");
|
|
66
|
+
expect(after).toMatch(/\[canisters\.backend\]/);
|
|
67
|
+
expect(after).toMatch(/main\s*=\s*"main\.mo"/);
|
|
68
|
+
expect(after).toMatch(/\[canisters\.backend\.check-stable\]/);
|
|
69
|
+
expect(after).toMatch(/path\s*=\s*"deployed\/backend\.most"/);
|
|
70
|
+
});
|
|
71
|
+
test("errors on unknown canister name", async () => {
|
|
72
|
+
const cwd = await makeTempFixture("basic");
|
|
73
|
+
const result = await cli(["deployed", "init", "ghost"], { cwd });
|
|
74
|
+
expect(result.exitCode).toBe(1);
|
|
75
|
+
expect(result.stderr).toMatch(/not found in mops\.toml/);
|
|
76
|
+
});
|
|
77
|
+
test("preserves an existing baseline file when wiring [check-stable].path", async () => {
|
|
78
|
+
const cwd = await makeTempFixture("basic");
|
|
79
|
+
const baseline = path.join(cwd, "deployed", "backend.most");
|
|
80
|
+
const customMost = "// Version: 1.0.0\nactor {\n stable var x : Nat\n};\n";
|
|
81
|
+
// pre-create the baseline (e.g. real prod snapshot copied in by the user)
|
|
82
|
+
await mkdir(path.join(cwd, "deployed"), { recursive: true });
|
|
83
|
+
await writeFile(baseline, customMost);
|
|
84
|
+
const result = await cli(["deployed", "init"], { cwd });
|
|
85
|
+
expect(result.exitCode).toBe(0);
|
|
86
|
+
expect(readFileSync(baseline, "utf-8")).toBe(customMost);
|
|
87
|
+
expect(readFileSync(path.join(cwd, "mops.toml"), "utf-8")).toMatch(/\[canisters\.backend\.check-stable\][\s\S]*path\s*=\s*"deployed\/backend\.most"/);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe("post-deploy hook", () => {
|
|
91
|
+
test("copies built .most into deployed/", async () => {
|
|
92
|
+
const cwd = await makeTempFixture("basic");
|
|
93
|
+
const buildResult = await cli(["build"], { cwd });
|
|
94
|
+
expect(buildResult.exitCode).toBe(0);
|
|
95
|
+
const result = await cli(["deployed"], { cwd });
|
|
96
|
+
expect(result.exitCode).toBe(0);
|
|
97
|
+
const built = path.join(cwd, ".mops", ".build", "backend.most");
|
|
98
|
+
const promoted = path.join(cwd, "deployed", "backend.most");
|
|
99
|
+
expect(existsSync(promoted)).toBe(true);
|
|
100
|
+
expect(readFileSync(promoted, "utf-8")).toBe(readFileSync(built, "utf-8"));
|
|
101
|
+
});
|
|
102
|
+
test("errors when source .most is missing", async () => {
|
|
103
|
+
const cwd = await makeTempFixture("basic");
|
|
104
|
+
const result = await cli(["deployed", "backend"], { cwd });
|
|
105
|
+
expect(result.exitCode).toBe(1);
|
|
106
|
+
expect(result.stderr).toMatch(/No built \.most/);
|
|
107
|
+
expect(result.stderr).toMatch(/Run `mops build backend` first/);
|
|
108
|
+
});
|
|
109
|
+
test("warns when [check-stable].path differs from <dir>/<name>.most", async () => {
|
|
110
|
+
const cwd = await makeTempFixture("basic");
|
|
111
|
+
const tomlPath = path.join(cwd, "mops.toml");
|
|
112
|
+
const toml = readFileSync(tomlPath, "utf-8");
|
|
113
|
+
await writeFile(tomlPath, toml +
|
|
114
|
+
'\n[canisters.backend.check-stable]\npath = "elsewhere/backend.most"\n');
|
|
115
|
+
const buildResult = await cli(["build"], { cwd });
|
|
116
|
+
expect(buildResult.exitCode).toBe(0);
|
|
117
|
+
const result = await cli(["deployed"], { cwd });
|
|
118
|
+
expect(result.exitCode).toBe(0);
|
|
119
|
+
expect(result.stderr).toMatch(/check-stable\]\.path = "elsewhere\/backend\.most"/);
|
|
120
|
+
expect(result.stderr).toMatch(/won't see updates from `mops deployed`/);
|
|
121
|
+
});
|
|
122
|
+
test("respects [deployed].dir from config", async () => {
|
|
123
|
+
const cwd = await makeTempFixture("basic");
|
|
124
|
+
const tomlPath = path.join(cwd, "mops.toml");
|
|
125
|
+
const toml = readFileSync(tomlPath, "utf-8");
|
|
126
|
+
await writeFile(tomlPath, `${toml}\n[deployed]\ndir = "snapshots"\n`);
|
|
127
|
+
await cli(["build"], { cwd });
|
|
128
|
+
const result = await cli(["deployed"], { cwd });
|
|
129
|
+
expect(result.exitCode).toBe(0);
|
|
130
|
+
expect(existsSync(path.join(cwd, "snapshots", "backend.most"))).toBe(true);
|
|
131
|
+
expect(existsSync(path.join(cwd, "deployed", "backend.most"))).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
test("--build-dir and --dir overrides", async () => {
|
|
134
|
+
const cwd = await makeTempFixture("basic");
|
|
135
|
+
const customOut = "custom-build";
|
|
136
|
+
const customDir = "snapshots";
|
|
137
|
+
const buildResult = await cli(["build", "backend", "--output", customOut], { cwd });
|
|
138
|
+
expect(buildResult.exitCode).toBe(0);
|
|
139
|
+
const result = await cli(["deployed", "--build-dir", customOut, "--dir", customDir], { cwd });
|
|
140
|
+
expect(result.exitCode).toBe(0);
|
|
141
|
+
expect(existsSync(path.join(cwd, customDir, "backend.most"))).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
test("with no canister names promotes all canisters", async () => {
|
|
144
|
+
const cwd = await makeTempFixture("multi");
|
|
145
|
+
const buildResult = await cli(["build"], { cwd });
|
|
146
|
+
expect(buildResult.exitCode).toBe(0);
|
|
147
|
+
const result = await cli(["deployed"], { cwd });
|
|
148
|
+
expect(result.exitCode).toBe(0);
|
|
149
|
+
expect(existsSync(path.join(cwd, "deployed", "foo.most"))).toBe(true);
|
|
150
|
+
expect(existsSync(path.join(cwd, "deployed", "bar.most"))).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
test("named canister filters to that canister only", async () => {
|
|
153
|
+
const cwd = await makeTempFixture("multi");
|
|
154
|
+
await cli(["build"], { cwd });
|
|
155
|
+
const result = await cli(["deployed", "foo"], { cwd });
|
|
156
|
+
expect(result.exitCode).toBe(0);
|
|
157
|
+
expect(existsSync(path.join(cwd, "deployed", "foo.most"))).toBe(true);
|
|
158
|
+
expect(existsSync(path.join(cwd, "deployed", "bar.most"))).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
describe("end-to-end with check-stable", () => {
|
|
162
|
+
test("init + build + deployed wires the check-stable baseline", async () => {
|
|
163
|
+
const cwd = await makeTempFixture("basic");
|
|
164
|
+
await cli(["deployed", "init"], { cwd });
|
|
165
|
+
await cli(["build"], { cwd });
|
|
166
|
+
const promoteResult = await cli(["deployed"], { cwd });
|
|
167
|
+
expect(promoteResult.exitCode).toBe(0);
|
|
168
|
+
const checkResult = await cli(["check-stable"], { cwd });
|
|
169
|
+
expect(checkResult.exitCode).toBe(0);
|
|
170
|
+
expect(checkResult.stdout).toMatch(/Stable compatibility check passed/);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { cp, rm } from "node:fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { cli, cliSnapshot } from "./helpers";
|
|
6
|
+
const fixturesDir = path.join(import.meta.dirname, "generate");
|
|
7
|
+
describe("generate candid", () => {
|
|
8
|
+
jest.setTimeout(120_000);
|
|
9
|
+
const tempDirs = [];
|
|
10
|
+
async function makeTempFixture(fixture) {
|
|
11
|
+
const src = path.join(fixturesDir, fixture);
|
|
12
|
+
const dest = path.join(fixturesDir, `_tmp_${fixture}_${Date.now()}`);
|
|
13
|
+
await cp(src, dest, { recursive: true });
|
|
14
|
+
tempDirs.push(dest);
|
|
15
|
+
return dest;
|
|
16
|
+
}
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
for (const dir of tempDirs) {
|
|
19
|
+
await rm(dir, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
tempDirs.length = 0;
|
|
22
|
+
});
|
|
23
|
+
test("default path: writes <main-dir>/<name>.did and sets [canisters.<name>].candid", async () => {
|
|
24
|
+
const cwd = await makeTempFixture("basic");
|
|
25
|
+
const tomlBefore = readFileSync(path.join(cwd, "mops.toml"), "utf-8");
|
|
26
|
+
expect(tomlBefore).not.toMatch(/"src\/foo\.did"/);
|
|
27
|
+
await cliSnapshot(["generate", "candid", "foo"], { cwd }, 0);
|
|
28
|
+
const did = readFileSync(path.join(cwd, "src/foo.did"), "utf-8");
|
|
29
|
+
expect(did).toMatchSnapshot("src/foo.did");
|
|
30
|
+
const tomlAfter = readFileSync(path.join(cwd, "mops.toml"), "utf-8");
|
|
31
|
+
expect(tomlAfter).toMatch(/candid\s*=\s*"src\/foo\.did"/);
|
|
32
|
+
});
|
|
33
|
+
test("configured path: overwrites in place and does not touch mops.toml", async () => {
|
|
34
|
+
const cwd = await makeTempFixture("basic");
|
|
35
|
+
const tomlBefore = readFileSync(path.join(cwd, "mops.toml"), "utf-8");
|
|
36
|
+
const didBefore = readFileSync(path.join(cwd, "candid/bar.did"), "utf-8");
|
|
37
|
+
expect(didBefore).toMatch(/Hand-curated/);
|
|
38
|
+
await cliSnapshot(["generate", "candid", "bar"], { cwd }, 0);
|
|
39
|
+
const didAfter = readFileSync(path.join(cwd, "candid/bar.did"), "utf-8");
|
|
40
|
+
expect(didAfter).not.toMatch(/Hand-curated/);
|
|
41
|
+
expect(didAfter).toMatch(/greet/);
|
|
42
|
+
expect(readFileSync(path.join(cwd, "mops.toml"), "utf-8")).toBe(tomlBefore);
|
|
43
|
+
});
|
|
44
|
+
test("no canister names: processes all canisters", async () => {
|
|
45
|
+
const cwd = await makeTempFixture("basic");
|
|
46
|
+
await cliSnapshot(["generate", "candid"], { cwd }, 0);
|
|
47
|
+
expect(existsSync(path.join(cwd, "src/foo.did"))).toBe(true);
|
|
48
|
+
const barDid = readFileSync(path.join(cwd, "candid/bar.did"), "utf-8");
|
|
49
|
+
expect(barDid).toMatch(/greet/);
|
|
50
|
+
expect(barDid).not.toMatch(/Hand-curated/);
|
|
51
|
+
});
|
|
52
|
+
test("--output writes to the given path and does not touch mops.toml", async () => {
|
|
53
|
+
const cwd = await makeTempFixture("basic");
|
|
54
|
+
const outPath = path.join(cwd, "out/custom.did");
|
|
55
|
+
const tomlBefore = readFileSync(path.join(cwd, "mops.toml"), "utf-8");
|
|
56
|
+
const result = await cli(["generate", "candid", "foo", "-o", outPath], {
|
|
57
|
+
cwd,
|
|
58
|
+
});
|
|
59
|
+
expect(result.exitCode).toBe(0);
|
|
60
|
+
expect(existsSync(outPath)).toBe(true);
|
|
61
|
+
expect(readFileSync(outPath, "utf-8")).toMatch(/call/);
|
|
62
|
+
expect(existsSync(path.join(cwd, "src/foo.did"))).toBe(false);
|
|
63
|
+
expect(readFileSync(path.join(cwd, "mops.toml"), "utf-8")).toBe(tomlBefore);
|
|
64
|
+
});
|
|
65
|
+
test("--output rejected with multiple canisters", async () => {
|
|
66
|
+
const cwd = await makeTempFixture("basic");
|
|
67
|
+
const result = await cli(["generate", "candid", "foo", "bar", "-o", "x.did"], { cwd });
|
|
68
|
+
expect(result.exitCode).toBe(1);
|
|
69
|
+
expect(result.stderr).toMatch(/single canister/i);
|
|
70
|
+
});
|
|
71
|
+
test("unknown canister name errors", async () => {
|
|
72
|
+
const cwd = await makeTempFixture("basic");
|
|
73
|
+
const result = await cli(["generate", "candid", "nope"], { cwd });
|
|
74
|
+
expect(result.exitCode).toBe(1);
|
|
75
|
+
expect(result.stderr).toMatch(/not found in mops\.toml/i);
|
|
76
|
+
});
|
|
77
|
+
test("moc failure: leaves destination and mops.toml untouched", async () => {
|
|
78
|
+
const cwd = await makeTempFixture("error");
|
|
79
|
+
const tomlBefore = readFileSync(path.join(cwd, "mops.toml"), "utf-8");
|
|
80
|
+
const result = await cli(["generate", "candid", "broken"], { cwd });
|
|
81
|
+
expect(result.exitCode).toBe(1);
|
|
82
|
+
expect(result.stderr).toMatch(/Failed to generate Candid/);
|
|
83
|
+
expect(existsSync(path.join(cwd, "src/broken.did"))).toBe(false);
|
|
84
|
+
expect(readFileSync(path.join(cwd, "mops.toml"), "utf-8")).toBe(tomlBefore);
|
|
85
|
+
});
|
|
86
|
+
test("rejects destination inside .mops/", async () => {
|
|
87
|
+
const cwd = await makeTempFixture("basic");
|
|
88
|
+
const result = await cli(["generate", "candid", "foo", "-o", ".mops/foo.did"], { cwd });
|
|
89
|
+
expect(result.exitCode).toBe(1);
|
|
90
|
+
expect(result.stderr).toMatch(/\.mops\//);
|
|
91
|
+
expect(result.stderr).toMatch(/Refusing/i);
|
|
92
|
+
});
|
|
93
|
+
test("creates file when [canisters.<name>].candid points to a missing path", async () => {
|
|
94
|
+
const cwd = await makeTempFixture("basic");
|
|
95
|
+
// bar's candid path points to candid/bar.did — delete it first
|
|
96
|
+
await rm(path.join(cwd, "candid/bar.did"));
|
|
97
|
+
const result = await cli(["generate", "candid", "bar"], { cwd });
|
|
98
|
+
expect(result.exitCode).toBe(0);
|
|
99
|
+
expect(existsSync(path.join(cwd, "candid/bar.did"))).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
test("--output collision with another canister's candid prints a warning", async () => {
|
|
102
|
+
const cwd = await makeTempFixture("basic");
|
|
103
|
+
const result = await cli(["generate", "candid", "foo", "-o", "candid/bar.did"], { cwd });
|
|
104
|
+
expect(result.exitCode).toBe(0);
|
|
105
|
+
expect(result.stderr).toMatch(/collides with \[canisters\.bar\]\.candid/);
|
|
106
|
+
});
|
|
107
|
+
});
|
package/dist/tests/helpers.d.ts
CHANGED
|
@@ -21,3 +21,11 @@ export declare const cliSnapshot: (args: string[], options: CliOptions, exitCode
|
|
|
21
21
|
TZ?: string;
|
|
22
22
|
};
|
|
23
23
|
}>>;
|
|
24
|
+
/**
|
|
25
|
+
* Set up `afterEach` cleanup and return a `makeTempFixture(name)` function
|
|
26
|
+
* that copies a fixture subtree into a unique scratch dir under `fixturesDir`.
|
|
27
|
+
*
|
|
28
|
+
* Use in tests that mutate fixture files (toml edits, baseline file creation, etc.)
|
|
29
|
+
* so they don't pollute the source-controlled fixture.
|
|
30
|
+
*/
|
|
31
|
+
export declare function useTempFixtures(fixturesDir: string): (fixture: string) => Promise<string>;
|
package/dist/tests/helpers.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { expect } from "@jest/globals";
|
|
1
|
+
import { afterEach, expect } from "@jest/globals";
|
|
2
2
|
import { execa } from "execa";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { cp, rm } from "node:fs/promises";
|
|
4
|
+
import path, { dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
// When MOPS_TEST_GLOBAL is set, invoke the globally-installed `mops` binary
|
|
6
7
|
// directly rather than the npm script. This exercises the real global-install
|
|
7
8
|
// code path where the binary lives outside the project tree.
|
|
@@ -52,3 +53,27 @@ export const cliSnapshot = async (args, options, exitCode) => {
|
|
|
52
53
|
}).toMatchSnapshot();
|
|
53
54
|
return result;
|
|
54
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* Set up `afterEach` cleanup and return a `makeTempFixture(name)` function
|
|
58
|
+
* that copies a fixture subtree into a unique scratch dir under `fixturesDir`.
|
|
59
|
+
*
|
|
60
|
+
* Use in tests that mutate fixture files (toml edits, baseline file creation, etc.)
|
|
61
|
+
* so they don't pollute the source-controlled fixture.
|
|
62
|
+
*/
|
|
63
|
+
export function useTempFixtures(fixturesDir) {
|
|
64
|
+
const tempDirs = [];
|
|
65
|
+
afterEach(async () => {
|
|
66
|
+
for (const dir of tempDirs) {
|
|
67
|
+
await rm(dir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
tempDirs.length = 0;
|
|
70
|
+
});
|
|
71
|
+
return async (fixture) => {
|
|
72
|
+
const src = path.join(fixturesDir, fixture);
|
|
73
|
+
const suffix = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
74
|
+
const dest = path.join(fixturesDir, `_tmp_${fixture}_${suffix}`);
|
|
75
|
+
await cp(src, dest, { recursive: true });
|
|
76
|
+
tempDirs.push(dest);
|
|
77
|
+
return dest;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -1,30 +1,16 @@
|
|
|
1
|
-
import { describe, expect, test
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
2
|
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
import path from "path";
|
|
5
|
-
import { cli, cliSnapshot, normalizePaths } from "./helpers";
|
|
3
|
+
import { rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { cli, cliSnapshot, normalizePaths, useTempFixtures } from "./helpers";
|
|
6
6
|
const normalizeTimestamp = (text) => text.replace(/\d{8}_\d{6}/g, "<TIMESTAMP>");
|
|
7
|
-
const fixturesDir = path.join(import.meta.dirname, "migrate");
|
|
8
7
|
describe("migrate", () => {
|
|
9
|
-
const
|
|
10
|
-
async function makeTempFixture(fixture) {
|
|
11
|
-
const src = path.join(fixturesDir, fixture);
|
|
12
|
-
const dest = path.join(fixturesDir, `_tmp_${fixture}_${Date.now()}`);
|
|
13
|
-
await cp(src, dest, { recursive: true });
|
|
14
|
-
tempDirs.push(dest);
|
|
15
|
-
return dest;
|
|
16
|
-
}
|
|
8
|
+
const makeTempFixture = useTempFixtures(path.join(import.meta.dirname, "migrate"));
|
|
17
9
|
async function patchMigrations(cwd, extra) {
|
|
18
10
|
const tomlPath = path.join(cwd, "mops.toml");
|
|
19
11
|
const toml = readFileSync(tomlPath, "utf-8");
|
|
20
12
|
await writeFile(tomlPath, toml.replace('next = "next-migration"', `next = "next-migration"\n${extra}`));
|
|
21
13
|
}
|
|
22
|
-
afterEach(async () => {
|
|
23
|
-
for (const dir of tempDirs) {
|
|
24
|
-
await rm(dir, { recursive: true, force: true });
|
|
25
|
-
}
|
|
26
|
-
tempDirs.length = 0;
|
|
27
|
-
});
|
|
28
14
|
describe("migrate new", () => {
|
|
29
15
|
test("creates a migration file with timestamp and template", async () => {
|
|
30
16
|
const cwd = await makeTempFixture("basic");
|
package/dist/types.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { execa } from "execa";
|
|
4
|
-
import {
|
|
5
|
-
TextDocument,
|
|
6
|
-
type TextEdit,
|
|
7
|
-
} from "vscode-languageserver-textdocument";
|
|
4
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
8
5
|
|
|
9
6
|
interface MocSpan {
|
|
10
7
|
file: string;
|
|
8
|
+
// Optional: older moc versions don't emit byte offsets.
|
|
9
|
+
byte_start?: number | null;
|
|
10
|
+
byte_end?: number | null;
|
|
11
11
|
line_start: number;
|
|
12
12
|
column_start: number;
|
|
13
13
|
line_end: number;
|
|
@@ -43,9 +43,14 @@ export function parseDiagnostics(stdout: string | undefined): MocDiagnostic[] {
|
|
|
43
43
|
.filter((d) => d !== null);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
interface Edit {
|
|
47
|
+
span: MocSpan;
|
|
48
|
+
newText: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
interface DiagnosticFix {
|
|
47
52
|
code: string;
|
|
48
|
-
edits:
|
|
53
|
+
edits: Edit[];
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
function extractDiagnosticFixes(
|
|
@@ -54,30 +59,19 @@ function extractDiagnosticFixes(
|
|
|
54
59
|
const result = new Map<string, DiagnosticFix[]>();
|
|
55
60
|
|
|
56
61
|
for (const diag of diagnostics) {
|
|
57
|
-
const editsByFile = new Map<string,
|
|
62
|
+
const editsByFile = new Map<string, Edit[]>();
|
|
58
63
|
|
|
59
64
|
for (const span of diag.spans) {
|
|
60
65
|
if (
|
|
61
|
-
span.suggestion_applicability
|
|
62
|
-
span.suggested_replacement
|
|
66
|
+
span.suggestion_applicability !== "MachineApplicable" ||
|
|
67
|
+
span.suggested_replacement === null
|
|
63
68
|
) {
|
|
64
|
-
|
|
65
|
-
const edits = editsByFile.get(file) ?? [];
|
|
66
|
-
edits.push({
|
|
67
|
-
range: {
|
|
68
|
-
start: {
|
|
69
|
-
line: span.line_start - 1,
|
|
70
|
-
character: span.column_start - 1,
|
|
71
|
-
},
|
|
72
|
-
end: {
|
|
73
|
-
line: span.line_end - 1,
|
|
74
|
-
character: span.column_end - 1,
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
newText: span.suggested_replacement,
|
|
78
|
-
});
|
|
79
|
-
editsByFile.set(file, edits);
|
|
69
|
+
continue;
|
|
80
70
|
}
|
|
71
|
+
const file = resolve(span.file);
|
|
72
|
+
const edits = editsByFile.get(file) ?? [];
|
|
73
|
+
edits.push({ span, newText: span.suggested_replacement });
|
|
74
|
+
editsByFile.set(file, edits);
|
|
81
75
|
}
|
|
82
76
|
|
|
83
77
|
for (const [file, edits] of editsByFile) {
|
|
@@ -90,19 +84,6 @@ function extractDiagnosticFixes(
|
|
|
90
84
|
return result;
|
|
91
85
|
}
|
|
92
86
|
|
|
93
|
-
type Range = TextEdit["range"];
|
|
94
|
-
|
|
95
|
-
function normalizeRange(range: Range): Range {
|
|
96
|
-
const { start, end } = range;
|
|
97
|
-
if (
|
|
98
|
-
start.line > end.line ||
|
|
99
|
-
(start.line === end.line && start.character > end.character)
|
|
100
|
-
) {
|
|
101
|
-
return { start: end, end: start };
|
|
102
|
-
}
|
|
103
|
-
return range;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
87
|
interface OffsetEdit {
|
|
107
88
|
start: number;
|
|
108
89
|
end: number;
|
|
@@ -110,27 +91,53 @@ interface OffsetEdit {
|
|
|
110
91
|
}
|
|
111
92
|
|
|
112
93
|
/**
|
|
113
|
-
* Applies diagnostic fixes to a
|
|
94
|
+
* Applies diagnostic fixes to a file's contents, processing each diagnostic as
|
|
114
95
|
* an atomic unit. If any edit from a diagnostic overlaps with an already-accepted
|
|
115
96
|
* edit, the entire diagnostic is skipped (picked up in subsequent iterations).
|
|
116
97
|
* Based on vscode-languageserver-textdocument's TextDocument.applyEdits.
|
|
98
|
+
*
|
|
99
|
+
* Edits prefer `byte_start`/`byte_end` (byte-accurate on any UTF-8 source);
|
|
100
|
+
* falls back to line+column for older moc, which mis-applies on non-ASCII
|
|
101
|
+
* lines since moc's `column_*` are bytes but LSP expects UTF-16 code units.
|
|
117
102
|
*/
|
|
118
103
|
function applyDiagnosticFixes(
|
|
119
|
-
|
|
104
|
+
content: string,
|
|
120
105
|
fixes: DiagnosticFix[],
|
|
121
106
|
): { text: string; appliedCodes: string[] } {
|
|
107
|
+
let buf: Buffer | null = null;
|
|
108
|
+
const byteToOffset = (byteOffset: number): number => {
|
|
109
|
+
buf ??= Buffer.from(content, "utf8");
|
|
110
|
+
return buf.subarray(0, byteOffset).toString("utf8").length;
|
|
111
|
+
};
|
|
112
|
+
let doc: TextDocument | null = null;
|
|
113
|
+
|
|
114
|
+
const toOffsetEdit = ({ span, newText }: Edit): OffsetEdit => {
|
|
115
|
+
if (span.byte_start != null && span.byte_end != null) {
|
|
116
|
+
const [s, e] =
|
|
117
|
+
span.byte_start <= span.byte_end
|
|
118
|
+
? [span.byte_start, span.byte_end]
|
|
119
|
+
: [span.byte_end, span.byte_start];
|
|
120
|
+
return { start: byteToOffset(s), end: byteToOffset(e), newText };
|
|
121
|
+
}
|
|
122
|
+
doc ??= TextDocument.create("inmemory://autofix", "motoko", 0, content);
|
|
123
|
+
const start = doc.offsetAt({
|
|
124
|
+
line: span.line_start - 1,
|
|
125
|
+
character: span.column_start - 1,
|
|
126
|
+
});
|
|
127
|
+
const end = doc.offsetAt({
|
|
128
|
+
line: span.line_end - 1,
|
|
129
|
+
character: span.column_end - 1,
|
|
130
|
+
});
|
|
131
|
+
return start <= end
|
|
132
|
+
? { start, end, newText }
|
|
133
|
+
: { start: end, end: start, newText };
|
|
134
|
+
};
|
|
135
|
+
|
|
122
136
|
const acceptedEdits: OffsetEdit[] = [];
|
|
123
137
|
const appliedCodes: string[] = [];
|
|
124
138
|
|
|
125
139
|
for (const fix of fixes) {
|
|
126
|
-
const offsets
|
|
127
|
-
const range = normalizeRange(e.range);
|
|
128
|
-
return {
|
|
129
|
-
start: doc.offsetAt(range.start),
|
|
130
|
-
end: doc.offsetAt(range.end),
|
|
131
|
-
newText: e.newText,
|
|
132
|
-
};
|
|
133
|
-
});
|
|
140
|
+
const offsets = fix.edits.map(toOffsetEdit);
|
|
134
141
|
|
|
135
142
|
const overlaps = offsets.some((o) =>
|
|
136
143
|
acceptedEdits.some((a) => o.start < a.end && o.end > a.start),
|
|
@@ -145,7 +152,6 @@ function applyDiagnosticFixes(
|
|
|
145
152
|
|
|
146
153
|
acceptedEdits.sort((a, b) => a.start - b.start);
|
|
147
154
|
|
|
148
|
-
const text = doc.getText();
|
|
149
155
|
const spans: string[] = [];
|
|
150
156
|
let lastOffset = 0;
|
|
151
157
|
|
|
@@ -154,7 +160,7 @@ function applyDiagnosticFixes(
|
|
|
154
160
|
continue;
|
|
155
161
|
}
|
|
156
162
|
if (edit.start > lastOffset) {
|
|
157
|
-
spans.push(
|
|
163
|
+
spans.push(content.substring(lastOffset, edit.start));
|
|
158
164
|
}
|
|
159
165
|
if (edit.newText.length) {
|
|
160
166
|
spans.push(edit.newText);
|
|
@@ -162,7 +168,7 @@ function applyDiagnosticFixes(
|
|
|
162
168
|
lastOffset = edit.end;
|
|
163
169
|
}
|
|
164
170
|
|
|
165
|
-
spans.push(
|
|
171
|
+
spans.push(content.substring(lastOffset));
|
|
166
172
|
return { text: spans.join(""), appliedCodes };
|
|
167
173
|
}
|
|
168
174
|
|
|
@@ -184,18 +190,19 @@ export async function autofixMotoko(
|
|
|
184
190
|
for (let iteration = 0; iteration < MAX_FIX_ITERATIONS; iteration++) {
|
|
185
191
|
const fixesByFile = new Map<string, DiagnosticFix[]>();
|
|
186
192
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
193
|
+
for (const file of files) {
|
|
194
|
+
const result = await execa(
|
|
195
|
+
mocPath,
|
|
196
|
+
[file, ...mocArgs, "--error-format=json"],
|
|
197
|
+
{ stdio: "pipe", reject: false },
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const diagnostics = parseDiagnostics(result.stdout);
|
|
201
|
+
for (const [targetFile, fixes] of extractDiagnosticFixes(diagnostics)) {
|
|
202
|
+
const existing = fixesByFile.get(targetFile) ?? [];
|
|
203
|
+
existing.push(...fixes);
|
|
204
|
+
fixesByFile.set(targetFile, existing);
|
|
205
|
+
}
|
|
199
206
|
}
|
|
200
207
|
|
|
201
208
|
if (fixesByFile.size === 0) {
|
|
@@ -206,8 +213,10 @@ export async function autofixMotoko(
|
|
|
206
213
|
|
|
207
214
|
for (const [file, fixes] of fixesByFile) {
|
|
208
215
|
const original = await readFile(file, "utf-8");
|
|
209
|
-
const
|
|
210
|
-
|
|
216
|
+
const { text: result, appliedCodes } = applyDiagnosticFixes(
|
|
217
|
+
original,
|
|
218
|
+
fixes,
|
|
219
|
+
);
|
|
211
220
|
|
|
212
221
|
if (result === original) {
|
|
213
222
|
continue;
|