From e13766ac57eba0c94d35c99afb739d0fb00777d9 Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Wed, 8 Nov 2023 00:09:11 +0000 Subject: [PATCH] feat: add flags support --- deno.jsonc | 16 +++++++++ deno.lock | 1 + mod.ts | 81 +++++++++++++++++++++++++++++++++++++------ mod_test.ts | 48 +++++++++++++++++++++++++ tests/deno_make.jsonc | 28 +++++++++++++++ 5 files changed, 164 insertions(+), 10 deletions(-) diff --git a/deno.jsonc b/deno.jsonc index f2542c0..c8bcc17 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -64,6 +64,22 @@ } } }, + "demo:flags": { + "description": "🎬 Demo: flags", + "task": "echo '$ $'", + "flags": { + "foo": { + "alias": "f", + "required": true, + "description": "Example of a required flag" + }, + "bar": { + "alias": "b", + "default": "bar", + "description": "Example of a standard flag" + } + } + }, "ci": { "description": "🤖 CI checks", "task": [ diff --git a/deno.lock b/deno.lock index c02867e..dc935a2 100644 --- a/deno.lock +++ b/deno.lock @@ -177,6 +177,7 @@ "https://deno.land/std@0.204.0/path/windows/to_namespaced_path.ts": "e0f4d4a5e77f28a5708c1a33ff24360f35637ba6d8f103d19661255ef7bfd50d", "https://deno.land/std@0.205.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", "https://deno.land/std@0.205.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.205.0/flags/mod.ts": "0948466fc437f017f00c0b972a422b3dc3317a790bcf326429d23182977eaf9f", "https://deno.land/std@0.205.0/fmt/colors.ts": "c51c4642678eb690dcf5ffee5918b675bf01a33fba82acf303701ae1a4f8c8d9", "https://deno.land/std@0.205.0/fs/_util.ts": "fbf57dcdc9f7bc8128d60301eece608246971a7836a3bb1e78da75314f08b978", "https://deno.land/std@0.205.0/fs/copy.ts": "ca19e4837965914471df38fbd61e16f9e8adfe89f9cffb0c83615c83ea3fc2bf", diff --git a/mod.ts b/mod.ts index b786225..f996871 100644 --- a/mod.ts +++ b/mod.ts @@ -2,7 +2,16 @@ import * as JSONC from "https://deno.land/std@0.205.0/jsonc/mod.ts" import { z as is } from "https://deno.land/x/zod@v3.21.4/mod.ts" import { fromZodError } from "https://esm.sh/zod-validation-error@1.5.0?pin=v133" -import { bgBrightBlue, bold, gray, italic, underline, yellow } from "https://deno.land/std@0.205.0/fmt/colors.ts" +import { + bgBrightBlue, + bold, + gray, + italic, + magenta, + underline, + yellow, +} from "https://deno.land/std@0.205.0/fmt/colors.ts" +import { parse } from "https://deno.land/std@0.205.0/flags/mod.ts" // Structure flags ========================================================================================================= @@ -378,6 +387,15 @@ const _make = is.object({ ) ).default(() => ({})), cwd: is.string().optional(), + flags: is.record( + is.string(), + is.object({ + alias: is.string().optional(), + default: is.unknown().optional(), + required: is.boolean().default(false), + description: is.string().default(""), + }), + ).default(() => ({})), deno: is.object({ bench, bundle, @@ -403,15 +421,42 @@ const _make = is.object({ /** Compute command to execute after applying deno flags */ export function command( raw: string, - { deno }: Pick, "deno">, - { colors = false } = {}, + { flags, deno, argv = [] }: Pick, "deno" | "flags"> & { argv?: string[] }, + { colors = false, parseArgv = true } = {}, ) { - for (const [subcommand, flags] of Object.entries(deno)) { + for (const [subcommand, options] of Object.entries(deno)) { raw = raw.replaceAll( `deno ${subcommand}`, - `deno ${subcommand} ${colors ? italic(underline(flags)) : flags}`, + `deno ${subcommand} ${colors ? italic(underline(options)) : options}`, ) } + const { _: args, ...options } = parse(argv, { + alias: Object.fromEntries( + Object.entries(flags).filter(([_, { alias }]) => alias).map(([key, { alias }]) => [alias, key]), + ), + default: Object.fromEntries( + Object.entries(flags).filter(([_, options]) => "default" in options).map(( + [key, options], + ) => [key, options.default]), + ), + }) + for (const [key, { required }] of Object.entries(flags)) { + if (parseArgv && required && (!(key in options))) { + throw new ReferenceError(`Missing flag: ${key}`) + } + if (parseArgv) { + raw = raw.replaceAll(`$<${key}>`, `${options[key]}`) + } else if (colors) { + raw = raw.replaceAll(`$<${key}>`, italic(underline(`$<${key}>`))) + } + } + for (let key = 0; key < args.length; key++) { + if (parseArgv) { + raw = raw.replaceAll(`$<${key}>`, `${args[key]}`) + } else if (colors) { + raw = raw.replaceAll(`$<${key}>`, italic(underline(`$<${key}>`))) + } + } return raw } @@ -419,6 +464,7 @@ export function command( export async function make( { task = "", + argv = [] as string[], config = "deno.jsonc", log = console.log, exit = true, @@ -440,11 +486,11 @@ export async function make( }), ) if (task) { - const { task: raw, env, deno, cwd } = tasks[task] + const { task: raw, env, deno, flags, cwd } = tasks[task] const temp = ".deno-make.json" const decoder = new TextDecoder() try { - const make = command(raw, { deno }) + const make = command(raw, { deno, flags, argv }) await Deno.writeTextFile(temp, JSON.stringify({ tasks: { make } })) const process = new Deno.Command("deno", { args: ["task", ...(cwd ? ["--cwd", cwd] : []), "--config", temp, "make"], @@ -470,7 +516,7 @@ export async function make( } } else if (Object.keys(tasks).length) { for ( - const [name, { task, description, env, cwd, deno }] of Object.entries( + const [name, { task, description, env, cwd, deno, flags }] of Object.entries( tasks, ) ) { @@ -490,12 +536,26 @@ export async function make( ) } } + if (Object.keys(flags).length) { + log(magenta(`Flags:`)) + for (const [key, { alias, required, description, ...options }] of Object.entries(flags)) { + log( + `${ + magenta( + ` --${key}${alias ? `, -${alias}` : ""}${ + "default" in options ? italic(` [=${options.default}]`) : required ? italic(bold(` (required)`)) : "" + }`, + ) + }\t${description}`, + ) + } + } if (cwd) { log(gray(`Working directory:`)) log(gray(` ${cwd}`)) } log(gray(`Task:`)) - log(gray(` ${command(task, { deno }, { colors: true })}`)) + log(gray(` ${command(task, { deno, flags }, { colors: true, parseArgv: false })}`)) log("") } return { code: 0 } @@ -505,5 +565,6 @@ export async function make( } if (import.meta.main) { - await make({ task: Deno.args[0] }) + const [task, ...argv] = Deno.args + await make({ task, argv }) } diff --git a/mod_test.ts b/mod_test.ts index fe4fd8d..481ca52 100644 --- a/mod_test.ts +++ b/mod_test.ts @@ -36,6 +36,54 @@ Deno.test("deno task make: print tasks", async () => { expect(code).to.equal(0) }) +Deno.test("deno task make: flags required", async () => { + const stdio = [] as string[] + const { code } = await make({ + task: "make:flags_required", + argv: ["--foo", "🦕"], + config: "tests/deno_make.jsonc", + log: (message) => stdio.push(message), + stdio: "piped", + exit: false, + }) + expect(stdio.join("\n")).to.include("🦕") + expect(code).to.equal(0) +}) + +Deno.test.ignore("deno task make: flags required throw", async () => { + await expect(() => + make({ task: "make:flags_required", argv: [], config: "tests/deno_make.jsonc", stdio: "null", exit: false }) + ).to +}) + +Deno.test("deno task make: flags defaults", async () => { + const stdio = [] as string[] + const { code } = await make({ + task: "make:flags_defaults", + argv: [], + config: "tests/deno_make.jsonc", + log: (message) => stdio.push(message), + stdio: "piped", + exit: false, + }) + expect(stdio.join("\n")).to.include("🦕") + expect(code).to.equal(0) +}) + +Deno.test("deno task make: flags positional", async () => { + const stdio = [] as string[] + const { code } = await make({ + task: "make:flags_positional", + argv: ["🦕"], + config: "tests/deno_make.jsonc", + log: (message) => stdio.push(message), + stdio: "piped", + exit: false, + }) + expect(stdio.join("\n")).to.include("🦕") + expect(code).to.equal(0) +}) + Deno.test("deno task make: exit code", async () => { const { exit } = Deno try { diff --git a/tests/deno_make.jsonc b/tests/deno_make.jsonc index 630924f..142b079 100644 --- a/tests/deno_make.jsonc +++ b/tests/deno_make.jsonc @@ -20,6 +20,34 @@ "TEST_INHERIT": true } }, + "make:flags_required": { + "task": "echo '$'", + "cwd": "tests", + "flags": { + "foo": { + "alias": "f", + "required": true + } + } + }, + "make:flags_defaults": { + "task": "echo '$'", + "cwd": "tests", + "flags": { + "foo": { + "default": "🦕" + } + } + }, + "make:flags_positional": { + "task": "echo '$<0>'", + "cwd": "tests", + "flags": { + "unused": { + "description": "This flag is unused" + } + } + }, "make:multiline": { "task": ["deno help"], "cwd": "tests",