Skip to content

Commit 1a2ed20

Browse files
feat(ucmd): finish parse method
Signed-off-by: Henry Gressmann <[email protected]>
1 parent c67967d commit 1a2ed20

File tree

5 files changed

+121
-54
lines changed

5 files changed

+121
-54
lines changed

.changeset/eighty-wasps-hug.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ucmd": patch
3+
---
4+
5+
finish parse function

packages/ucmd/lib/index.test.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,61 @@ import { describe, expect, test } from "vitest";
22
import { createCommand, ucmd } from ".";
33
import type { CommandContext, Command } from ".";
44

5+
describe("parse commands", () => {
6+
test("ucmd", () => {
7+
let x = ucmd("example")
8+
.withBaseCommand({
9+
args: {
10+
foo: true,
11+
bar: false,
12+
},
13+
})
14+
.withCommand({
15+
name: "build",
16+
args: {
17+
test: {
18+
type: "string",
19+
default: "test",
20+
},
21+
foo: true,
22+
bar: false,
23+
},
24+
});
25+
26+
expect(x.parse(["build", "test1", "--foo", "--test", "test", "test2"])).toMatchInlineSnapshot(`
27+
{
28+
"res": {
29+
"positionals": [
30+
"test1",
31+
"test2",
32+
],
33+
"values": {
34+
"foo": true,
35+
"test": "test",
36+
},
37+
},
38+
"run": [Function],
39+
}
40+
`);
41+
42+
expect(x.parse(["test1", "--foo", "test", "test2"])).toMatchInlineSnapshot(`
43+
{
44+
"res": {
45+
"positionals": [
46+
"test1",
47+
"test",
48+
"test2",
49+
],
50+
"values": {
51+
"foo": true,
52+
},
53+
},
54+
"run": [Function],
55+
}
56+
`);
57+
});
58+
});
59+
560
describe("basic api", () => {
661
test("ucmd", () => {
762
let x = ucmd("example");
@@ -39,7 +94,8 @@ describe("basic api", () => {
3994
name: "build",
4095
args: {
4196
foo: true,
42-
bar: false,
97+
bar: {},
98+
baz: false,
4399
},
44100
});
45101

@@ -48,7 +104,8 @@ describe("basic api", () => {
48104
name: "build",
49105
args: {
50106
foo: true,
51-
bar: false,
107+
bar: {},
108+
baz: false,
52109
},
53110
};
54111

packages/ucmd/lib/index.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { parseArgs } from "node:util";
22

33
import type {
44
AddCommand,
5+
BaseCommand,
56
Command,
67
CommandArgs,
78
CommandContext,
@@ -10,7 +11,7 @@ import type {
1011
LiteralString,
1112
ucmdState,
1213
} from "./types";
13-
import { generateOptions, toCommandArgs, CommandArg } from "./utils";
14+
import { normalizeCommandArgs, NormalizedCommandArg, toParseArgOptions } from "./utils";
1415

1516
const defaultState: ucmdState<{}> = {
1617
name: "",
@@ -43,16 +44,19 @@ class UCMD<TCommands extends CommandsLike, TBaseCommand> {
4344
>(
4445
command:
4546
| LiteralString<TNewCommandName>
46-
| Partial<Command<TNewCommandArgs, LiteralString<TNewCommandName>>>
47-
| Command<TNewCommandArgs, LiteralString<TNewCommandName>>,
47+
| Command<TNewCommandArgs, LiteralString<TNewCommandName>>
48+
| Partial<Command<TNewCommandArgs, LiteralString<TNewCommandName>>>,
4849
run?: CommandFn<TNewCommandArgs extends infer T ? T : never>,
4950
): UCMD<TUpdatedCommands, TBaseCommand> {
5051
let newCMD = createCommand(command, run);
5152
Object.assign(this.#state.commands, { [newCMD.name]: newCMD });
5253
return this as unknown as UCMD<TUpdatedCommands, TBaseCommand>;
5354
}
5455

55-
withBaseCommand<TNewBaseCommand extends Command<CommandArgs, string>>(): UCMD<TCommands, TNewBaseCommand> {
56+
withBaseCommand<TNewBaseCommand extends BaseCommand<CommandArgs>>(
57+
baseCommand: TNewBaseCommand,
58+
): UCMD<TCommands, TNewBaseCommand> {
59+
(this.#state as unknown as { baseCommand: TNewBaseCommand }).baseCommand = baseCommand;
5660
return this as unknown as UCMD<TCommands, TNewBaseCommand>;
5761
}
5862

@@ -61,34 +65,32 @@ class UCMD<TCommands extends CommandsLike, TBaseCommand> {
6165

6266
let run: CommandFn<{}> = defaultCommand;
6367
let commandArgs = args || process.argv.slice(2);
64-
let command: string | undefined = undefined;
65-
let options: CommandArg[] = [];
68+
let command: string | undefined = commandArgs[0];
69+
let options: NormalizedCommandArg[] = [];
6670

6771
// Get the command
68-
if (commandArgs[0] && !commandArgs[0].startsWith("-")) {
69-
command = commandArgs[0];
72+
if (command && !command.startsWith("-") && this.#state.commands[command]) {
7073
commandArgs = commandArgs.slice(1);
7174

72-
if (!this.#state.commands[command]) return console.log("Command not found. Try --help");
7375
run = this.#state.commands[command]?.run ?? defaultCommand;
74-
options = toCommandArgs(this.#state.commands?.[command]?.args || []);
76+
options = normalizeCommandArgs(this.#state.commands?.[command]?.args || []);
7577
}
7678

7779
// If no command is specified, run the baseCommand
78-
else if (!command) {
80+
else {
7981
if (!this.#state.baseCommand) {
8082
console.log("No command specified");
8183
return;
8284
}
8385

8486
run = this.#state.baseCommand.run ?? defaultCommand;
85-
options = toCommandArgs(this.#state.baseCommand.args || []);
86-
return;
87+
options = normalizeCommandArgs(this.#state.baseCommand.args || []);
8788
}
8889

8990
let res = parseArgs({
9091
args: commandArgs,
91-
options: generateOptions(options),
92+
allowPositionals: true,
93+
options: toParseArgOptions(options),
9294
});
9395

9496
return { res, run };
@@ -103,7 +105,6 @@ class UCMD<TCommands extends CommandsLike, TBaseCommand> {
103105
}
104106

105107
export const ucmd = <T extends string>(name: T) => {
106-
console.log(name);
107108
return new UCMD().withName(name);
108109
};
109110

packages/ucmd/lib/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ export type CommandArgOptions = {
99
multiple?: boolean | undefined;
1010
};
1111

12-
export type CommandArgs<TCommandArgs extends Record<string, CommandArgOptions | true> = {}> = {
13-
[key in keyof TCommandArgs]: CommandArgOptions | true;
12+
export type CommandArgs<TCommandArgs extends Record<string, CommandArgOptions | boolean> = {}> = {
13+
[key in keyof TCommandArgs]: CommandArgOptions | boolean;
1414
};
1515

1616
export type BaseCommand<TCommandArguments extends CommandArgs> = {
1717
description?: string | undefined;
1818
run?: CommandFn<TCommandArguments>;
19-
args?: TCommandArguments | undefined;
19+
args?: Record<string, CommandArgOptions | boolean> | TCommandArguments;
2020
};
2121

2222
export type CommandLike = Command<CommandArgs, string>;

packages/ucmd/lib/utils.ts

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,47 @@
1-
import type { CommandArgOptions, ParseArgsOptionConfig } from "./types";
1+
import type { CommandArgOptions, CommandArgs, ParseArgsOptionConfig } from "./types";
22

3-
export type CommandArg = string | [string, CommandArgOptions] | (CommandArgOptions & { name: string });
3+
export type NormalizedCommandArg = CommandArgOptions & { name: string };
44

5-
export const toCommandArgs = (args: Record<string, CommandArgOptions | true> | CommandArg[]): CommandArg[] => {
5+
export const normalizeCommandArgs = (args: CommandArgs | NormalizedCommandArg[]): NormalizedCommandArg[] => {
66
if (Array.isArray(args)) return args;
77

8-
return Object.entries(args).map(([name, options]) => {
9-
if (typeof options === "boolean") return name;
10-
return {
11-
name,
12-
...options,
13-
};
14-
});
15-
};
16-
17-
export const generateOptions = (options: CommandArg[]): Record<string, ParseArgsOptionConfig> => {
18-
let opts = options.map((option) => {
19-
if (typeof option === "string")
20-
return {
21-
[option]: toParseArgOptions({}),
22-
};
23-
24-
if (Array.isArray(option))
25-
return {
26-
[option[0]]: toParseArgOptions(option[1]),
8+
return Object.entries(args)
9+
.map(([name, options]) => {
10+
if (options === false) return undefined;
11+
if (options === undefined || options === true) return { name };
12+
if (options === null || typeof options !== "object") throw new Error(`Invalid command arg options for ${name}`);
13+
let opts: CommandArgOptions = options;
14+
return <NormalizedCommandArg>{
15+
name,
16+
...opts,
2717
};
28-
29-
return {
30-
[option.name]: toParseArgOptions(option),
31-
};
32-
});
33-
34-
return Object.assign({}, ...opts);
18+
})
19+
.filter((arg) => arg !== undefined) as NormalizedCommandArg[];
3520
};
3621

37-
const toParseArgOptions = (options: CommandArgOptions): ParseArgsOptionConfig => {
38-
return {
39-
type: "string",
40-
default: options.default,
41-
short: options.short,
22+
export const toParseArgOptions = (options: NormalizedCommandArg[]): Record<string, ParseArgsOptionConfig> =>
23+
Object.assign(
24+
{},
25+
...options.map((option) => ({
26+
[option.name]: toParseArgOption(option),
27+
})),
28+
);
29+
30+
const toParseArgOption = (options: CommandArgOptions): ParseArgsOptionConfig => {
31+
let opt = <ParseArgsOptionConfig>{
32+
type: "boolean",
4233
};
34+
35+
if (options.default !== undefined) opt.default = options.default;
36+
if (options.type !== undefined) {
37+
if (options.type === "number") {
38+
opt.type = "string";
39+
} else {
40+
opt.type = options.type;
41+
}
42+
}
43+
if (options.short !== undefined) opt.short = options.short;
44+
if (options.multiple !== undefined) opt.multiple = options.multiple;
45+
46+
return opt;
4347
};

0 commit comments

Comments
 (0)