diff --git a/src/executor.ts b/src/executor.ts index 795e03a..1837178 100644 --- a/src/executor.ts +++ b/src/executor.ts @@ -1,7 +1,7 @@ import { TError, TFunction, TScript, TScripts } from "./types"; import child_process from "node:child_process"; import { isCI } from "ci-info"; -import { printCommand, printCommands } from "./help"; +import { Help } from "./help"; type Options = { excludeArgs?: true; @@ -54,13 +54,13 @@ class Executor { } if (Object.hasOwn(context, "$description")) { - printCommand(context, rest); + Help.printCommand(context, rest); console.log(); } console.log(`\x1b[1mTry one of the following:\x1b[0m`); - printCommands(context, rest, false); + Help.printCommands(context, rest, false); } else { console.error(`\x1b[31mScript '${path.join(".")}' not found\x1b[0m`); } diff --git a/src/help.ts b/src/help.ts index 3ffa967..101e7e3 100644 --- a/src/help.ts +++ b/src/help.ts @@ -1,131 +1,136 @@ import { TConfig, TScript } from "./types"; import minimist from "minimist"; -export function printHelp( - config: TConfig, - argv: minimist.ParsedArgs, - args: unknown, -): void { - const helps: string[] = []; - if (typeof args === "boolean") { - if (!args) return; - } else if (typeof args === "string" || typeof args === "number") { - helps.push(`${args}`); - } else if (Array.isArray(args)) { - helps.push(...(args as string[])); - } else { - return; - } +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +class Help { + static printHelp( + config: TConfig, + argv: minimist.ParsedArgs, + args: unknown, + ): void { + const helps: string[] = []; + if (typeof args === "boolean") { + if (!args) return; + } else if (typeof args === "string" || typeof args === "number") { + helps.push(`${args}`); + } else if (Array.isArray(args)) { + helps.push(...(args as string[])); + } else { + return; + } - helps.push(...argv._); + helps.push(...argv._); - if (helps.length === 0) { - //Printing all commands - console.log(`\n\x1b[1mAvailable commands:\x1b[0m`); - printCommands(config.scripts); - } else { - // TODO clean up this mess - for (const help of helps) { - console.log(`\n\x1b[1mAvailable commands: \x1b[90m${help}\x1b[0m`); - const path = help.split("."); + if (helps.length === 0) { + //Printing all commands + console.log(`\n\x1b[1mAvailable commands:\x1b[0m`); + Help.printCommands(config.scripts); + } else { + // TODO clean up this mess + for (const help of helps) { + console.log(`\n\x1b[1mAvailable commands: \x1b[90m${help}\x1b[0m`); + const path = help.split("."); - const todo: [TScript, string[], string[]][] = [ - [config.scripts, [], path], - ]; - while (todo.length > 0) { - const current = todo.shift(); - if (!current) continue; + const todo: [TScript, string[], string[]][] = [ + [config.scripts, [], path], + ]; + while (todo.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const current = todo.shift()!; - if (current[2].length === 0) { - printCommands(current[0], current[1]); - } else if (current[2][0] === "*") { - if (typeof current[0] === "object") { - if (Array.isArray(current[0])) { - for (let i = 0; i < current[0].length; i++) { - todo.push([current[0][i], [...current[1], i.toString()], []]); - } - } else { - for (const key in current[0]) { - if (Object.hasOwn(current[0], key)) { - todo.push([ - current[0][key], - [...current[1], key], - current[2].slice(1), - ]); + if (current[2].length === 0) { + Help.printCommands(current[0], current[1]); + } else if (current[2][0] === "*") { + if (typeof current[0] === "object") { + if (Array.isArray(current[0])) { + for (let i = 0; i < current[0].length; i++) { + todo.push([current[0][i], [...current[1], i.toString()], []]); + } + } else { + for (const key in current[0]) { + if (Object.hasOwn(current[0], key)) { + todo.push([ + current[0][key], + [...current[1], key], + current[2].slice(1), + ]); + } } } } - } - } else { - const sub = current[2][0]; + } else { + const sub = current[2][0]; - if (typeof current[0] === "object") { - if (Array.isArray(current[0])) { - const i = parseInt(sub); - todo.push([ - current[0][i], - [...current[1], i.toString()], - current[2].slice(1), - ]); - } else { - todo.push([ - current[0][sub], - [...current[1], sub], - current[2].slice(1), - ]); + if (typeof current[0] === "object") { + if (Array.isArray(current[0])) { + const i = parseInt(sub); + todo.push([ + current[0][i], + [...current[1], i.toString()], + current[2].slice(1), + ]); + } else { + todo.push([ + current[0][sub], + [...current[1], sub], + current[2].slice(1), + ]); + } } } } } } + process.exit(0); } - process.exit(0); -} -export function printCommands( - script: TScript | undefined, - path: string[] = [], - printSelf = true, -) { - if (Array.isArray(script)) { - script.forEach((s, i) => { - printCommand(s, [...path, i.toString()]); - }); - } else if (typeof script === "object") { - if (printSelf && path.length > 0) printCommand(script, [...path]); - for (const key in script) { - if (Object.hasOwn(script, key)) { - printCommand(script[key], [...path, key]); + static printCommands( + script: TScript | undefined, + path: string[] = [], + printSelf = true, + ) { + if (Array.isArray(script)) { + script.forEach((s, i) => { + Help.printCommand(s, [...path, i.toString()]); + }); + } else if (typeof script === "object") { + if (printSelf && path.length > 0) Help.printCommand(script, [...path]); + for (const key in script) { + if (Object.hasOwn(script, key)) { + Help.printCommand(script[key], [...path, key]); + } } + } else { + Help.printCommand(script, path); } - } else { - printCommand(script, path); } -} -export function printCommand(scripts: TScript | undefined, path: string[]) { - if (scripts === undefined) { - console.error(`\x1b[31mScript '${path.join(".")}' not found\x1b[0m`); - return; - } + static printCommand(scripts: TScript | undefined, path: string[]) { + if (scripts === undefined) { + console.error(`\x1b[31mScript '${path.join(".")}' not found\x1b[0m`); + return; + } - if (path[path.length - 1] === "$description") return; + if (path[path.length - 1] === "$description") return; - const prefix = `\x1b[92m${path.join(".")}\x1b[90m - \x1b[0m`; - let suffix: string; - if (typeof scripts === "string") { - suffix = scripts; - } else if (typeof scripts === "function") { - suffix = "\x1b[90m\x1b[0m"; - } else if (Array.isArray(scripts)) { - suffix = `\x1b[90m[${scripts.length}]\x1b[0m`; - } else { - if (Object.hasOwn(scripts, "$description")) { - suffix = `\x1b[90m${scripts.$description as string}\x1b[0m`; + const prefix = `\x1b[92m${path.join(".")}\x1b[90m - \x1b[0m`; + let suffix: string; + if (typeof scripts === "string") { + suffix = scripts; + } else if (typeof scripts === "function") { + suffix = "\x1b[90m\x1b[0m"; + } else if (Array.isArray(scripts)) { + suffix = `\x1b[90m[${scripts.length}]\x1b[0m`; } else { - suffix = `\x1b[90m{${Object.keys(scripts).join(", ")}}\x1b[0m`; + if (Object.hasOwn(scripts, "$description")) { + suffix = `\x1b[90m${scripts.$description as string}\x1b[0m`; + } else { + suffix = `\x1b[90m{${Object.keys(scripts).join(", ")}}\x1b[0m`; + } } - } - console.log(`${prefix}${suffix}`); + console.log(`${prefix}${suffix}`); + } } + +export { Help }; diff --git a/src/index.ts b/src/index.ts index e017fc4..0defed0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import * as fs from "node:fs"; import path from "path"; import { TScript, TError } from "./types"; -import { printHelp } from "./help"; +import { Help } from "./help"; import { loadConfig } from "./configLoader"; import { Executor } from "./executor"; @@ -17,8 +17,8 @@ process.argv = argv["--"] ?? []; const config = loadConfig(argv); async function main() { - printHelp(config, argv, argv.h); - printHelp(config, argv, argv.help); + Help.printHelp(config, argv, argv.h); + Help.printHelp(config, argv, argv.help); handleNoArgs(); @@ -51,7 +51,7 @@ function handleNoArgs() { return; } - printHelp(config, argv, true); + Help.printHelp(config, argv, true); } function addToPath(env: NodeJS.ProcessEnv, path: string | null): void { diff --git a/test/executor.spec.ts b/test/executor.spec.ts index 7bb258b..f25ae0c 100644 --- a/test/executor.spec.ts +++ b/test/executor.spec.ts @@ -4,6 +4,12 @@ import { Executor } from "../src/executor"; import sinon from "sinon"; import child_process from "node:child_process"; import { TError } from "../src/types"; +import { Help } from "../src/help"; + +sinon.restore(); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +let consoleLog = sinon.stub(console, "log"); +let consoleError = sinon.stub(console, "error"); function suite(name: string): ReturnType { const test = _suite(name); @@ -11,8 +17,8 @@ function suite(name: string): ReturnType { sinon.restore(); // Don't output anything - sinon.stub(console, "log"); - sinon.stub(console, "error"); + consoleLog = sinon.stub(console, "log"); + consoleError = sinon.stub(console, "error"); process.argv = []; }); return test; @@ -52,6 +58,75 @@ notFoundSuite("with ignoreNotFound option should not exit", () => { assert.equal(exit.callCount, 0); }); +notFoundSuite("with no options should print error", () => { + // Arrange + sinon.stub(process, "exit"); + + // Act + Executor.notFound(["test"], {}); + + // Assert + assert.equal(consoleError.callCount, 1); + assert.match(consoleError.args[0][0] as string, "Script 'test' not found"); +}); + +notFoundSuite("with context should print subscripts", () => { + // Arrange + sinon.stub(process, "exit"); + const printCommand = sinon.stub(Help, "printCommand"); + const printCommands = sinon.stub(Help, "printCommands"); + + // Act + Executor.notFound(["wow", "test"], {}, { r: "test" }); + + // Assert + assert.equal(consoleError.callCount, 1); + assert.match( + consoleError.args[0][0] as string, + "Script 'wow' does not have a 'test' script", + ); + assert.equal(printCommand.callCount, 0); + assert.equal(printCommands.callCount, 1); +}); + +notFoundSuite("with context should print description", () => { + // Arrange + sinon.stub(process, "exit"); + const printCommand = sinon.stub(Help, "printCommand"); + const printCommands = sinon.stub(Help, "printCommands"); + + // Act + Executor.notFound(["wow", "test"], {}, { r: "test", $description: "desc" }); + + // Assert + assert.equal(consoleError.callCount, 1); + assert.match( + consoleError.args[0][0] as string, + "Script 'wow' does not have a 'test' script", + ); + assert.equal(printCommand.callCount, 1); + assert.equal(printCommands.callCount, 1); +}); + +notFoundSuite("with context root should not say does not have", () => { + // Arrange + sinon.stub(process, "exit"); + const printCommand = sinon.stub(Help, "printCommand"); + const printCommands = sinon.stub(Help, "printCommands"); + + // Act + Executor.notFound(["test"], {}, { r: "test" }); + + // Assert + assert.equal(consoleError.callCount, 1); + assert.match( + consoleError.args[0][0] as string, + "Script 'test' does not exist", + ); + assert.equal(printCommand.callCount, 0); + assert.equal(printCommands.callCount, 1); +}); + notFoundSuite.run(); // endregion diff --git a/test/help.spec.ts b/test/help.spec.ts new file mode 100644 index 0000000..a0dd413 --- /dev/null +++ b/test/help.spec.ts @@ -0,0 +1,299 @@ +import { suite as _suite } from "uvu"; +import * as assert from "uvu/assert"; +import sinon from "sinon"; +import { Help } from "../src/help"; + +sinon.restore(); +let consoleLog = sinon.stub(console, "log"); +let consoleError = sinon.stub(console, "error"); + +function suite(name: string): ReturnType { + const test = _suite(name); + test.before.each(() => { + sinon.restore(); + + // Don't output anything + consoleLog = sinon.stub(console, "log"); + consoleError = sinon.stub(console, "error"); + process.argv = []; + }); + return test; +} + +const config = { + scripts: { + test: "echo test", + build: "echo build", + lint: ["echo lint", "echo lint2"], + format: "echo format", + sub: { + test: "echo sub.test", + build: "echo sub.build", + lint: "echo sub.lint", + }, + }, +}; + +//region printHelp +const printHelp = suite("printHelp"); + +printHelp("should not print anything if args is false", () => { + // Arrange + + // Act + Help.printHelp(config, { _: [] }, false); + + // Assert + assert.is(consoleLog.callCount, 0); +}); + +printHelp("should exit if args is true", () => { + // Arrange + const exit = sinon.stub(process, "exit"); + + // Act + Help.printHelp(config, { _: [] }, true); + + // Assert + assert.is(exit.callCount, 1); + assert.is(exit.getCall(0).args[0], 0); +}); + +printHelp("should print all root commands if args is true", () => { + // Arrange + sinon.stub(process, "exit"); + + // Act + Help.printHelp(config, { _: [] }, true); + + // Assert + assert.is(consoleLog.callCount, 6); + assert.match(consoleLog.getCall(0).args[0] as string, "Available commands:"); + assert.match(consoleLog.getCall(1).args[0] as string, "test"); + assert.match(consoleLog.getCall(2).args[0] as string, "build"); + assert.match(consoleLog.getCall(3).args[0] as string, "lint"); + assert.match(consoleLog.getCall(4).args[0] as string, "format"); + assert.match(consoleLog.getCall(5).args[0] as string, "sub"); +}); + +printHelp("should print sub commands of args", () => { + // Arrange + sinon.stub(process, "exit"); + + // Act + Help.printHelp(config, { _: [] }, "sub"); + + // Assert + assert.is(consoleLog.callCount, 5); + assert.match(consoleLog.getCall(0).args[0] as string, "Available commands:"); + assert.match(consoleLog.getCall(1).args[0] as string, "sub"); + assert.match(consoleLog.getCall(2).args[0] as string, "sub.test"); + assert.match(consoleLog.getCall(3).args[0] as string, "sub.build"); + assert.match(consoleLog.getCall(4).args[0] as string, "sub.lint"); +}); + +printHelp("should print sub commands of args with .*", () => { + // Arrange + sinon.stub(process, "exit"); + + // Act + Help.printHelp(config, { _: [] }, "sub.*"); + + // Assert + assert.is(consoleLog.callCount, 4); + assert.match(consoleLog.getCall(0).args[0] as string, "Available commands:"); + assert.match(consoleLog.getCall(1).args[0] as string, "sub.test"); + assert.match(consoleLog.getCall(2).args[0] as string, "sub.build"); + assert.match(consoleLog.getCall(3).args[0] as string, "sub.lint"); +}); + +printHelp("should print sub commands of args with *", () => { + // Arrange + sinon.stub(process, "exit"); + + // Act + Help.printHelp(config, { _: [] }, "*"); + + // Assert + assert.is(consoleLog.callCount, 10); + assert.match(consoleLog.getCall(0).args[0] as string, "Available commands:"); + assert.match(consoleLog.getCall(1).args[0] as string, "test"); + assert.match(consoleLog.getCall(2).args[0] as string, "build"); + assert.match(consoleLog.getCall(3).args[0] as string, "lint.0"); + assert.match(consoleLog.getCall(4).args[0] as string, "lint.1"); + assert.match(consoleLog.getCall(5).args[0] as string, "format"); + assert.match(consoleLog.getCall(6).args[0] as string, "sub"); + assert.match(consoleLog.getCall(7).args[0] as string, "sub.test"); + assert.match(consoleLog.getCall(8).args[0] as string, "sub.build"); + assert.match(consoleLog.getCall(9).args[0] as string, "sub.lint"); +}); + +printHelp("should print command if single response", () => { + // Arrange + sinon.stub(process, "exit"); + + // Act + Help.printHelp(config, { _: [] }, "test"); + + // Assert + assert.is(consoleLog.callCount, 2); + assert.match(consoleLog.getCall(0).args[0] as string, "Available commands:"); + assert.match(consoleLog.getCall(1).args[0] as string, "test"); +}); + +printHelp("should print command with array", () => { + // Arrange + sinon.stub(process, "exit"); + + // Act + Help.printHelp(config, { _: [] }, "lint"); + + // Assert + assert.is(consoleLog.callCount, 3); + assert.match(consoleLog.getCall(0).args[0] as string, "Available commands:"); + assert.match(consoleLog.getCall(1).args[0] as string, "lint.0"); + assert.match(consoleLog.getCall(2).args[0] as string, "lint.1"); +}); + +printHelp("should print command with array and index", () => { + // Arrange + sinon.stub(process, "exit"); + + // Act + Help.printHelp(config, { _: [] }, "lint.0"); + + // Assert + assert.is(consoleLog.callCount, 2); + assert.match(consoleLog.getCall(0).args[0] as string, "Available commands:"); + assert.match(consoleLog.getCall(1).args[0] as string, "lint.0"); +}); + +printHelp("should print command with array and *", () => { + // Arrange + sinon.stub(process, "exit"); + + // Act + Help.printHelp(config, { _: [] }, "lint.*"); + + // Assert + assert.is(consoleLog.callCount, 3); + assert.match(consoleLog.getCall(0).args[0] as string, "Available commands:"); + assert.match(consoleLog.getCall(1).args[0] as string, "lint.0"); + assert.match(consoleLog.getCall(2).args[0] as string, "lint.1"); +}); + +printHelp("should print commands with array args", () => { + // Arrange + sinon.stub(process, "exit"); + + // Act + Help.printHelp(config, { _: [] }, ["lint", "format"]); + + // Assert + assert.is(consoleLog.callCount, 5); + assert.match(consoleLog.getCall(0).args[0] as string, "Available commands:"); + assert.match(consoleLog.getCall(1).args[0] as string, "lint.0"); + assert.match(consoleLog.getCall(2).args[0] as string, "lint.1"); + assert.match(consoleLog.getCall(3).args[0] as string, "format"); +}); + +printHelp("should do nothing if undefined args", () => { + // Arrange + sinon.stub(process, "exit"); + + // Act + Help.printHelp(config, { _: [] }, undefined); + + // Assert + assert.is(consoleLog.callCount, 0); +}); + +printHelp.run(); +//endregion + +//region printCommand +const printCommand = suite("printCommand"); + +printCommand("should print not found if undefined", () => { + // Arrange + + // Act + Help.printCommand(undefined, []); + + // Assert + assert.is(consoleError.callCount, 1); + assert.match(consoleError.getCall(0).args[0] as string, "not found"); +}); + +printCommand("should not print $description standalone", () => { + // Arrange + + // Act + Help.printCommand({ $description: "test" }, ["$description"]); + + // Assert + assert.is(consoleLog.callCount, 0); +}); + +printCommand("should print $description if available", () => { + // Arrange + + // Act + Help.printCommand({ $description: "desc" }, ["test"]); + + // Assert + assert.is(consoleLog.callCount, 1); + assert.match(consoleLog.getCall(0).args[0] as string, "test"); + assert.match(consoleLog.getCall(0).args[0] as string, "desc"); +}); + +printCommand("should print command if string", () => { + // Arrange + + // Act + Help.printCommand("test", ["path"]); + + // Assert + assert.is(consoleLog.callCount, 1); + assert.match(consoleLog.getCall(0).args[0] as string, "test"); + assert.match(consoleLog.getCall(0).args[0] as string, "path"); +}); + +printCommand("should print command if array", () => { + // Arrange + + // Act + Help.printCommand(["test", "arg2", "arg3"], ["test", "path"]); + + // Assert + assert.is(consoleLog.callCount, 1); + assert.match(consoleLog.getCall(0).args[0] as string, "[3]"); + assert.match(consoleLog.getCall(0).args[0] as string, "test.path"); +}); + +printCommand("should print command if object", () => { + // Arrange + + // Act + Help.printCommand({ test: "test", sub: "dee" }, ["test", "path"]); + + // Assert + assert.is(consoleLog.callCount, 1); + assert.match(consoleLog.getCall(0).args[0] as string, "test.path"); + assert.match(consoleLog.getCall(0).args[0] as string, "{test, sub}"); +}); + +printCommand("should print command if function", () => { + // Arrange + + // Act + Help.printCommand(() => {}, ["olo", "path"]); + + // Assert + assert.is(consoleLog.callCount, 1); + assert.match(consoleLog.getCall(0).args[0] as string, "olo.path"); + assert.match(consoleLog.getCall(0).args[0] as string, ""); +}); + +printCommand.run(); +//endregion