Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Mops CLI Changelog

## Next
- Add `mops check` subcommand (for Motoko files) with autofix logic
- Warn for `dfx` projects instead of requiring `mops toolchain init`
- Allow specifying toolchain file paths in `mops.toml`
- Add `mops lint` subcommand and `lintoko` toolchain management
Expand Down
54 changes: 45 additions & 9 deletions cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { add } from "./commands/add.js";
import { bench } from "./commands/bench.js";
import { build, DEFAULT_BUILD_OUTPUT_DIR } from "./commands/build.js";
import { bump } from "./commands/bump.js";
import { check } from "./commands/check.js";
import { checkCandid } from "./commands/check-candid.js";
import { docsCoverage } from "./commands/docs-coverage.js";
import { docs } from "./commands/docs.js";
Expand Down Expand Up @@ -75,6 +76,22 @@ if (fs.existsSync(networkFile)) {

let program = new Command();

function parseExtraArgs(variadicArgs?: string[]): {
extraArgs: string[];
args: string[];
} {
const rawArgs = process.argv.slice(2);
const dashDashIndex = rawArgs.indexOf("--");
const extraArgs =
dashDashIndex !== -1 ? rawArgs.slice(dashDashIndex + 1) : [];
const args = variadicArgs
? extraArgs.length > 0
? variadicArgs.slice(0, variadicArgs.length - extraArgs.length)
: variadicArgs
: [];
return { extraArgs, args };
}

program.name("mops");

// --version
Expand Down Expand Up @@ -274,18 +291,38 @@ program
),
)
.allowUnknownOption(true) // TODO: restrict unknown before "--"
.action(async (canisters, options, command) => {
.action(async (canisters, options) => {
checkConfigFile(true);
const extraArgsIndex = command.args.indexOf("--");
const { extraArgs, args } = parseExtraArgs(canisters);
await installAll({
silent: true,
lock: "ignore",
installFromLockFile: true,
});
await build(canisters.length ? canisters : undefined, {
await build(args.length ? args : undefined, {
...options,
extraArgs:
extraArgsIndex !== -1 ? command.args.slice(extraArgsIndex + 1) : [],
extraArgs,
});
});

// check
program
.command("check <files...>")
.description("Check Motoko files for syntax errors and type issues")
.option("--verbose", "Verbose console output")
.addOption(new Option("--fix", "Apply autofixes"))
.allowUnknownOption(true)
.action(async (files, options) => {
checkConfigFile(true);
const { extraArgs, args: fileList } = parseExtraArgs(files);
await installAll({
silent: true,
lock: "ignore",
installFromLockFile: true,
});
await check(fileList, {
...options,
extraArgs,
});
});

Expand Down Expand Up @@ -686,13 +723,12 @@ program
),
)
.allowUnknownOption(true)
.action(async (filter, options, command) => {
.action(async (filter, options) => {
checkConfigFile(true);
const extraArgsIndex = command.args.indexOf("--");
const { extraArgs } = parseExtraArgs();
await lint(filter, {
...options,
extraArgs:
extraArgsIndex !== -1 ? command.args.slice(extraArgsIndex + 1) : [],
extraArgs,
});
});

Expand Down
98 changes: 98 additions & 0 deletions cli/commands/check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import chalk from "chalk";
import { execa } from "execa";
import { cliError } from "../error.js";
import { getMocPath } from "../helpers/get-moc-path.js";
import { autofixMotoko } from "../helpers/autofix-motoko.js";
import { sourcesArgs } from "./sources.js";

export interface CheckOptions {
verbose: boolean;
fix: boolean;
extraArgs: string[];
}

export async function check(
files: string | string[],
options: Partial<CheckOptions> = {},
): Promise<void> {
const fileList = Array.isArray(files) ? files : [files];

if (fileList.length === 0) {
cliError("No Motoko files specified for checking");
}

let mocPath = getMocPath();
let sources = await sourcesArgs();
const mocArgs = ["--check", ...sources.flat(), ...(options.extraArgs ?? [])];

const compileErrors = async (
filesToCheck: string[],
): Promise<string | null> => {
let allErrors = "";

for (const file of filesToCheck) {
const result = await execa(mocPath, [file, ...mocArgs], {
stdio: "pipe",
reject: false,
});

if (result.stderr) {
allErrors += result.stderr + "\n";
}
if (result.stdout?.trim()) {
allErrors += result.stdout + "\n";
}
}

return allErrors.trim() ? allErrors.trim() : null;
};

if (options.fix) {
if (options.verbose) {
console.log(chalk.blue("check"), chalk.gray("Attempting to fix files"));
}

const fixResult = await autofixMotoko(fileList, compileErrors);
if (fixResult) {
console.log(
chalk.green(
`✓ Fixed ${fixResult.fixedCount} file(s) with the following fixes:`,
),
);
for (const [code, count] of Object.entries(fixResult.fixedErrorCounts)) {
console.log(chalk.green(` ${code}: ${count} fix(es)`));
}
} else {
if (options.verbose) {
console.log(chalk.yellow("No fixes were needed"));
}
}
}

for (const file of fileList) {
try {
const args = [file, ...mocArgs];
if (options.verbose) {
console.log(chalk.blue("check"), chalk.gray("Running moc:"));
console.log(chalk.gray(mocPath, JSON.stringify(args)));
}

const result = await execa(mocPath, args, {
stdio: "inherit",
reject: false,
});

if (result.exitCode !== 0) {
cliError(
`✗ Check failed for file ${file} (exit code: ${result.exitCode})`,
);
}

console.log(chalk.green(`✓ ${file}`));
} catch (err: any) {
cliError(
`Error while checking ${file}${err?.message ? `\n${err.message}` : ""}`,
);
}
}
}
8 changes: 7 additions & 1 deletion cli/environments/nodejs/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import * as wasm from "../../wasm/pkg/nodejs/wasm.js";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { setWasmBindings } from "../../wasm.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const wasm = require(path.join(__dirname, "../../wasm/pkg/nodejs/wasm.js"));

setWasmBindings(wasm);

export * from "../../cli.js";
Loading
Loading