Skip to content

Commit

Permalink
change: task resolution (#4)
Browse files Browse the repository at this point in the history
* change: task resolution

* Refactor package manager commands to use 'exec' for better compatibility

* Enhance command help by listing available workspaces

* Bump version to 0.1.3 and update help command to display task details

* Rename task property to 'name' for clarity in help output

* Enhance YAML loading with optional missing file handling and placeholder support

* Fix command formatting in help output by joining commands with a space
  • Loading branch information
maastrich authored Nov 16, 2024
1 parent 24c7661 commit acccf00
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 204 deletions.
Binary file modified bun.lockb
Binary file not shown.
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@maastrich/moonx",
"version": "0.1.2",
"version": "0.1.3",
"description": "A CLI tool to help you with moon syntax",
"keywords": [
"moon",
Expand Down Expand Up @@ -28,10 +28,9 @@
"sort": "bun x sort-package-json"
},
"dependencies": {
"@moonrepo/types": "^1.8.0",
"@moonrepo/types": "^1.21.3",
"cac": "^6.7.14",
"chalk": "^5.3.0",
"glob": "^10.3.10",
"node-emoji": "^2.1.0",
"nunjucks": "^3.2.4",
"yaml": "^2.3.4"
Expand All @@ -43,7 +42,7 @@
"@withfig/autocomplete-types": "^1.29.0",
"bun-types": "^1.0.11",
"prettier": "^3.0.3",
"semver": "^7.5.4",
"semver": "^7.6.3",
"sort-package-json": "^2.6.0"
}
}
16 changes: 0 additions & 16 deletions src/cli/exec.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/cli/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function list(
if (!commands.has(command)) {
return Array.from(commands.keys());
}
const workspaces = commands.get(command)!;
const workspaces = commands.get(command) ?? [];
if (!wss.length) {
return workspaces;
}
Expand Down
48 changes: 32 additions & 16 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import { cac } from "cac";
import { spawnSync } from "child_process";

import pkg from "../package.json";

import { exec } from "./cli/exec.js";
import { list } from "./cli/list.js";
import { help } from "./utils/help.js";
import { logger } from "./utils/logger.js";
import { scan } from "./utils/scan-moon.js";

const isMoonInstalledGlobally = spawnSync("which", ["moon"]).status === 0;

if (!isMoonInstalledGlobally) {
logger.error(
"Moon is not installed globally. Please install it with: proto install moon",
);
}
import { moon } from "./utils/utils.js";

const commands = await scan();

Expand All @@ -39,14 +30,39 @@ cli
stdout.end();
});

