diff --git a/deno.jsonc b/deno.jsonc index c8bcc17..ec100ee 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -66,17 +66,36 @@ }, "demo:flags": { "description": "🎬 Demo: flags", - "task": "echo '$ $'", + "task": [ + "echo '0: $<0>' &&", + "echo '1: $<1>' &&", + "echo 'foo: $' &&", + "echo 'bar: $'" + ], + "args": [ + { + "alias": "0", + "required": true, + "description": "Example of required argument" + }, + { + "alias": "1", + "default": true, + "description": "Example of optional argument" + } + ], "flags": { "foo": { "alias": "f", - "required": true, - "description": "Example of a required flag" + "description": "Example of a standard flag" }, "bar": { "alias": "b", "default": "bar", - "description": "Example of a standard flag" + "description": "Example of a defaulted flag" + }, + "baz": { + "description": "Example of a non-aliased flag" } } }, diff --git a/deno.lock b/deno.lock index dc935a2..7c4eed8 100644 --- a/deno.lock +++ b/deno.lock @@ -13,6 +13,7 @@ }, "redirects": { "https://esm.sh/v132/@types/chai@~4.3/index.d.ts": "https://esm.sh/v132/@types/chai@4.3.8/index.d.ts", + "https://esm.sh/v133/@types/chai-as-promised@~7.1/index.d.ts": "https://esm.sh/v133/@types/chai-as-promised@7.1.8/index.d.ts", "https://esm.sh/v133/@types/chai@~4.3/index.d.ts": "https://esm.sh/v133/@types/chai@4.3.9/index.d.ts" }, "remote": { @@ -277,6 +278,7 @@ "https://deno.land/x/zod@v3.21.4/locales/en.ts": "a7a25cd23563ccb5e0eed214d9b31846305ddbcdb9c5c8f508b108943366ab4c", "https://deno.land/x/zod@v3.21.4/mod.ts": "64e55237cb4410e17d968cd08975566059f27638ebb0b86048031b987ba251c4", "https://deno.land/x/zod@v3.21.4/types.ts": "b5d061babea250de14fc63764df5b3afa24f2b088a1d797fc060ba49a0ddff28", + "https://esm.sh/chai-as-promised@7.1.1?pin=v133": "c94bdef9e8697e6cca465af6ec3437cea7cf53e44ae62e8a869f976d0ab3f2c7", "https://esm.sh/chai@4.3.10": "e413ea40e1248a0a06e8812f22ec265c4a41cb57829c19017af5cb1def62880f", "https://esm.sh/chai@4.3.10?pin=v133": "e25839044ba92464bd0194939276cc97cbdec7f42ad2c49609d0092eaac1c8e1", "https://esm.sh/string-argv@0.3.2": "ba1d5479e1ed6293e5a7ffd3297e0eb55a23a5d5d7ac71306ee829fba4243402", @@ -291,6 +293,7 @@ "https://esm.sh/v132/string-argv@0.3.2/denonext/string-argv.mjs": "a7977920da1d72bf89b1db36d2c24fdfa82df6ce4f6c2b59a930b641a6ba9b37", "https://esm.sh/v132/type-detect@4.0.8/denonext/type-detect.mjs": "deb58bd7203992249a5795f7da35d00b67077fe6c03019349cb614ba22ef52ad", "https://esm.sh/v133/assertion-error@1.1.0/denonext/assertion-error.mjs": "0a4a5dccfb89070dd1e09fad036e706aa51d9dd3236ab019aed08bef1841695b", + "https://esm.sh/v133/chai-as-promised@7.1.1/denonext/chai-as-promised.mjs": "bbcd90c4502fe553f17aa1923cb07c22cb2302c1da1d50a046796ab71b9f7f2e", "https://esm.sh/v133/chai@4.3.10/denonext/chai.mjs": "fa4ea11c224f9f3abc5272c8917c0c629ec5ae2bec2fafe4edee09dfddcc4f68", "https://esm.sh/v133/check-error@1.0.3/denonext/check-error.mjs": "04b0b4e7d4470a991f1211e35075d68ad3d96602236853f615527af0e889a265", "https://esm.sh/v133/deep-eql@4.1.3/denonext/deep-eql.mjs": "3f406af09e31cfb3d403689e277eb392ee18361ca682c26a3955db094ba94802", diff --git a/mod.ts b/mod.ts index f996871..84d8f40 100644 --- a/mod.ts +++ b/mod.ts @@ -183,7 +183,7 @@ const inspect = is.union([ listen: is.string().min(1).optional().transform((v) => v ? `--inspect='${v}'` : ""), break: is.string().min(1).optional().transform((v) => v ? `--inspect-brk='${v}'` : ""), wait: is.string().min(1).optional().transform((v) => v ? `--inspect-wait='${v}'` : ""), - }).transform((v) => Object.values(v ?? {}).filter(Boolean).join(" ")), + }).transform((v) => Object.values(v).filter(Boolean).join(" ")), ]).optional() /** Watch flags */ @@ -193,7 +193,7 @@ const watch = is.union([ is.object({ files: is.array(is.string()).optional().transform((v) => v?.length ? `--watch='${v.join(",")}'` : ""), clearScreen: is.boolean().optional().transform((v) => v === false ? "--no-clear-screen" : ""), - }).transform((v) => Object.values(v ?? {}).filter(Boolean).join(" ")), + }).transform((v) => Object.values(v).filter(Boolean).join(" ")), ]).optional() // Deno flags ========================================================================================================= @@ -387,12 +387,21 @@ const _make = is.object({ ) ).default(() => ({})), cwd: is.string().optional(), + args: is.array( + is.object({ + alias: is.string(), + default: is.unknown().optional(), + required: is.boolean().default(false), + description: is.string().default(""), + }).refine((value) => !(("default" in value) && (value.required)), { + message: "Cannot have default when value is required", + }), + ).default(() => []), 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(() => ({})), @@ -421,7 +430,7 @@ const _make = is.object({ /** Compute command to execute after applying deno flags */ export function command( raw: string, - { flags, deno, argv = [] }: Pick, "deno" | "flags"> & { argv?: string[] }, + { flags, args, deno, argv = [] }: Pick, "deno" | "flags" | "args"> & { argv?: string[] }, { colors = false, parseArgv = true } = {}, ) { for (const [subcommand, options] of Object.entries(deno)) { @@ -430,7 +439,7 @@ export function command( `deno ${subcommand} ${colors ? italic(underline(options)) : options}`, ) } - const { _: args, ...options } = parse(argv, { + const { _, ...options } = parse(argv, { alias: Object.fromEntries( Object.entries(flags).filter(([_, { alias }]) => alias).map(([key, { alias }]) => [alias, key]), ), @@ -440,21 +449,29 @@ export function command( ) => [key, options.default]), ), }) - for (const [key, { required }] of Object.entries(flags)) { - if (parseArgv && required && (!(key in options))) { - throw new ReferenceError(`Missing flag: ${key}`) - } + for (let i = 0; i < args.length; i++) { + const { alias, required, default: defaults } = args[i] if (parseArgv) { - raw = raw.replaceAll(`$<${key}>`, `${options[key]}`) - } else if (colors) { - raw = raw.replaceAll(`$<${key}>`, italic(underline(`$<${key}>`))) + if (required && (!(i in argv))) { + throw new ReferenceError(`Missing argument: ${alias}`) + } + raw = raw.replaceAll(`$<${alias}>`, `${argv[i] ?? defaults}`) + continue + } + if (colors) { + raw = raw + .replaceAll(`$<${alias}>`, italic(underline(`$<${alias}>`))) + .replaceAll(`$<${i}>`, italic(underline(`$<${alias}>`))) } } - for (let key = 0; key < args.length; key++) { + for (const alias of Object.keys(flags)) { if (parseArgv) { - raw = raw.replaceAll(`$<${key}>`, `${args[key]}`) - } else if (colors) { - raw = raw.replaceAll(`$<${key}>`, italic(underline(`$<${key}>`))) + raw = raw.replaceAll(`$<${alias}>`, `${options[alias]}`) + continue + } + if (colors) { + raw = raw + .replaceAll(`$<${alias}>`, italic(underline(`$<${alias}>`))) } } return raw @@ -486,11 +503,11 @@ export async function make( }), ) if (task) { - const { task: raw, env, deno, flags, cwd } = tasks[task] + const { task: raw, env, deno, flags, args, cwd } = tasks[task] const temp = ".deno-make.json" const decoder = new TextDecoder() try { - const make = command(raw, { deno, flags, argv }) + const make = command(raw, { deno, flags, args, argv }) await Deno.writeTextFile(temp, JSON.stringify({ tasks: { make } })) const process = new Deno.Command("deno", { args: ["task", ...(cwd ? ["--cwd", cwd] : []), "--config", temp, "make"], @@ -516,7 +533,7 @@ export async function make( } } else if (Object.keys(tasks).length) { for ( - const [name, { task, description, env, cwd, deno, flags }] of Object.entries( + const [name, { task, description, env, cwd, deno, flags, args }] of Object.entries( tasks, ) ) { @@ -531,22 +548,36 @@ export async function make( log( gray( ` ${k}=${v}${inherited ? underline(italic("→ inherited")) : ""}` - .trim(), + .trimEnd(), ), ) } } + if (args.length) { + log(magenta(`Arguments:`)) + for (const { alias, required, description, ...options } of args) { + log( + ` ${ + magenta( + `${required ? `<${alias}>` : `[${alias}${"default" in options ? `=${options.default}` : ""}]`}`.padEnd( + 24, + ), + ) + }${description}`, + ) + } + } if (Object.keys(flags).length) { log(magenta(`Flags:`)) - for (const [key, { alias, required, description, ...options }] of Object.entries(flags)) { + for (const [key, { alias, description, ...options }] of Object.entries(flags)) { log( - `${ + ` ${ magenta( - ` --${key}${alias ? `, -${alias}` : ""}${ - "default" in options ? italic(` [=${options.default}]`) : required ? italic(bold(` (required)`)) : "" - }`, + `${alias ? `-${alias},` : " "} --${key}${"default" in options ? `[=${options.default}]` : ""}`.padEnd( + 24, + ), ) - }\t${description}`, + }${description}`, ) } } @@ -555,7 +586,7 @@ export async function make( log(gray(` ${cwd}`)) } log(gray(`Task:`)) - log(gray(` ${command(task, { deno, flags }, { colors: true, parseArgv: false })}`)) + log(gray(` ${command(task, { deno, args, flags }, { colors: true, parseArgv: false })}`)) log("") } return { code: 0 } diff --git a/mod_test.ts b/mod_test.ts index 481ca52..ea29923 100644 --- a/mod_test.ts +++ b/mod_test.ts @@ -1,7 +1,9 @@ import { make } from "./mod.ts" import { expandGlob } from "https://deno.land/std@0.205.0/fs/mod.ts" import * as JSONC from "https://deno.land/std@0.205.0/jsonc/mod.ts" -import { expect } from "https://esm.sh/chai@4.3.10?pin=v133" +import chai from "https://esm.sh/chai@4.3.10?pin=v133" +import chaiAsPromised from "https://esm.sh/chai-as-promised@7.1.1?pin=v133" +const { expect } = chai.use(chaiAsPromised) for await (const { path, name: _name } of expandGlob("tests/*.jsonc")) { const name = _name.replace(".jsonc", "").replaceAll("_", " ") @@ -36,51 +38,36 @@ Deno.test("deno task make: print tasks", async () => { expect(code).to.equal(0) }) -Deno.test("deno task make: flags required", async () => { +Deno.test("deno task make: args", 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", + task: "make:args", argv: ["🦕"], config: "tests/deno_make.jsonc", log: (message) => stdio.push(message), stdio: "piped", exit: false, }) - expect(stdio.join("\n")).to.include("🦕") + expect(stdio.join("\n")).to.include("🦕").and.to.include("🦖") + expect(code).to.equal(0) +}) + +Deno.test("deno task make: missing args required throw", async () => { + await expect(make({ task: "make:args", argv: [], config: "tests/deno_make.jsonc", stdio: "null", exit: false })).to.be + .rejectedWith(Error, /missing argument/i) +}) + +Deno.test("deno task make: flags", async () => { + const stdio = [] as string[] + const { code } = await make({ + task: "make:flags", + argv: ["--foo", "🦕"], + config: "tests/deno_make.jsonc", + log: (message) => stdio.push(message), + stdio: "piped", + exit: false, + }) + expect(stdio.join("\n")).to.include("🦕").and.to.include("🦖") expect(code).to.equal(0) }) diff --git a/tests/deno_make.jsonc b/tests/deno_make.jsonc index 142b079..220490f 100644 --- a/tests/deno_make.jsonc +++ b/tests/deno_make.jsonc @@ -20,31 +20,32 @@ "TEST_INHERIT": true } }, - "make:flags_required": { - "task": "echo '$'", + "make:args": { + "task": "echo '$ / $'", "cwd": "tests", - "flags": { - "foo": { - "alias": "f", + "args": [ + { + "alias": "foo", "required": true + }, + { + "alias": "bar", + "default": "🦖" + }, + { + "alias": "baz" } - } + ] }, - "make:flags_defaults": { - "task": "echo '$'", + "make:flags": { + "task": "echo '$ / $'", "cwd": "tests", "flags": { "foo": { - "default": "🦕" - } - } - }, - "make:flags_positional": { - "task": "echo '$<0>'", - "cwd": "tests", - "flags": { - "unused": { - "description": "This flag is unused" + }, + "bar": { + "alias": "b", + "default": "🦖" } } },