Skip to content
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
4 changes: 2 additions & 2 deletions .codex/skills/writeJsDoc/assets/index-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Describe the behavior in one short paragraph.

@remarks Optional extra detail.

@see https://utils.duplojs.dev/en/v1/api/namespace/function
@see [`Alias.or.Other`](https://server-utils.duplojs.dev/en/v1/api/namespace/other)
@see https://server-utils.duplojs.dev/en/v0/api/namespace/function
@see [`Alias.or.Other`](https://server-utils.duplojs.dev/en/v0/api/namespace/other)

@namespace X
2 changes: 1 addition & 1 deletion integration/deno/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
},
"imports": {
"@duplojs/server-utils": "../../dist/index.mjs",
"@duplojs/utils": "npm:@duplojs/utils@1.5.1",
"@duplojs/utils": "npm:@duplojs/utils@1.6.3",
"@std/assert": "jsr:@std/assert@^1.0.0",
"@std/testing": "jsr:@std/testing@^1.0.0"
},
Expand Down
8 changes: 4 additions & 4 deletions integration/deno/deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions integration/node/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ exports[`node integration > command integration with nested help and execute > h
Seed a target
OPTIONS:
- force: -f, --force
force execution
force execution
SUBJECT:[string]"
`;

Expand All @@ -25,7 +25,7 @@ exports[`node integration > command integration with nested help and execute > h
Seed a target
OPTIONS:
- force: -f, --force
force execution
force execution
SUBJECT:[string]"
`;

Expand Down
6 changes: 3 additions & 3 deletions integration/node/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,23 +286,23 @@ describe("node integration", () => {
expect(logSpy).toHaveBeenCalled();
expect(
stripAnsiColor(
logSpy.mock.calls.map((call) => call.join("")).join("\n"),
logSpy.mock.calls.map(([message]) => String(message)).join("\n"),
),
).toMatchSnapshot("help root");

logSpy.mockClear();
await command.execute(["--help"]);
expect(
stripAnsiColor(
logSpy.mock.calls.map((call) => call.join("")).join("\n"),
logSpy.mock.calls.map(([message]) => String(message)).join("\n"),
),
).toMatchSnapshot("help command");

logSpy.mockClear();
await command.execute(["help-db", "--help"]);
expect(
stripAnsiColor(
logSpy.mock.calls.map((call) => call.join("")).join("\n"),
logSpy.mock.calls.map(([message]) => String(message)).join("\n"),
),
).toMatchSnapshot("help sub-command");

Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"@duplojs/dev-tools": "0.2.0"
},
"peerDependencies": {
"@duplojs/utils": ">=1.5.13 <2.0.0"
"@duplojs/utils": ">=1.6.3 <2.0.0"
},
"workspaces": [
"integration",
Expand Down
120 changes: 96 additions & 24 deletions scripts/command/create.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { type SimplifyTopLevel, type Kind, type AnyFunction, type RemoveKind, A, DP, type MaybePromise, O } from "@duplojs/utils";
import { type SimplifyTopLevel, type Kind, type AnyFunction, type RemoveKind, A, DP, E, unwrap, type MaybePromise, O } from "@duplojs/utils";
import { createBooleanOption, type Option } from "./options";
import { createDuplojsServerUtilsKind } from "@scripts/kind";
import type { EligibleDataParser } from "./types";
import { exitProcess } from "@scripts/common";
import { exitProcess } from "@scripts/common/exitProcess";
import { addIssue, addDataParserError, createError, interpretError, popErrorPath, setErrorPath, SymbolCommandError, type CommandError } from "./error";
import { logHelp } from "./help";
import { CommandManyArgumentsError } from "./errors";

export type Subject = (
| EligibleDataParser
Expand Down Expand Up @@ -32,6 +32,16 @@ export type Subject = (
>
);

function printError(commandError: CommandError, error?: CommandError): SymbolCommandError {
if (!error) {
// eslint-disable-next-line no-console
console.error(interpretError(commandError));
exitProcess(1);
}

return SymbolCommandError;
}

const commandKind = createDuplojsServerUtilsKind("command");

const helpOption = createBooleanOption("help", { aliases: ["h"] });
Expand All @@ -43,7 +53,7 @@ export interface Command extends Kind<
readonly description: string | null;
readonly subject: Subject | null | readonly Command[];
readonly options: readonly Option[];
execute(args: readonly string[]): MaybePromise<void>;
execute(args: readonly string[], error?: CommandError): Promise<undefined | SymbolCommandError>;
}

export interface CreateCommandParams<
Expand All @@ -60,11 +70,14 @@ export interface CreateCommandExecuteParams<
GenericSubject extends Subject,
> {
options: {
[GenericOptionName in GenericOptions[number]["name"]]: ReturnType<
Extract<
GenericOptions[number],
{ name: GenericOptionName }
>["execute"]
[GenericOptionName in GenericOptions[number]["name"]]: Exclude<
ReturnType<
Extract<
GenericOptions[number],
{ name: GenericOptionName }
>["execute"]
>,
SymbolCommandError
>["result"]
};
subject: DP.Output<GenericSubject>;
Expand Down Expand Up @@ -105,24 +118,38 @@ export function create(
description: params.description ?? null,
options: params.options ?? [],
subject: params.subject ?? null,
execute: async(args: readonly string[]) => {
execute: async(args, error?) => {
const commandError = error ?? createError(self.name);
const pathIndex = commandError.currentCommandPath.length;

if (self.subject instanceof Array) {
for (const command of self.subject) {
if (args[0] === command.name) {
await command.execute(A.shift(args));
let result: Awaited<ReturnType<typeof command.execute>> | undefined = undefined;

setErrorPath(commandError, command.name, pathIndex);
try {
result = await command.execute(A.shift(args), commandError);
} finally {
popErrorPath(commandError);
}

return;
if (result === SymbolCommandError) {
return printError(commandError, error);
}

return result;
}
}
}

const help = helpOption.execute(args);
const help = helpOption.execute(args, commandError);

if (help.result) {
if (help === SymbolCommandError) {
return printError(commandError, error);
} else if (help.result) {
logHelp(self);
exitProcess(0);

return;
return void exitProcess(0);
}

const commandOptions = A.reduce(
Expand All @@ -134,8 +161,12 @@ export function create(
options: {},
restArgs: args,
}),
({ element: option, lastValue, next }) => {
const optionResult = option.execute(lastValue.restArgs);
({ element: option, lastValue, next, exit }) => {
const optionResult = option.execute(lastValue.restArgs, commandError);

if (optionResult === SymbolCommandError) {
return exit(SymbolCommandError);
}

return next({
options: O.override(
Expand All @@ -149,31 +180,72 @@ export function create(
},
);

if (commandOptions === SymbolCommandError) {
return printError(commandError, error);
}

if (self.subject === null) {
await execute({ options: commandOptions.options });
} else if (
DP.identifier(self.subject, DP.arrayKind)
|| DP.identifier(self.subject, DP.tupleKind)
) {
const subjectResult = self.subject.parse(commandOptions.restArgs);

if (E.isLeft(subjectResult)) {
addDataParserError(
commandError,
unwrap(subjectResult),
{
type: "subject",
},
);

return printError(commandError, error);
}

await execute({
options: commandOptions.options,
subject: self.subject.parseOrThrow(commandOptions.restArgs),
subject: unwrap(subjectResult),
});
} else if (DP.identifier(self.subject, DP.dataParserKind)) {
if (commandOptions.restArgs.length > 1) {
throw new CommandManyArgumentsError(commandOptions.restArgs.length);
addIssue(
commandError,
{
type: "command",
expected: "exactly one subject argument",
received: commandOptions.restArgs,
message: `Expected exactly one subject argument, received ${commandOptions.restArgs.length}.`,
},
);

return printError(commandError, error);
}

const subjectResult = self.subject.parse(commandOptions.restArgs);

if (E.isLeft(subjectResult)) {
addDataParserError(
commandError,
unwrap(subjectResult),
{
type: "subject",
},
);

return printError(commandError, error);
}

await execute({
options: commandOptions.options,
subject: self.subject.parseOrThrow(commandOptions.restArgs),
subject: unwrap(subjectResult),
});
} else {
await execute({});
}
exitProcess(0);

return;
return void exitProcess(0);
},
} satisfies RemoveKind<Command>,
);
Expand Down
Loading
Loading