ic-mops 2.3.1 → 2.4.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/RELEASE.md +16 -162
  3. package/bundle/cli.tgz +0 -0
  4. package/cli.ts +2 -5
  5. package/commands/bench-replica.ts +13 -34
  6. package/commands/build.ts +70 -24
  7. package/commands/check-stable.ts +2 -2
  8. package/commands/check.ts +9 -8
  9. package/commands/replica.ts +17 -42
  10. package/dist/cli.js +3 -2
  11. package/dist/commands/bench-replica.d.ts +3 -4
  12. package/dist/commands/bench-replica.js +5 -23
  13. package/dist/commands/build.js +47 -20
  14. package/dist/commands/check-stable.js +2 -2
  15. package/dist/commands/check.js +9 -8
  16. package/dist/commands/replica.d.ts +3 -4
  17. package/dist/commands/replica.js +9 -29
  18. package/dist/helpers/pocket-ic-client.d.ts +9 -0
  19. package/dist/helpers/pocket-ic-client.js +18 -0
  20. package/dist/mops.d.ts +5 -0
  21. package/dist/mops.js +7 -0
  22. package/dist/package.json +1 -1
  23. package/dist/release-cli.js +6 -2
  24. package/dist/tests/build.test.js +83 -16
  25. package/dist/tests/check.test.js +7 -0
  26. package/dist/tests/pocket-ic.test.d.ts +1 -0
  27. package/dist/tests/pocket-ic.test.js +12 -0
  28. package/dist/wasm/pkg/nodejs/wasm_bg.wasm +0 -0
  29. package/dist/wasm/pkg/nodejs/wasm_bg.wasm.d.ts +1 -1
  30. package/dist/wasm/pkg/web/wasm.d.ts +1 -1
  31. package/dist/wasm/pkg/web/wasm_bg.wasm +0 -0
  32. package/dist/wasm/pkg/web/wasm_bg.wasm.d.ts +1 -1
  33. package/helpers/pocket-ic-client.ts +32 -0
  34. package/mops.ts +8 -0
  35. package/package.json +1 -1
  36. package/release-cli.ts +7 -2
  37. package/tests/__snapshots__/build.test.ts.snap +12 -0
  38. package/tests/build/custom-output/dfx.json +8 -0
  39. package/tests/build/custom-output/mops.toml +8 -0
  40. package/tests/build/custom-output/src/Main.mo +5 -0
  41. package/tests/build.test.ts +92 -20
  42. package/tests/check/canisters-subdir/mops.toml +8 -0
  43. package/tests/check/canisters-subdir/src/backend/main.mo +5 -0
  44. package/tests/check.test.ts +11 -0
  45. package/tests/pocket-ic/mops.toml +3 -0
  46. package/tests/pocket-ic/test/hello.test.mo +3 -0
  47. package/tests/pocket-ic.test.ts +19 -0
  48. package/wasm/pkg/nodejs/wasm_bg.wasm +0 -0
  49. package/wasm/pkg/nodejs/wasm_bg.wasm.d.ts +1 -1
  50. package/wasm/pkg/web/wasm.d.ts +1 -1
  51. package/wasm/pkg/web/wasm_bg.wasm +0 -0
  52. package/wasm/pkg/web/wasm_bg.wasm.d.ts +1 -1
