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
5 changes: 2 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# AGENTS.md

An extension of the
[Deno Standard Library](https://github.com/denoland/deno_std), published on JSR
under the `@stdext` scope.
An extension of the [Deno Standard Library](https://github.com/denoland/std),
published on JSR under the `@stdext` scope.

## Repository layout

Expand Down
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# Deno Standard Library Extended

[![JSR @stdext](https://jsr.io/badges/@stdext)](https://jsr.io/@stdext)
[![codecov](https://codecov.io/gh/halvardssm/deno_stdext/graph/badge.svg?token=T1JEMGF8VW)](https://codecov.io/gh/halvardssm/deno_stdext)
[![ci](https://github.com/halvardssm/deno_stdext/actions/workflows/ci.yml/badge.svg)](https://github.com/halvardssm/deno_stdext/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/halvardssm/std-js/graph/badge.svg?token=T1JEMGF8VW)](https://codecov.io/gh/halvardssm/std-js)
[![ci](https://github.com/halvardssm/std-js/actions/workflows/ci.yml/badge.svg)](https://github.com/halvardssm/std-js/actions/workflows/ci.yml)

An extension of the
[Deno Standard Library](https://github.com/denoland/deno_std).
An extension of the [Deno Standard Library](https://github.com/denoland/std).

Multiple languages such as Rust, Go and PHP offer a standard library, which
allows the reduction of third-party libraries. This project is used to extend
Expand Down Expand Up @@ -46,6 +45,8 @@ console.log(dump(buffer));

- [assert](https://jsr.io/@stdext/assert): The assert package, contains
validators and assertions
- [cli](https://jsr.io/@stdext/cli): The cli package contains a command
framework and helpers for building command line applications
- [collections](https://jsr.io/@stdext/collections): The collections package
contains commonly used utilities and structures
- [crypto](https://jsr.io/@stdext/crypto): The crypto package contains utility
Expand Down Expand Up @@ -74,13 +75,15 @@ console.log(dump(buffer));
with fully inferred types
- [xml](https://jsr.io/@stdext/xml): The xml package provides XML parsing,
serialization, and XSD validation, backed by a WebAssembly implementation
- [zip](https://jsr.io/@stdext/zip): The zip package contains helpers for
creating and extracting zip archives

## Platform support

The packages are primarily developed and tested on macOS and Linux. Windows
support is not guaranteed, and packages may not work as expected on Windows. If
you encounter an issue on Windows, please
[report it](https://github.com/halvardssm/deno_stdext/issues).
[report it](https://github.com/halvardssm/std-js/issues).

## Versioning

Expand Down Expand Up @@ -125,7 +128,7 @@ more details.
## Deprecation Policy

We follow the
[Deno STD Deprecation Policy](https://github.com/denoland/deno_std?tab=readme-ov-file#deprecation-policy).
[Deno STD Deprecation Policy](https://github.com/denoland/std?tab=readme-ov-file#deprecation-policy).

When functionalities are adopted either by JavaScript language APIs, new Web
Standard APIs or Deno STD, we mark the functions as deprecated, and will remove
Expand All @@ -147,9 +150,9 @@ the license allows for it.
To bump versions and publish the packages to JSR, do the following:

1. Trigger the manual GitHub action
[version bump](https://github.com/halvardssm/stdext/actions/workflows/version_bump.yml).
[version bump](https://github.com/halvardssm/std-js/actions/workflows/version_bump.yml).
2. Check the created PR, verify the changes, and merge.
3. Create a [new release](https://github.com/halvardssm/stdext/releases/new),
3. Create a [new release](https://github.com/halvardssm/std-js/releases/new),
with the tag name `release-[date]` (e.g. `release-2024.12.29`) and auto
generate release notes.

Expand Down
97 changes: 97 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# @stdext/cli

Extends [@std/cli](https://jsr.io/@std/cli)

The cli package contains helpers for building command line applications.

## Commands

`defineCommand` and `runCommand` build a CLI from plain objects. The flags and
arguments given to `run` are typed from the definition.

```ts ignore
import { defineCommand, runCommand } from "@stdext/cli";

const serve = defineCommand({
name: "serve",
version: "2.1.0",
description: "Start the server",
options: {
port: { type: "string", alias: "p", default: "8000", env: "MYTOOL_PORT" },
config: { type: "string", required: true, env: "MYTOOL_CONFIG" },
dryRun: { type: "boolean" },
},
args: [{ name: "dir", required: true }],
run({ flags, args, rest, stdout }) {
// flags.port: string, flags.config: string, flags.dryRun: boolean
stdout(`Serving ${args.dir} on ${flags.port}`);
},
});

const cli = defineCommand({
name: "mytool",
version: "1.0.0",
description: "An example tool",
commands: [serve],
});

Deno.exit(await runCommand(cli, Deno.args));
```

```
$ mytool serve ./public --config app.toml -p 3000
$ MYTOOL_PORT=3000 mytool serve ./public --config app.toml
$ mytool serve --help
$ mytool serve --version
mytool 1.0.0
mytool serve 2.1.0
```

- **Options** are `string` or `boolean`, and are passed as `--name`, where a
camelCase name is written `--camel-case`. An `alias` is a single character
passed as `-p`.
- **Value order:** a flag takes its value from the arguments, then its `env`
variable, then its `default`. An empty variable counts as not set. Boolean
variables accept `1`, `true`, `yes` and `on`, and `0`, `false`, `no` and
`off`.
- **Required** string options fail when none of the three provides a value.
- **Arguments** are positional, and can be `required`, or `variadic` for the
last one. Everything after `--` is passed as `rest`, unparsed.
- **Subcommands** are chosen by the first arguments, so the options of a command
come after its name.
- **Help and version:** `--help` / `-h` show generated help for the command.
`--version` / `-V` print the version of the command, and of its parent
commands, one per line, e.g. `mytool 1.0.0` and `mytool serve 2.1.0`. Any
command can have a `version`, and commands without one are left out. Replace
the help with the `help` option of `runCommand`.
- **Help when empty:** set `helpOnEmpty: true` on a command to show its help,
and succeed, when it is called without any arguments or flags. This takes the
place of running the command, or failing on a missing required argument, or on
`Missing command` for a command with subcommands.
- **Suggestions:** an unknown command or `--option` is reported with the closest
known one, e.g. `Unknown command 'serv'. Did you mean 'serve'?`, when it is
within two edits. Configure it with the `suggest` option of `runCommand`:
`false` turns it off, and `{ maxDistance: 3 }` changes the distance.
- **Errors:** a usage error, such as an unknown flag or a missing required
option, is written to stderr and exits with code `2`, or the `usageExitCode`
option of `runCommand`. A command can throw `UsageError` as well. Other errors
are not caught.
- **Testing:** `runCommand` returns the exit code, and takes `stdout`, `stderr`
and `env` options, so commands can be tested without subprocesses.

## parseArgs

`parseArgs` is a drop in replacement for the one in `@std/cli`, with a
`required` option: a list of flags that throws when they are not present. The
flags in `required`, and in `default`, are not `undefined` in the type of the
result.

```ts
import { parseArgs } from "@stdext/cli/parse-args";

const args = parseArgs(["--name", "foo"], {
string: ["name"],
required: ["name"],
});
const name: string = args.name;
```
227 changes: 227 additions & 0 deletions cli/_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// Copyright 2018-2026 the Deno authors. MIT license.
// Types copied from `@std/cli/parse-args`, where they are internal and not
// exported. They are needed to infer the result of `parseArgs` from its options
// the same way as `@std/cli` does.

/** Combines recursively all intersection types and returns a new single type.
* @internal
*/
type Id<TRecord> = TRecord extends Record<string, unknown>
? TRecord extends infer InferredRecord
? { [Key in keyof InferredRecord]: Id<InferredRecord[Key]> }
: never
: TRecord;

/** Converts a union type `A | B | C` into an intersection type `A & B & C`.
* @internal
*/
type UnionToIntersection<TValue> =
(TValue extends unknown ? (args: TValue) => unknown : never) extends
(args: infer R) => unknown ? R extends Record<string, unknown> ? R : never
: never;

/** @internal */
export type BooleanType = boolean | string | undefined;
/** @internal */
export type StringType = string | undefined;
/** @internal */
type ArgType = StringType | BooleanType;

/** @internal */
export type Collectable = string | undefined;
/** @internal */
export type Negatable = string | undefined;

type UseTypes<
TBooleans extends BooleanType,
TStrings extends StringType,
TCollectable extends Collectable,
> = undefined extends (
& (false extends TBooleans ? undefined : TBooleans)
& TCollectable
& TStrings
) ? false
: true;

/**
* Creates a record with all available flags with the corresponding type and
* default type.
* @internal
*/
export type Values<
TBooleans extends BooleanType,
TStrings extends StringType,
TCollectable extends Collectable,
TNegatable extends Negatable,
TDefault extends Record<string, unknown> | undefined,
TAliases extends Aliases | undefined,
> = UseTypes<TBooleans, TStrings, TCollectable> extends true ?
& Record<string, unknown>
& AddAliases<
SpreadDefaults<
& CollectValues<TStrings, string, TCollectable, TNegatable>
& RecursiveRequired<CollectValues<TBooleans, boolean, TCollectable>>
& CollectUnknownValues<
TBooleans,
TStrings,
TCollectable,
TNegatable
>,
DedotRecord<TDefault>
>,
TAliases
>
// deno-lint-ignore no-explicit-any
: Record<string, any>;

/** @internal */
export type Aliases<TArgNames = string, TAliasNames extends string = string> =
Partial<
Record<Extract<TArgNames, string>, TAliasNames | ReadonlyArray<TAliasNames>>
>;

type AddAliases<
TArgs,
TAliases extends Aliases | undefined,
> = {
[TArgName in keyof TArgs as AliasNames<TArgName, TAliases>]: TArgs[TArgName];
};

type AliasNames<
TArgName,
TAliases extends Aliases | undefined,
> = TArgName extends keyof TAliases
? string extends TAliases[TArgName] ? TArgName
: TAliases[TArgName] extends string ? TArgName | TAliases[TArgName]
: TAliases[TArgName] extends Array<string>
? TArgName | TAliases[TArgName][number]
: TArgName
: TArgName;

/**
* Spreads all default values of Record `TDefaults` into Record `TArgs`
* and makes default values required.
*
* **Example:**
* `SpreadValues<{ foo?: boolean, bar?: number }, { foo: number }>`
*
* **Result:** `{ foo: boolean | number, bar?: number }`
*/
type SpreadDefaults<TArgs, TDefaults> = TDefaults extends undefined ? TArgs
: TArgs extends Record<string, unknown> ?
& Omit<TArgs, keyof TDefaults>
& {
[Default in keyof TDefaults]: Default extends keyof TArgs
? (TArgs[Default] & TDefaults[Default] | TDefaults[Default]) extends
Record<string, unknown>
? NonNullable<SpreadDefaults<TArgs[Default], TDefaults[Default]>>
: TDefaults[Default] | NonNullable<TArgs[Default]>
: unknown;
}
: never;

/**
* Defines the Record for the `default` option to add
* auto-suggestion support for IDE's.
* @internal
*/
type Defaults<TBooleans extends BooleanType, TStrings extends StringType> = Id<
UnionToIntersection<
& Record<string, unknown>
// Dedotted auto suggestions: { foo: { bar: unknown } }
& MapTypes<TStrings, unknown>
& MapTypes<TBooleans, unknown>
// Flat auto suggestions: { "foo.bar": unknown }
& MapDefaults<TBooleans>
& MapDefaults<TStrings>
>
>;

type MapDefaults<TArgNames extends ArgType> = Partial<
Record<TArgNames extends string ? TArgNames : string, unknown>
>;

type RecursiveRequired<TRecord> = TRecord extends Record<string, unknown> ? {
[Key in keyof TRecord]-?: RecursiveRequired<TRecord[Key]>;
}
: TRecord;

/** Same as `MapTypes` but also supports collectable options. */
type CollectValues<
TArgNames extends ArgType,
TType,
TCollectable extends Collectable,
TNegatable extends Negatable = undefined,
> = UnionToIntersection<
Extract<TArgNames, TCollectable> extends string ?
& (Exclude<TArgNames, TCollectable> extends never ? Record<never, never>
: MapTypes<Exclude<TArgNames, TCollectable>, TType, TNegatable>)
& (Extract<TArgNames, TCollectable> extends never ? Record<never, never>
: RecursiveRequired<
MapTypes<Extract<TArgNames, TCollectable>, Array<TType>, TNegatable>
>)
: MapTypes<TArgNames, TType, TNegatable>
>;

/** Same as `Record` but also supports dotted and negatable options. */
type MapTypes<
TArgNames extends ArgType,
TType,
TNegatable extends Negatable = undefined,
> = undefined extends TArgNames ? Record<never, never>
: TArgNames extends `${infer Name}.${infer Rest}` ? {
[Key in Name]?: MapTypes<
Rest,
TType,
TNegatable extends `${Name}.${infer Negate}` ? Negate : undefined
>;
}
: TArgNames extends string ? Partial<
Record<TArgNames, TNegatable extends TArgNames ? TType | false : TType>
>
: Record<never, never>;

type CollectUnknownValues<
TBooleans extends BooleanType,
TStrings extends StringType,
TCollectable extends Collectable,
TNegatable extends Negatable,
> = UnionToIntersection<
TCollectable extends TBooleans & TStrings ? Record<never, never>
: DedotRecord<
// Unknown collectable & non-negatable args.
& Record<
Exclude<
Extract<Exclude<TCollectable, TNegatable>, string>,
Extract<TStrings | TBooleans, string>
>,
Array<unknown>
>
// Unknown collectable & negatable args.
& Record<
Exclude<
Extract<Extract<TCollectable, TNegatable>, string>,
Extract<TStrings | TBooleans, string>
>,
Array<unknown> | false
>
>
>;

/** Converts `{ "foo.bar.baz": unknown }` into `{ foo: { bar: { baz: unknown } } }`. */
type DedotRecord<TRecord> = Record<string, unknown> extends TRecord ? TRecord
: TRecord extends Record<string, unknown> ? UnionToIntersection<
ValueOf<
{
[Key in keyof TRecord]: Key extends string ? Dedot<Key, TRecord[Key]>
: never;
}
>
>
: TRecord;

type Dedot<TKey extends string, TValue> = TKey extends
`${infer Name}.${infer Rest}` ? { [Key in Name]: Dedot<Rest, TValue> }
: { [Key in TKey]: TValue };

type ValueOf<TValue> = TValue[keyof TValue];
Loading
Loading