Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

change: task resolution #4

Merged
merged 7 commits into from
Nov 16, 2024
Merged
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
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
Loading