for (const name of commands.keys()) {
for (const [name, workspaces] of commands) {
cli
.command(`${name} [...workspaces]`, "", { allowUnknownOptions: true })
.command(`${name} [...workspaces]`, workspaces.join(), {
allowUnknownOptions: true,
})
.action(async (wss: Array<string>, options) => {
const workspaces = exec(name, wss, commands);
const rest = ["--", ...options["--"]];
return Bun.spawnSync({
cmd: ["moon", "run", workspaces, rest].flat(),
if (wss.length === 0) {
return moon([`:${name}`, rest].flat(), {
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
onExit(_, exitCode) {
if (exitCode) {
logger.error(`task ${name} failed`);
process.exit(exitCode);
}
},
});
}
const filterd = wss.filter((ws) => {
if (!workspaces.includes(ws)) {
logger.warn(`task ${name} does not exist on workspace ${ws}`);
return false;
}
return true;
});
if (filterd.length === 0) {
logger.error(`no valid workspaces for task ${name}`);
return;
}

return moon([filterd.map((ws) => `${name}:${ws}`), rest].flat(), {
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
Expand All @@ -62,7 +78,7 @@ for (const name of commands.keys()) {

cli.help((sections) => {
if (sections.some((section) => section.title === "Commands")) {
return [{ body: help.moonx(Array.from(commands.keys())) }];
return [{ body: help.moonx(Array.from(commands.entries())) }];
}
const usage = sections.find((section) => section.title === "Usage");
if (!usage) {
Expand Down
12 changes: 9 additions & 3 deletions src/utils/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ ${chalk.bold(
${chalk.bold("Commands:")}
{% for task in tasks -%}
${chalk.blue("{{ task }}")}
${chalk.blue("{{ task.name }}")}{{ task.spacing }}{{ task.commands }}
{% endfor %}
${chalk.bold("Moon option:")}
Expand All @@ -43,8 +43,14 @@ For more info, run any command with the --help flag
e.g. ${chalk.yellow("moonx <command> --help")}
`);

function moonx(tasks: Array<string>) {
return renderString(_moonx, { tasks });
function moonx(tasks: Array<[string, string[]]>) {
return renderString(_moonx, {
tasks: tasks.map(([task, commands]) => ({
name: task,
spacing: " ".repeat(30 - task.length),
commands: commands.join(" "),
})),
});
}

const _task = style(`
Expand Down
41 changes: 23 additions & 18 deletions src/utils/load-yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,31 @@ import { parse } from "yaml";

import { logger } from "./logger.js";

async function loadOne<T extends object>(path: string): Promise<T> {
type LoadOptions<T> =
| {
allowMissing?: never;
}
| {
allowMissing: true;
placeholder: T;
};

export async function load<T extends object>(
path: string,
options?: LoadOptions<T>,
): Promise<T> {
const file = Bun.file(path);
const content = await file.text();
logger.debug(`loaded ${path}`);
return parse(content);
}

export async function load<T extends object>(path: string): Promise<T>;
export async function load<T extends object[]>(...paths: string[]): Promise<T>;
export async function load(...paths: string[]) {
try {
if (paths.length === 0) {
throw new Error("no path provided");
if (!file.exists()) {
if (options?.allowMissing) {
logger.debug(`file ${path} does not exist`);
return options.placeholder;
}
if (paths.length === 1) {
const [path] = paths;
return await loadOne(path);
}
return await Promise.all(paths.map((path) => loadOne(path)));
} catch {
return null;
logger.error(`file ${path} does not exist`);
process.exit(1);
}

const content = await file.text();
logger.debug(`loaded ${path}`);
return parse(content);
}
168 changes: 32 additions & 136 deletions src/utils/scan-moon.ts
Original file line number Diff line number Diff line change
@@ -1,151 +1,47 @@
import {
PartialProjectConfig,
PartialWorkspaceConfig,
PartialInheritedTasksConfig,
} from "@moonrepo/types";
import type { Task } from "@moonrepo/types";

import { glob } from "glob";
import { basename, join } from "path";

import { isWorkspaceProjectsConfig } from "./assertions.js";
import { load } from "./load-yaml.js";
import { logger } from "./logger.js";
import { mapFromGlob } from "./utils.js";

async function scanProjects(map: Map<string, string>) {
const projects = new Map<string, PartialProjectConfig>();
for (const [name, path] of map) {
const project = await load<PartialProjectConfig>(join(path, "moon.yml"));
if (!project) {
logger.warn(`No project config found for ${name} at ${path}`);
continue;
}
projects.set(name, project);
}
return projects;
}

async function scanWorkspace() {
const workspace = await load<PartialWorkspaceConfig>(".moon/workspace.yml");
if (!workspace) {
throw new Error("No workspace config found");
}
const { projects } = workspace;
const map = new Map<string, string>();
if (!projects) {
return scanProjects(map);
}
if (projects instanceof Array) {
const files = await mapFromGlob(projects);
for (const [name, path] of files) {
map.set(name, path);
}
} else if (isWorkspaceProjectsConfig(projects)) {
const files = await mapFromGlob(projects.globs ?? undefined);
for (const [name, path] of files) {
map.set(name, path);
}
for (const [name, path] of Object.entries(projects.sources ?? {})) {
map.set(name, path);
}
} else {
for (const [name, path] of Object.entries(projects)) {
map.set(name, path);
}
}
return scanProjects(map);
}

async function scanTaggedTasks() {
const tags = new Map<string, PartialInheritedTasksConfig>();
const files = await glob(".moon/tasks/tag-*.yml");
for (const file of files) {
const task = await load<PartialInheritedTasksConfig>(file);
if (!task) {
logger.warn(`Could not load tag from ${file}`);
continue;
}
const tag = basename(file).replace(/^tag-(.+)\.yml$/, "$1");
tags.set(tag, task);
}
return tags;
}
import { moon } from "./utils.js";

function mergeTasks(
projectname: string,
project: PartialProjectConfig,
inherited: PartialInheritedTasksConfig,
options: { ignoreWorkspaceFilters?: boolean } = {},
) {
const { tasks } = inherited;
if (!tasks) {
return;
}
project.tasks ??= {};
for (const [name, task] of Object.entries(tasks)) {
if (project.tasks[name]) {
logger.debug(
`Task ${name} already defined in project ${projectname}, skip merging`,
);
continue;
}
const { workspace } = project;
if (options.ignoreWorkspaceFilters || !workspace) {
project.tasks[name] = task;
continue;
}
const { inheritedTasks } = workspace;
if (!inheritedTasks) {
project.tasks[name] = task;
continue;
}
const { include, exclude, rename } = inheritedTasks;
if (include && !include.includes(name)) {
continue;
}
if (exclude && exclude.includes(name)) {
continue;
}
if (rename && name in rename) {
project.tasks[rename[name]] = task;
continue;
}
project.tasks[name] = task;
}
}
type QueryResult = {
tasks: Record<string, Record<string, Task>>;
options: object;
};

export async function scan() {
const tasks =
(await load<PartialInheritedTasksConfig>(".moon/tasks.yml")) ?? {};
const taggedTasks = await scanTaggedTasks();
const workspaces = await scanWorkspace();
const moonxfig = await load<{
"ignore-tasks": string[];
}>("moonx.yml", {
allowMissing: true,
placeholder: {
"ignore-tasks": [],
},
});

const res = moon(["query", "tasks", "--json"], {
stdout: "pipe",
stderr: "inherit",
stdin: "inherit",
});

const { tasks }: QueryResult = JSON.parse(res.stdout.toString());
const projects = Object.entries(tasks).map(
([name, tasks]) => [name, Object.keys(tasks)] as const,
);

// Map<task, Array<project>>
const commands = new Map<string, Array<string>>();

for (const [name, workspace] of workspaces) {
mergeTasks(name, workspace, tasks);
}
for (const [name, project] of workspaces) {
const { tags } = project;
if (!tags) {
continue;
}
for (const tag of tags) {
const inherited = taggedTasks.get(tag);
if (!inherited) {
logger.warn(`Tag ${tag} not found for project ${name}`);
for (const [name, tasks] of projects) {
for (const task of tasks) {
if (moonxfig["ignore-tasks"].includes(task)) {
continue;
}
mergeTasks(name, project, inherited, {
ignoreWorkspaceFilters: true,
});
}
}
const commands = new Map<string, Array<string>>();
for (const [name, project] of workspaces) {
for (const task in project.tasks) {
const command = commands.get(task) ?? [];
command.push(name);
commands.set(task, command);
}
}

return commands;
}
Loading

0 comments on commit acccf00

Please sign in to comment.