@@ -1,5 +1,4 @@
1
- import { PocketIc, PocketIcServer } from "pic-ic";
2
- import { PocketIc as PocketIcMops, PocketIcServer as PocketIcServerMops } from "pic-js-mops";
1
+ import { type AnyPocketIcServer, type AnyPocketIc } from "../helpers/pocket-ic-client.js";
3
2
  export declare class BenchReplica {
4
3
  type: "dfx" | "pocket-ic" | "dfx-pocket-ic";
5
4
  verbose: boolean;
@@ -8,8 +7,8 @@ export declare class BenchReplica {
8
7
  canisterId: string;
9
8
  actor: any;
10
9
  }>;
11
- pocketIcServer?: PocketIcServer | PocketIcServerMops;
12
- pocketIc?: PocketIc | PocketIcMops;
10
+ pocketIcServer?: AnyPocketIcServer;
11
+ pocketIc?: AnyPocketIc;
13
12
  constructor(type: "dfx" | "pocket-ic" | "dfx-pocket-ic", verbose?: boolean);
14
13
  start({ silent }?: {
15
14
  silent?: boolean | undefined;
@@ -3,9 +3,8 @@ import { execSync } from "node:child_process";
3
3
  import path from "node:path";
4
4
  import fs from "node:fs";
5
5
  import { execaCommand } from "execa";
6
- import { PocketIc, PocketIcServer } from "pic-ic";
7
- import { PocketIc as PocketIcMops, PocketIcServer as PocketIcServerMops, } from "pic-js-mops";
8
- import { getRootDir, readConfig } from "../mops.js";
6
+ import { getRootDir } from "../mops.js";
7
+ import { startPocketIc, } from "../helpers/pocket-ic-client.js";
9
8
  import { createActor, idlFactory } from "../declarations/bench/index.js";
10
9
  import { toolchain } from "./toolchain/index.js";
11
10
  import { getDfxVersion } from "../helpers/get-dfx-version.js";
@@ -36,26 +35,9 @@ export class BenchReplica {
36
35
  }
37
36
  else {
38
37
  let pocketIcBin = await toolchain.bin("pocket-ic");
39
- let config = readConfig();
40
- if (config.toolchain?.["pocket-ic"] !== "4.0.0" &&
41
- !config.toolchain?.["pocket-ic"]?.startsWith("9.")) {
42
- console.error("Current Mops CLI only supports pocket-ic 9.x.x and 4.0.0");
43
- process.exit(1);
44
- }
45
- // pocket-ic 9.x.x
46
- if (config.toolchain?.["pocket-ic"]?.startsWith("9.")) {
47
- this.pocketIcServer = await PocketIcServerMops.start({
48
- binPath: pocketIcBin,
49
- });
50
- this.pocketIc = await PocketIcMops.create(this.pocketIcServer.getUrl());
51
- }
52
- // pocket-ic 4.0.0
53
- else {
54
- this.pocketIcServer = await PocketIcServer.start({
55
- binPath: pocketIcBin,
56
- });
57
- this.pocketIc = await PocketIc.create(this.pocketIcServer.getUrl());
58
- }
38
+ let pic = await startPocketIc({ binPath: pocketIcBin });
39
+ this.pocketIcServer = pic.server;
40
+ this.pocketIc = pic.client;
59
41
  }
60
42
  }
61
43
  async stop() {
@@ -7,7 +7,7 @@ import { cliError } from "../error.js";
7
7
  import { isCandidCompatible } from "../helpers/is-candid-compatible.js";
8
8
  import { resolveCanisterConfigs } from "../helpers/resolve-canisters.js";
9
9
  import { getWasmBindings } from "../wasm.js";
10
- import { getGlobalMocArgs, readConfig } from "../mops.js";
10
+ import { getGlobalMocArgs, readConfig, resolveConfigPath } from "../mops.js";
11
11
  import { sourcesArgs } from "./sources.js";
12
12
  import { toolchain } from "./toolchain/index.js";
13
13
  export const DEFAULT_BUILD_OUTPUT_DIR = ".mops/.build";
@@ -15,9 +15,12 @@ export async function build(canisterNames, options) {
15
15
  if (canisterNames?.length == 0) {
16
16
  cliError("No canisters specified to build");
17
17
  }
18
- let outputDir = options.outputDir ?? DEFAULT_BUILD_OUTPUT_DIR;
19
- let mocPath = await toolchain.bin("moc", { fallback: true });
20
18
  let config = readConfig();
19
+ let configOutputDir = config.build?.outputDir
20
+ ? resolveConfigPath(config.build.outputDir)
21
+ : undefined;
22
+ let outputDir = options.outputDir ?? configOutputDir ?? DEFAULT_BUILD_OUTPUT_DIR;
23
+ let mocPath = await toolchain.bin("moc", { fallback: true });
21
24
  let canisters = resolveCanisterConfigs(config);
22
25
  if (!Object.keys(canisters).length) {
23
26
  cliError(`No Motoko canisters found in mops.toml configuration`);
@@ -45,6 +48,7 @@ export async function build(canisterNames, options) {
45
48
  if (!motokoPath) {
46
49
  cliError(`No main file is specified for canister ${canisterName}`);
47
50
  }
51
+ motokoPath = resolveConfigPath(motokoPath);
48
52
  const wasmPath = join(outputDir, `${canisterName}.wasm`);
49
53
  let args = [
50
54
  "-c",
@@ -55,19 +59,7 @@ export async function build(canisterNames, options) {
55
59
  ...(await sourcesArgs()).flat(),
56
60
  ...getGlobalMocArgs(config),
57
61
  ];
58
- if (config.build?.args) {
59
- if (typeof config.build.args === "string") {
60
- cliError(`[build] config 'args' should be an array of strings in mops.toml config file`);
61
- }
62
- args.push(...config.build.args);
63
- }
64
- if (canister.args) {
65
- if (typeof canister.args === "string") {
66
- cliError(`Canister config 'args' should be an array of strings for canister ${canisterName}`);
67
- }
68
- args.push(...canister.args);
69
- }
70
- args.push(...(options.extraArgs ?? []));
62
+ args.push(...collectExtraArgs(config, canister, canisterName, options.extraArgs));
71
63
  const isPublicCandid = true; // always true for now to reduce corner cases
72
64
  const candidVisibility = isPublicCandid ? "icp:public" : "icp:private";
73
65
  if (isPublicCandid) {
@@ -98,10 +90,12 @@ export async function build(canisterNames, options) {
98
90
  console.log(result.stdout);
99
91
  }
100
92
  const generatedDidPath = join(outputDir, `${canisterName}.did`);
101
- if (canister.candid) {
102
- const originalCandidPath = canister.candid;
93
+ const resolvedCandidPath = canister.candid
94
+ ? resolveConfigPath(canister.candid)
95
+ : null;
96
+ if (resolvedCandidPath) {
103
97
  try {
104
- const compatible = await isCandidCompatible(generatedDidPath, originalCandidPath);
98
+ const compatible = await isCandidCompatible(generatedDidPath, resolvedCandidPath);
105
99
  if (!compatible) {
106
100
  cliError(`Candid compatibility check failed for canister ${canisterName}`);
107
101
  }
@@ -115,7 +109,7 @@ export async function build(canisterNames, options) {
115
109
  }
116
110
  options.verbose &&
117
111
  console.log(chalk.gray(`Adding metadata to ${wasmPath}`));
118
- const candidPath = canister.candid ?? generatedDidPath;
112
+ const candidPath = resolvedCandidPath ?? generatedDidPath;
119
113
  const candidText = await readFile(candidPath, "utf-8");
120
114
  const customSections = [
121
115
  { name: `${candidVisibility} candid:service`, data: candidText },
@@ -139,3 +133,36 @@ export async function build(canisterNames, options) {
139
133
  }
140
134
  console.log(chalk.green(`\n✓ Built ${Object.keys(filteredCanisters).length} canister${Object.keys(filteredCanisters).length == 1 ? "" : "s"} successfully`));
141
135
  }
136
+ const managedFlags = {
137
+ "-o": "use [build].outputDir in mops.toml or --output flag instead",
138
+ "-c": "this flag is always set by mops build",
139
+ "--idl": "this flag is always set by mops build",
140
+ "--public-metadata": "this flag is managed by mops build",
141
+ };
142
+ function collectExtraArgs(config, canister, canisterName, extraArgs) {
143
+ const args = [];
144
+ if (config.build?.args) {
145
+ if (typeof config.build.args === "string") {
146
+ cliError(`[build] config 'args' should be an array of strings in mops.toml config file`);
147
+ }
148
+ args.push(...config.build.args);
149
+ }
150
+ if (canister.args) {
151
+ if (typeof canister.args === "string") {
152
+ cliError(`Canister config 'args' should be an array of strings for canister ${canisterName}`);
153
+ }
154
+ args.push(...canister.args);
155
+ }
156
+ if (extraArgs) {
157
+ args.push(...extraArgs);
158
+ }
159
+ const warned = new Set();
160
+ for (const arg of args) {
161
+ const hint = managedFlags[arg];
162
+ if (hint && !warned.has(arg)) {
163
+ warned.add(arg);
164
+ console.warn(chalk.yellow(`Warning: '${arg}' in args for canister ${canisterName} may conflict with mops build — ${hint}`));
165
+ }
166
+ }
167
+ return args;
168
+ }
@@ -4,7 +4,7 @@ import { rename, rm } from "node:fs/promises";
4
4
  import chalk from "chalk";
5
5
  import { execa } from "execa";
6
6
  import { cliError } from "../error.js";
7
- import { getGlobalMocArgs, readConfig } from "../mops.js";
7
+ import { getGlobalMocArgs, readConfig, resolveConfigPath } from "../mops.js";
8
8
  import { resolveSingleCanister } from "../helpers/resolve-canisters.js";
9
9
  import { sourcesArgs } from "./sources.js";
10
10
  import { toolchain } from "./toolchain/index.js";
@@ -19,7 +19,7 @@ export async function checkStable(oldFile, canisterName, options = {}) {
19
19
  const globalMocArgs = getGlobalMocArgs(config);
20
20
  await runStableCheck({
21
21
  oldFile,
22
- canisterMain: canister.main,
22
+ canisterMain: resolveConfigPath(canister.main),
23
23
  canisterName: name,
24
24
  mocPath,
25
25
  globalMocArgs,
@@ -1,9 +1,9 @@
1
- import { relative } from "node:path";
1
+ import path from "node:path";
2
2
  import { existsSync } from "node:fs";
3
3
  import chalk from "chalk";
4
4
  import { execa } from "execa";
5
5
  import { cliError } from "../error.js";
6
- import { getGlobalMocArgs, readConfig } from "../mops.js";
6
+ import { getGlobalMocArgs, readConfig, resolveConfigPath } from "../mops.js";
7
7
  import { autofixMotoko } from "../helpers/autofix-motoko.js";
8
8
  import { getMocSemVer } from "../helpers/get-moc-version.js";
9
9
  import { resolveCanisterConfigs, resolveCanisterEntrypoints, } from "../helpers/resolve-canisters.js";
@@ -19,7 +19,7 @@ export async function check(files, options = {}) {
19
19
  let fileList = Array.isArray(files) ? files : files ? [files] : [];
20
20
  const config = readConfig();
21
21
  if (fileList.length === 0) {
22
- fileList = resolveCanisterEntrypoints(config);
22
+ fileList = resolveCanisterEntrypoints(config).map(resolveConfigPath);
23
23
  }
24
24
  if (fileList.length === 0) {
25
25
  cliError("No Motoko files specified and no canisters defined in mops.toml.\n" +
@@ -55,7 +55,7 @@ export async function check(files, options = {}) {
55
55
  for (const [file, codes] of fixResult.fixedFiles) {
56
56
  const unique = [...new Set(codes)].sort();
57
57
  const n = codes.length;
58
- const rel = relative(process.cwd(), file);
58
+ const rel = path.relative(process.cwd(), file);
59
59
  console.log(chalk.green(`Fixed ${rel} (${n} ${n === 1 ? "fix" : "fixes"}: ${unique.join(", ")})`));
60
60
  }
61
61
  const fileCount = fixResult.fixedFiles.size;
@@ -96,18 +96,19 @@ export async function check(files, options = {}) {
96
96
  if (!canister.main) {
97
97
  cliError(`No main file specified for canister '${name}' in mops.toml`);
98
98
  }
99
- if (!existsSync(stableConfig.path)) {
99
+ const stablePath = resolveConfigPath(stableConfig.path);
100
+ if (!existsSync(stablePath)) {
100
101
  if (stableConfig.skipIfMissing) {
101
102
  continue;
102
103
  }
103
- cliError(`Deployed file not found: ${stableConfig.path} (canister '${name}')\n` +
104
+ cliError(`Deployed file not found: ${stablePath} (canister '${name}')\n` +
104
105
  "Set skipIfMissing = true in [canisters." +
105
106
  name +
106
107
  ".check-stable] to skip this check when the file is missing.");
107
108
  }
108
109
  await runStableCheck({
109
- oldFile: stableConfig.path,
110
- canisterMain: canister.main,
110
+ oldFile: stablePath,
111
+ canisterMain: resolveConfigPath(canister.main),
111
112
  canisterName: name,
112
113
  mocPath,
113
114
  globalMocArgs,
@@ -1,8 +1,7 @@
1
1
  import { ChildProcessWithoutNullStreams } from "node:child_process";
2
2
  import { PassThrough } from "node:stream";
3
3
  import { IDL } from "@icp-sdk/core/candid";
4
- import { PocketIc, PocketIcServer } from "pic-ic";
5
- import { PocketIc as PocketIcMops, PocketIcServer as PocketIcServerMops } from "pic-js-mops";
4
+ import { type AnyPocketIcServer, type AnyPocketIc } from "../helpers/pocket-ic-client.js";
6
5
  type StartOptions = {
7
6
  type?: "dfx" | "pocket-ic" | "dfx-pocket-ic";
8
7
  dir?: string;
@@ -18,8 +17,8 @@ export declare class Replica {
18
17
  actor: any;
19
18
  stream: PassThrough;
20
19
  }>;
21
- pocketIcServer?: PocketIcServer | PocketIcServerMops;
22
- pocketIc?: PocketIc | PocketIcMops;
20
+ pocketIcServer?: AnyPocketIcServer;
21
+ pocketIc?: AnyPocketIc;
23
22
  dfxProcess?: ChildProcessWithoutNullStreams;
24
23
  dir: string;
25
24
  ttl: number;
@@ -5,10 +5,8 @@ import fs from "node:fs";
5
5
  import { PassThrough } from "node:stream";
6
6
  import { spawn as spawnAsync } from "promisify-child-process";
7
7
  import { Actor, HttpAgent } from "@icp-sdk/core/agent";
8
- import { PocketIc, PocketIcServer } from "pic-ic";
9
- import { PocketIc as PocketIcMops, PocketIcServer as PocketIcServerMops, } from "pic-js-mops";
10
8
  import chalk from "chalk";
11
- import { readConfig } from "../mops.js";
9
+ import { startPocketIc, } from "../helpers/pocket-ic-client.js";
12
10
  import { toolchain } from "./toolchain/index.js";
13
11
  import { getDfxVersion } from "../helpers/get-dfx-version.js";
14
12
  export class Replica {
@@ -74,32 +72,14 @@ export class Replica {
74
72
  }
75
73
  else {
76
74
  let pocketIcBin = await toolchain.bin("pocket-ic");
77
- let config = readConfig();
78
- if (config.toolchain?.["pocket-ic"] !== "4.0.0" &&
79
- !config.toolchain?.["pocket-ic"]?.startsWith("9.")) {
80
- console.error("Current Mops CLI only supports pocket-ic 9.x.x and 4.0.0");
81
- process.exit(1);
82
- }
83
- // pocket-ic 9.x.x
84
- if (config.toolchain?.["pocket-ic"]?.startsWith("9.")) {
85
- this.pocketIcServer = await PocketIcServerMops.start({
86
- showRuntimeLogs: false,
87
- showCanisterLogs: false,
88
- binPath: pocketIcBin,
89
- ttl: this.ttl,
90
- });
91
- this.pocketIc = await PocketIcMops.create(this.pocketIcServer.getUrl());
92
- }
93
- // pocket-ic 4.0.0
94
- else {
95
- this.pocketIcServer = await PocketIcServer.start({
96
- showRuntimeLogs: false,
97
- showCanisterLogs: false,
98
- binPath: pocketIcBin,
99
- ttl: this.ttl,
100
- });
101
- this.pocketIc = await PocketIc.create(this.pocketIcServer.getUrl());
102
- }
75
+ let pic = await startPocketIc({
76
+ binPath: pocketIcBin,
77
+ showRuntimeLogs: false,
78
+ showCanisterLogs: false,
79
+ ttl: this.ttl,
80
+ });
81
+ this.pocketIcServer = pic.server;
82
+ this.pocketIc = pic.client;
103
83
  // process canister logs
104
84
  this._attachCanisterLogHandler(this.pocketIcServer.serverProcess);
105
85
  }
@@ -0,0 +1,9 @@
1
+ import { PocketIc, PocketIcServer } from "pic-ic";
2
+ import { PocketIc as PocketIcModern, PocketIcServer as PocketIcServerModern, type StartServerOptions } from "pic-js-mops";
3
+ export type AnyPocketIcServer = PocketIcServer | PocketIcServerModern;
4
+ export type AnyPocketIc = PocketIc | PocketIcModern;
5
+ export type AnySetupCanister = PocketIc["setupCanister"] & PocketIcModern["setupCanister"];
6
+ export declare function startPocketIc(options: StartServerOptions): Promise<{
7
+ server: AnyPocketIcServer;
8
+ client: AnyPocketIc;
9
+ }>;
@@ -0,0 +1,18 @@
1
+ import semver from "semver";
2
+ import { PocketIc, PocketIcServer } from "pic-ic";
3
+ import { PocketIc as PocketIcModern, PocketIcServer as PocketIcServerModern, } from "pic-js-mops";
4
+ import { readConfig } from "../mops.js";
5
+ function isLegacy() {
6
+ let version = readConfig().toolchain?.["pocket-ic"];
7
+ return !!version && !!semver.valid(version) && semver.lt(version, "9.0.0");
8
+ }
9
+ export async function startPocketIc(options) {
10
+ if (isLegacy()) {
11
+ let server = await PocketIcServer.start(options);
12
+ let client = await PocketIc.create(server.getUrl());
13
+ return { server, client };
14
+ }
15
+ let server = await PocketIcServerModern.start(options);
16
+ let client = await PocketIcModern.create(server.getUrl());
17
+ return { server, client };
18
+ }
package/dist/mops.d.ts CHANGED
@@ -11,6 +11,11 @@ export declare function setNetwork(network: string): void;
11
11
  export declare let getIdentity: () => Promise<Identity | undefined>;
12
12
  export declare function getClosestConfigFile(dir?: string): string;
13
13
  export declare function getRootDir(): string;
14
+ /**
15
+ * Resolve a path from mops.toml config (relative to project root)
16
+ * into a path relative to the current working directory.
17
+ */
18
+ export declare function resolveConfigPath(configPath: string): string;
14
19
  export declare function checkConfigFile(exit?: boolean): boolean;
15
20
  export declare function progressBar(step: number, total: number): string;
16
21
  export declare function parseGithubURL(href: string): {
package/dist/mops.js CHANGED
@@ -87,6 +87,13 @@ export function getRootDir() {
87
87
  }
88
88
  return path.dirname(configFile);
89
89
  }
90
+ /**
91
+ * Resolve a path from mops.toml config (relative to project root)
92
+ * into a path relative to the current working directory.
93
+ */
94
+ export function resolveConfigPath(configPath) {
95
+ return path.relative(process.cwd(), path.resolve(getRootDir(), configPath));
96
+ }
90
97
  export function checkConfigFile(exit = false) {
91
98
  let configFile = getClosestConfigFile();
92
99
  if (!configFile) {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "bin/mops.js",
@@ -8,8 +8,12 @@ import { sha256 } from "@noble/hashes/sha256";
8
8
  import { bytesToHex } from "@noble/hashes/utils";
9
9
  import { findChangelogEntry } from "./helpers/find-changelog-entry.js";
10
10
  let __dirname = new URL(".", import.meta.url).pathname;
11
- // build using Docker
12
- execSync("./build.sh", { stdio: "inherit", cwd: __dirname });
11
+ if (!process.env.SKIP_BUILD) {
12
+ execSync("./build.sh", { stdio: "inherit", cwd: __dirname });
13
+ }
14
+ else if (!fs.existsSync(path.resolve(__dirname, "bundle/cli.tgz"))) {
15
+ throw new Error("SKIP_BUILD is set but bundle/cli.tgz does not exist. Run build.sh first.");
16
+ }
13
17
  let commitHash = process.env.COMMIT_HASH || execSync("git rev-parse HEAD").toString().trim();
14
18
  let version = JSON.parse(fs.readFileSync(path.resolve(__dirname, "package.json"), "utf8")).version;
15
19
  let major = semver.parse(version)?.major;
@@ -1,22 +1,84 @@
1
1
  import { describe, expect, test } from "@jest/globals";
2
2
  import { execa } from "execa";
3
- import { existsSync } from "node:fs";
3
+ import { existsSync, rmSync } from "node:fs";
4
4
  import path from "path";
5
- import { cliSnapshot } from "./helpers";
5
+ import { cli, cliSnapshot } from "./helpers";
6
6
  const distBin = path.resolve(import.meta.dirname, "../dist/bin/mops.js");
7
+ function cleanFixture(cwd, ...extras) {
8
+ rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
9
+ for (const p of extras) {
10
+ rmSync(p, { recursive: true, force: true });
11
+ }
12
+ }
7
13
  describe("build", () => {
8
14
  test("ok", async () => {
9
15
  const cwd = path.join(import.meta.dirname, "build/success");
10
- await cliSnapshot(["build", "--verbose"], { cwd }, 0);
11
- await cliSnapshot(["build", "foo"], { cwd }, 0);
12
- await cliSnapshot(["build", "bar"], { cwd }, 0);
13
- await cliSnapshot(["build", "foo", "bar"], { cwd }, 0);
16
+ try {
17
+ await cliSnapshot(["build", "--verbose"], { cwd }, 0);
18
+ await cliSnapshot(["build", "foo"], { cwd }, 0);
19
+ await cliSnapshot(["build", "bar"], { cwd }, 0);
20
+ await cliSnapshot(["build", "foo", "bar"], { cwd }, 0);
21
+ }
22
+ finally {
23
+ cleanFixture(cwd);
24
+ }
14
25
  });
15
26
  test("error", async () => {
16
27
  const cwd = path.join(import.meta.dirname, "build/error");
17
- await cliSnapshot(["build", "foo", "--verbose"], { cwd }, 0);
18
- expect((await cliSnapshot(["build", "bar"], { cwd }, 1)).stderr).toMatch("Candid compatibility check failed for canister bar");
19
- expect((await cliSnapshot(["build", "foo", "bar"], { cwd }, 1)).stderr).toMatch("Candid compatibility check failed for canister bar");
28
+ try {
29
+ await cliSnapshot(["build", "foo", "--verbose"], { cwd }, 0);
30
+ expect((await cliSnapshot(["build", "bar"], { cwd }, 1)).stderr).toMatch("Candid compatibility check failed for canister bar");
31
+ expect((await cliSnapshot(["build", "foo", "bar"], { cwd }, 1)).stderr).toMatch("Candid compatibility check failed for canister bar");
32
+ }
33
+ finally {
34
+ cleanFixture(cwd);
35
+ }
36
+ });
37
+ // [build].outputDir in mops.toml should control where build output goes
38
+ test("custom output path via config outputDir", async () => {
39
+ const cwd = path.join(import.meta.dirname, "build/custom-output");
40
+ const customOut = path.join(cwd, "custom-out");
41
+ const customWasm = path.join(customOut, "main.wasm");
42
+ const customDid = path.join(customOut, "main.did");
43
+ const defaultDid = path.join(cwd, ".mops/.build/main.did");
44
+ try {
45
+ const result = await cli(["build"], { cwd });
46
+ expect(result.exitCode).toBe(0);
47
+ expect(existsSync(customWasm)).toBe(true);
48
+ expect(existsSync(customDid)).toBe(true);
49
+ expect(existsSync(defaultDid)).toBe(false);
50
+ }
51
+ finally {
52
+ cleanFixture(cwd, customOut);
53
+ }
54
+ });
55
+ // Regression: --output CLI option was silently ignored due to
56
+ // Commander storing it as options.output while build() read options.outputDir
57
+ test("--output CLI option", async () => {
58
+ const cwd = path.join(import.meta.dirname, "build/success");
59
+ const outputDir = path.join(cwd, "cli-output-test");
60
+ try {
61
+ const result = await cli(["build", "foo", "--output", outputDir], {
62
+ cwd,
63
+ });
64
+ expect(result.exitCode).toBe(0);
65
+ expect(existsSync(path.join(outputDir, "foo.wasm"))).toBe(true);
66
+ expect(existsSync(path.join(outputDir, "foo.did"))).toBe(true);
67
+ }
68
+ finally {
69
+ cleanFixture(cwd, outputDir);
70
+ }
71
+ });
72
+ test("warns when args contain managed flags", async () => {
73
+ const cwd = path.join(import.meta.dirname, "build/success");
74
+ const artifact = path.join(cwd, "x");
75
+ const artifactDid = path.join(cwd, "x.did");
76
+ try {
77
+ await cliSnapshot(["build", "foo", "--", "-o", "x", "-c", "--idl"], { cwd }, 1);
78
+ }
79
+ finally {
80
+ cleanFixture(cwd, artifact, artifactDid);
81
+ }
20
82
  });
21
83
  // Regression: bin/mops.js must route through environments/nodejs/cli.js
22
84
  // so that setWasmBindings() is called before any command runs.
@@ -25,12 +87,17 @@ describe("build", () => {
25
87
  const hasDistBin = existsSync(distBin);
26
88
  (hasDistBin ? test : test.skip)("wasm bindings initialized via dist entry point", async () => {
27
89
  const cwd = path.join(import.meta.dirname, "build/success");
28
- const result = await execa("node", [distBin, "build", "foo"], {
29
- cwd,
30
- stdio: "pipe",
31
- reject: false,
32
- });
33
- expect(result.stderr).not.toContain("Wasm bindings have not been set");
34
- expect(result.exitCode).toBe(0);
90
+ try {
91
+ const result = await execa("node", [distBin, "build", "foo"], {
92
+ cwd,
93
+ stdio: "pipe",
94
+ reject: false,
95
+ });
96
+ expect(result.stderr).not.toContain("Wasm bindings have not been set");
97
+ expect(result.exitCode).toBe(0);
98
+ }
99
+ finally {
100
+ cleanFixture(cwd);
101
+ }
35
102
  });
36
103
  });
@@ -38,6 +38,13 @@ describe("check", () => {
38
38
  const cwd = path.join(import.meta.dirname, "check/canisters");
39
39
  await cliSnapshot(["check"], { cwd }, 0);
40
40
  });
41
+ test("canister entrypoint resolved relative to config root when run from subdirectory", async () => {
42
+ const fixtureRoot = path.join(import.meta.dirname, "check/canisters-subdir");
43
+ const subdir = path.join(fixtureRoot, "src/backend");
44
+ const result = await cli(["check"], { cwd: subdir });
45
+ expect(result.exitCode).toBe(0);
46
+ expect(result.stdout).toMatch(/✓/);
47
+ });
41
48
  test("[moc] args applied when using canister fallback", async () => {
42
49
  const cwd = path.join(import.meta.dirname, "check/canisters-moc-args");
43
50
  const result = await cli(["check"], { cwd });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { describe, test, expect } from "@jest/globals";
2
+ import path from "path";
3
+ import { cli } from "./helpers";
4
+ describe("pocket-ic", () => {
5
+ test("runs tests with pocket-ic 12.0.0", async () => {
6
+ const cwd = path.join(import.meta.dirname, "pocket-ic");
7
+ const result = await cli(["test", "--reporter", "verbose", "--replica", "pocket-ic"], { cwd });
8
+ expect(result.stderr).not.toContain("is not supported");
9
+ expect(result.stderr).not.toContain("only supports pocket-ic 9.x.x and 4.0.0");
10
+ expect(result.exitCode).toBe(0);
11
+ });
12
+ });
Binary file
@@ -1,8 +1,8 @@
1
1
  /* tslint:disable */
2
2
  /* eslint-disable */
3
3
  export const memory: WebAssembly.Memory;
4
- export const is_candid_compatible: (a: number, b: number, c: number, d: number) => number;
5
4
  export const add_custom_sections: (a: number, b: number, c: any) => [number, number, number, number];
5
+ export const is_candid_compatible: (a: number, b: number, c: number, d: number) => number;
6
6
  export const __wbindgen_malloc: (a: number, b: number) => number;
7
7
  export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
8
8
  export const __wbindgen_exn_store: (a: number) => void;
@@ -9,8 +9,8 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
9
9
 
10
10
  export interface InitOutput {
11
11
  readonly memory: WebAssembly.Memory;
12
- readonly is_candid_compatible: (a: number, b: number, c: number, d: number) => number;
13
12
  readonly add_custom_sections: (a: number, b: number, c: any) => [number, number, number, number];
13
+ readonly is_candid_compatible: (a: number, b: number, c: number, d: number) => number;
14
14
  readonly __wbindgen_malloc: (a: number, b: number) => number;
15
15
  readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
16
16
  readonly __wbindgen_exn_store: (a: number) => void;
Binary file
@@ -1,8 +1,8 @@
1
1
  /* tslint:disable */
2
2
  /* eslint-disable */
3
3
  export const memory: WebAssembly.Memory;
4
- export const is_candid_compatible: (a: number, b: number, c: number, d: number) => number;
5
4
  export const add_custom_sections: (a: number, b: number, c: any) => [number, number, number, number];
5
+ export const is_candid_compatible: (a: number, b: number, c: number, d: number) => number;
6
6
  export const __wbindgen_malloc: (a: number, b: number) => number;
7
7
  export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
8
8
  export const __wbindgen_exn_store: (a: number) => void;
@@ -0,0 +1,32 @@
1
+ import semver from "semver";
2
+ import { PocketIc, PocketIcServer } from "pic-ic";
3
+ import {
4
+ PocketIc as PocketIcModern,
5
+ PocketIcServer as PocketIcServerModern,
6
+ type StartServerOptions,
7
+ } from "pic-js-mops";
8
+ import { readConfig } from "../mops.js";
9
+
10
+ export type AnyPocketIcServer = PocketIcServer | PocketIcServerModern;
11
+ export type AnyPocketIc = PocketIc | PocketIcModern;
12
+ export type AnySetupCanister = PocketIc["setupCanister"] &
13
+ PocketIcModern["setupCanister"];
14
+
15
+ function isLegacy(): boolean {
16
+ let version = readConfig().toolchain?.["pocket-ic"];
17
+ return !!version && !!semver.valid(version) && semver.lt(version, "9.0.0");
18
+ }
19
+
20
+ export async function startPocketIc(
21
+ options: StartServerOptions,
22
+ ): Promise<{ server: AnyPocketIcServer; client: AnyPocketIc }> {
23
+ if (isLegacy()) {
24
+ let server = await PocketIcServer.start(options);
25
+ let client = await PocketIc.create(server.getUrl());
26
+ return { server, client };
27
+ }
28
+
29
+ let server = await PocketIcServerModern.start(options);
30
+ let client = await PocketIcModern.create(server.getUrl());
31
+ return { server, client };
32
+ }
package/mops.ts CHANGED
@@ -100,6 +100,14 @@ export function getRootDir() {
100
100
  return path.dirname(configFile);
101
101
  }
102
102
 
103
+ /**
104
+ * Resolve a path from mops.toml config (relative to project root)
105
+ * into a path relative to the current working directory.
106
+ */
107
+ export function resolveConfigPath(configPath: string): string {
108
+ return path.relative(process.cwd(), path.resolve(getRootDir(), configPath));
109
+ }
110
+
103
111
  export function checkConfigFile(exit = false) {
104
112
  let configFile = getClosestConfigFile();
105
113
  if (!configFile) {